Repository files navigation

HeaderWright

Chrome Web Store

A small, dependency-free Chrome extension for setting, appending, and removing HTTP request headers, built on Manifest V3's declarativeNetRequest API only. No webRequest permission, ever. HeaderWright receives no request or response events: it hands declarative rules to Chrome, and Chrome applies them internally. That's verifiable by reading the manifest rather than trusting a description.

What it does

  • Header profiles: set / append / remove request headers
  • Per-profile domain scoping (subdomains included)
  • Master on/off toggle with a badge showing that toggle's state, plus a distinct state when Chrome rejects a rule registration
  • Deterministic JSON import/export — canonical key-sorted, byte-stable output, so configs can be shared and versioned in git

What it deliberately does not do (yet)

  • No response header modification — a later version
  • No account, no sync, no backend — profiles live in local extension storage
  • No telemetry

Install

Install from the Chrome Web Store

To run from source instead, see Development.

Permissions

HeaderWright does not request access to any site at install time. When you add a profile scoped to a domain, the browser prompts for permission on that domain and its subdomains only — one extra click per new domain, not once for everything.

Subdomains are part of the grant because they are part of the rule: a profile on example.com also applies to api.example.com, and the permission has to cover the same hosts the rule does. Through v0.1.3 it didn't, and headers silently failed to apply on subdomains — see finding 18.

Why:

  • Bounded blast radius. If the extension is ever compromised — a bad update, a hijacked account, a supply-chain slip — the damage is limited to domains you've actually granted, not every site you visit.

  • Revocable per site. Deleting a profile, editing its domains, or importing a config that drops a domain all release that domain's permission — unless another profile still references it. Verified against chrome.permissions.getAll(): the grant is gone immediately, not deferred. This holds for a partially-held grant too: a domain carrying only the pre-0.1.4 apex permission releases what it has rather than being skipped because it does not hold the complete set (finding 20).

  • What you see is what's true. HeaderWright's own popup is the honest display: a green dot means the grant is currently held, a gray dot means it isn't and the headers will not apply. Both are read from Chrome at render time rather than remembered.

    One exception, as of v0.1.5. A green dot means the GRANT is held, which is not quite the same as the headers applying. If two profiles write the same header on overlapping domains, neither applies — but the grant is still held, so the dots stay green, the domains are still counted in the "domains granted" total, the status line still reads "applying", and the badge still reads ON. The per-profile collision marker on each card is the only surface that reports it, and it is what to read. Closing the gap on the other four means the status line and badge reporting how many profiles were skipped, which is new signal rather than a corrected one, so it is queued rather than patched (findings 21, 22 and 26).

    One caveat, found while verifying this: the site list under chrome://extensions → Details is not a reliable view of what is currently granted. It has been observed listing a domain that chrome.permissions.getAll() reports as not granted. Trust the popup, or the API, over that panel.

  • Lighter store review. Broad host permissions draw more scrutiny from the Chrome Web Store; asking for nothing until it's needed avoids that by construction, not by explanation.

This costs one extra permission prompt the first time you scope a profile to a new domain. That's the tradeoff, made deliberately.

A note on the prompt's wording: Chrome phrases every host grant as "read and change your data" on the site. That describes the permission class, not this extension — with declarativeNetRequest only, the browser applies the rules itself and no extension code observes any request. The manifest is the proof.

A note on prompts that don't appear: re-adding a domain you previously removed may produce no dialog at all. The grant is still acquired — Chrome appears to suppress the prompt for a host you have already consented to for this extension. Nothing is hidden from you when this happens: the popup's dot, chrome.permissions.contains() and chrome.permissions.getAll() all agree that the grant is held.

How far that suppression reaches is only partly established. Uninstalling the extension does clear it — reinstalling and granting the same host prompts again, observed twice. Whether it is scoped to the browsing session or persists indefinitely for an install that stays in place is not yet verified; settling it needs a fresh browser profile, and it is recorded here as open rather than guessed at. The practical consequence either way is that the absence of a prompt is not evidence that a grant was skipped — read the dot.

Development

No build step — the extension/ directory is the extension. Load it unpacked from chrome://extensions with Developer mode on.

Run the selftests with node test/selftest.mjs from the repo root (no dependencies). The suite covers rule construction and the canonical export format, and ends with a check-count tripwire: adding or removing checks requires updating EXPECTED_CHECKS in the same commit.

The suite verifies construction, not application — a rule can be built correctly and still no-op on a domain without a permission grant, so release verification also includes a manual smoke test against a live endpoint. Those steps are written down in test/SMOKE.md rather than left to memory.

Defects found so far, what caused them, and what changed are recorded in FINDINGS.md — including the ones introduced by earlier fixes and the limitations that are known rather than solved.

Versioning

Each minor version adds exactly one feature; patch versions are fixes only, never features.

License

MIT — see LICENSE.

About

A small Chrome extension for setting, appending, and removing HTTP request headers by profile. Built entirely on Manifest V3's declarativeNetRequest — no webRequest permission, so nothing in it can read your traffic. Permissions are requested per-site, not all at once at install.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

HeaderWright

Chrome Web Store

A small, dependency-free Chrome extension for setting, appending, and removing HTTP request headers, built on Manifest V3's declarativeNetRequest API only. No webRequest permission, ever. HeaderWright receives no request or response events: it hands declarative rules to Chrome, and Chrome applies them internally. That's verifiable by reading the manifest rather than trusting a description.

What it does

  • Header profiles: set / append / remove request headers
  • Per-profile domain scoping (subdomains included)
  • Master on/off toggle with a badge showing that toggle's state, plus a distinct state when Chrome rejects a rule registration
  • Deterministic JSON import/export — canonical key-sorted, byte-stable output, so configs can be shared and versioned in git

What it deliberately does not do (yet)

  • No response header modification — a later version
  • No account, no sync, no backend — profiles live in local extension storage
  • No telemetry

Install

Install from the Chrome Web Store

To run from source instead, see Development.

Permissions

HeaderWright does not request access to any site at install time. When you add a profile scoped to a domain, the browser prompts for permission on that domain and its subdomains only — one extra click per new domain, not once for everything.

Subdomains are part of the grant because they are part of the rule: a profile on example.com also applies to api.example.com, and the permission has to cover the same hosts the rule does. Through v0.1.3 it didn't, and headers silently failed to apply on subdomains — see finding 18.

Why:

  • Bounded blast radius. If the extension is ever compromised — a bad update, a hijacked account, a supply-chain slip — the damage is limited to domains you've actually granted, not every site you visit.

  • Revocable per site. Deleting a profile, editing its domains, or importing a config that drops a domain all release that domain's permission — unless another profile still references it. Verified against chrome.permissions.getAll(): the grant is gone immediately, not deferred. This holds for a partially-held grant too: a domain carrying only the pre-0.1.4 apex permission releases what it has rather than being skipped because it does not hold the complete set (finding 20).

  • What you see is what's true. HeaderWright's own popup is the honest display: a green dot means the grant is currently held, a gray dot means it isn't and the headers will not apply. Both are read from Chrome at render time rather than remembered.

    One exception, as of v0.1.5. A green dot means the GRANT is held, which is not quite the same as the headers applying. If two profiles write the same header on overlapping domains, neither applies — but the grant is still held, so the dots stay green, the domains are still counted in the "domains granted" total, the status line still reads "applying", and the badge still reads ON. The per-profile collision marker on each card is the only surface that reports it, and it is what to read. Closing the gap on the other four means the status line and badge reporting how many profiles were skipped, which is new signal rather than a corrected one, so it is queued rather than patched (findings 21, 22 and 26).

    One caveat, found while verifying this: the site list under chrome://extensions → Details is not a reliable view of what is currently granted. It has been observed listing a domain that chrome.permissions.getAll() reports as not granted. Trust the popup, or the API, over that panel.

  • Lighter store review. Broad host permissions draw more scrutiny from the Chrome Web Store; asking for nothing until it's needed avoids that by construction, not by explanation.

This costs one extra permission prompt the first time you scope a profile to a new domain. That's the tradeoff, made deliberately.

A note on the prompt's wording: Chrome phrases every host grant as "read and change your data" on the site. That describes the permission class, not this extension — with declarativeNetRequest only, the browser applies the rules itself and no extension code observes any request. The manifest is the proof.

A note on prompts that don't appear: re-adding a domain you previously removed may produce no dialog at all. The grant is still acquired — Chrome appears to suppress the prompt for a host you have already consented to for this extension. Nothing is hidden from you when this happens: the popup's dot, chrome.permissions.contains() and chrome.permissions.getAll() all agree that the grant is held.

How far that suppression reaches is only partly established. Uninstalling the extension does clear it — reinstalling and granting the same host prompts again, observed twice. Whether it is scoped to the browsing session or persists indefinitely for an install that stays in place is not yet verified; settling it needs a fresh browser profile, and it is recorded here as open rather than guessed at. The practical consequence either way is that the absence of a prompt is not evidence that a grant was skipped — read the dot.

Development

No build step — the extension/ directory is the extension. Load it unpacked from chrome://extensions with Developer mode on.

Run the selftests with node test/selftest.mjs from the repo root (no dependencies). The suite covers rule construction and the canonical export format, and ends with a check-count tripwire: adding or removing checks requires updating EXPECTED_CHECKS in the same commit.

The suite verifies construction, not application — a rule can be built correctly and still no-op on a domain without a permission grant, so release verification also includes a manual smoke test against a live endpoint. Those steps are written down in test/SMOKE.md rather than left to memory.

Defects found so far, what caused them, and what changed are recorded in FINDINGS.md — including the ones introduced by earlier fixes and the limitations that are known rather than solved.

Versioning

Each minor version adds exactly one feature; patch versions are fixes only, never features.

License

MIT — see LICENSE.

About

A small Chrome extension for setting, appending, and removing HTTP request headers by profile. Built entirely on Manifest V3's declarativeNetRequest — no webRequest permission, so nothing in it can read your traffic. Permissions are requested per-site, not all at once at install.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

HeaderWright

Chrome Web Store

A small, dependency-free Chrome extension for setting, appending, and removing HTTP request headers, built on Manifest V3's declarativeNetRequest API only. No webRequest permission, ever. HeaderWright receives no request or response events: it hands declarative rules to Chrome, and Chrome applies them internally. That's verifiable by reading the manifest rather than trusting a description.

What it does

  • Header profiles: set / append / remove request headers
  • Per-profile domain scoping (subdomains included)
  • Master on/off toggle with a badge showing that toggle's state, plus a distinct state when Chrome rejects a rule registration
  • Deterministic JSON import/export — canonical key-sorted, byte-stable output, so configs can be shared and versioned in git

What it deliberately does not do (yet)

  • No response header modification — a later version
  • No account, no sync, no backend — profiles live in local extension storage
  • No telemetry

Install

Install from the Chrome Web Store

To run from source instead, see Development.

Permissions

HeaderWright does not request access to any site at install time. When you add a profile scoped to a domain, the browser prompts for permission on that domain and its subdomains only — one extra click per new domain, not once for everything.

Subdomains are part of the grant because they are part of the rule: a profile on example.com also applies to api.example.com, and the permission has to cover the same hosts the rule does. Through v0.1.3 it didn't, and headers silently failed to apply on subdomains — see finding 18.

Why:

  • Bounded blast radius. If the extension is ever compromised — a bad update, a hijacked account, a supply-chain slip — the damage is limited to domains you've actually granted, not every site you visit.

  • Revocable per site. Deleting a profile, editing its domains, or importing a config that drops a domain all release that domain's permission — unless another profile still references it. Verified against chrome.permissions.getAll(): the grant is gone immediately, not deferred. This holds for a partially-held grant too: a domain carrying only the pre-0.1.4 apex permission releases what it has rather than being skipped because it does not hold the complete set (finding 20).

  • What you see is what's true. HeaderWright's own popup is the honest display: a green dot means the grant is currently held, a gray dot means it isn't and the headers will not apply. Both are read from Chrome at render time rather than remembered.

    One exception, as of v0.1.5. A green dot means the GRANT is held, which is not quite the same as the headers applying. If two profiles write the same header on overlapping domains, neither applies — but the grant is still held, so the dots stay green, the domains are still counted in the "domains granted" total, the status line still reads "applying", and the badge still reads ON. The per-profile collision marker on each card is the only surface that reports it, and it is what to read. Closing the gap on the other four means the status line and badge reporting how many profiles were skipped, which is new signal rather than a corrected one, so it is queued rather than patched (findings 21, 22 and 26).

    One caveat, found while verifying this: the site list under chrome://extensions → Details is not a reliable view of what is currently granted. It has been observed listing a domain that chrome.permissions.getAll() reports as not granted. Trust the popup, or the API, over that panel.

  • Lighter store review. Broad host permissions draw more scrutiny from the Chrome Web Store; asking for nothing until it's needed avoids that by construction, not by explanation.

This costs one extra permission prompt the first time you scope a profile to a new domain. That's the tradeoff, made deliberately.

A note on the prompt's wording: Chrome phrases every host grant as "read and change your data" on the site. That describes the permission class, not this extension — with declarativeNetRequest only, the browser applies the rules itself and no extension code observes any request. The manifest is the proof.

A note on prompts that don't appear: re-adding a domain you previously removed may produce no dialog at all. The grant is still acquired — Chrome appears to suppress the prompt for a host you have already consented to for this extension. Nothing is hidden from you when this happens: the popup's dot, chrome.permissions.contains() and chrome.permissions.getAll() all agree that the grant is held.

How far that suppression reaches is only partly established. Uninstalling the extension does clear it — reinstalling and granting the same host prompts again, observed twice. Whether it is scoped to the browsing session or persists indefinitely for an install that stays in place is not yet verified; settling it needs a fresh browser profile, and it is recorded here as open rather than guessed at. The practical consequence either way is that the absence of a prompt is not evidence that a grant was skipped — read the dot.

Development

No build step — the extension/ directory is the extension. Load it unpacked from chrome://extensions with Developer mode on.

Run the selftests with node test/selftest.mjs from the repo root (no dependencies). The suite covers rule construction and the canonical export format, and ends with a check-count tripwire: adding or removing checks requires updating EXPECTED_CHECKS in the same commit.

The suite verifies construction, not application — a rule can be built correctly and still no-op on a domain without a permission grant, so release verification also includes a manual smoke test against a live endpoint. Those steps are written down in test/SMOKE.md rather than left to memory.

Defects found so far, what caused them, and what changed are recorded in FINDINGS.md — including the ones introduced by earlier fixes and the limitations that are known rather than solved.

Versioning

Each minor version adds exactly one feature; patch versions are fixes only, never features.

License

MIT — see LICENSE.

About

A small Chrome extension for setting, appending, and removing HTTP request headers by profile. Built entirely on Manifest V3's declarativeNetRequest — no webRequest permission, so nothing in it can read your traffic. Permissions are requested per-site, not all at once at install.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

HeaderWright

Chrome Web Store

A small, dependency-free Chrome extension for setting, appending, and removing HTTP request headers, built on Manifest V3's declarativeNetRequest API only. No webRequest permission, ever. HeaderWright receives no request or response events: it hands declarative rules to Chrome, and Chrome applies them internally. That's verifiable by reading the manifest rather than trusting a description.

What it does

  • Header profiles: set / append / remove request headers
  • Per-profile domain scoping (subdomains included)
  • Master on/off toggle with a badge showing that toggle's state, plus a distinct state when Chrome rejects a rule registration
  • Deterministic JSON import/export — canonical key-sorted, byte-stable output, so configs can be shared and versioned in git

What it deliberately does not do (yet)

  • No response header modification — a later version
  • No account, no sync, no backend — profiles live in local extension storage
  • No telemetry

Install

Install from the Chrome Web Store

To run from source instead, see Development.

Permissions

HeaderWright does not request access to any site at install time. When you add a profile scoped to a domain, the browser prompts for permission on that domain and its subdomains only — one extra click per new domain, not once for everything.

Subdomains are part of the grant because they are part of the rule: a profile on example.com also applies to api.example.com, and the permission has to cover the same hosts the rule does. Through v0.1.3 it didn't, and headers silently failed to apply on subdomains — see finding 18.

Why:

  • Bounded blast radius. If the extension is ever compromised — a bad update, a hijacked account, a supply-chain slip — the damage is limited to domains you've actually granted, not every site you visit.

  • Revocable per site. Deleting a profile, editing its domains, or importing a config that drops a domain all release that domain's permission — unless another profile still references it. Verified against chrome.permissions.getAll(): the grant is gone immediately, not deferred. This holds for a partially-held grant too: a domain carrying only the pre-0.1.4 apex permission releases what it has rather than being skipped because it does not hold the complete set (finding 20).

  • What you see is what's true. HeaderWright's own popup is the honest display: a green dot means the grant is currently held, a gray dot means it isn't and the headers will not apply. Both are read from Chrome at render time rather than remembered.

    One exception, as of v0.1.5. A green dot means the GRANT is held, which is not quite the same as the headers applying. If two profiles write the same header on overlapping domains, neither applies — but the grant is still held, so the dots stay green, the domains are still counted in the "domains granted" total, the status line still reads "applying", and the badge still reads ON. The per-profile collision marker on each card is the only surface that reports it, and it is what to read. Closing the gap on the other four means the status line and badge reporting how many profiles were skipped, which is new signal rather than a corrected one, so it is queued rather than patched (findings 21, 22 and 26).

    One caveat, found while verifying this: the site list under chrome://extensions → Details is not a reliable view of what is currently granted. It has been observed listing a domain that chrome.permissions.getAll() reports as not granted. Trust the popup, or the API, over that panel.

  • Lighter store review. Broad host permissions draw more scrutiny from the Chrome Web Store; asking for nothing until it's needed avoids that by construction, not by explanation.

This costs one extra permission prompt the first time you scope a profile to a new domain. That's the tradeoff, made deliberately.

A note on the prompt's wording: Chrome phrases every host grant as "read and change your data" on the site. That describes the permission class, not this extension — with declarativeNetRequest only, the browser applies the rules itself and no extension code observes any request. The manifest is the proof.

A note on prompts that don't appear: re-adding a domain you previously removed may produce no dialog at all. The grant is still acquired — Chrome appears to suppress the prompt for a host you have already consented to for this extension. Nothing is hidden from you when this happens: the popup's dot, chrome.permissions.contains() and chrome.permissions.getAll() all agree that the grant is held.

How far that suppression reaches is only partly established. Uninstalling the extension does clear it — reinstalling and granting the same host prompts again, observed twice. Whether it is scoped to the browsing session or persists indefinitely for an install that stays in place is not yet verified; settling it needs a fresh browser profile, and it is recorded here as open rather than guessed at. The practical consequence either way is that the absence of a prompt is not evidence that a grant was skipped — read the dot.

Development

No build step — the extension/ directory is the extension. Load it unpacked from chrome://extensions with Developer mode on.

Run the selftests with node test/selftest.mjs from the repo root (no dependencies). The suite covers rule construction and the canonical export format, and ends with a check-count tripwire: adding or removing checks requires updating EXPECTED_CHECKS in the same commit.

The suite verifies construction, not application — a rule can be built correctly and still no-op on a domain without a permission grant, so release verification also includes a manual smoke test against a live endpoint. Those steps are written down in test/SMOKE.md rather than left to memory.

Defects found so far, what caused them, and what changed are recorded in FINDINGS.md — including the ones introduced by earlier fixes and the limitations that are known rather than solved.

Versioning

Each minor version adds exactly one feature; patch versions are fixes only, never features.

License

MIT — see LICENSE.

About

A small Chrome extension for setting, appending, and removing HTTP request headers by profile. Built entirely on Manifest V3's declarativeNetRequest — no webRequest permission, so nothing in it can read your traffic. Permissions are requested per-site, not all at once at install.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

HeaderWright

Chrome Web Store

A small, dependency-free Chrome extension for setting, appending, and removing HTTP request headers, built on Manifest V3's declarativeNetRequest API only. No webRequest permission, ever. HeaderWright receives no request or response events: it hands declarative rules to Chrome, and Chrome applies them internally. That's verifiable by reading the manifest rather than trusting a description.

What it does

  • Header profiles: set / append / remove request headers
  • Per-profile domain scoping (subdomains included)
  • Master on/off toggle with a badge showing that toggle's state, plus a distinct state when Chrome rejects a rule registration
  • Deterministic JSON import/export — canonical key-sorted, byte-stable output, so configs can be shared and versioned in git

What it deliberately does not do (yet)

  • No response header modification — a later version
  • No account, no sync, no backend — profiles live in local extension storage
  • No telemetry

Install

Install from the Chrome Web Store

To run from source instead, see Development.

Permissions

HeaderWright does not request access to any site at install time. When you add a profile scoped to a domain, the browser prompts for permission on that domain and its subdomains only — one extra click per new domain, not once for everything.

Subdomains are part of the grant because they are part of the rule: a profile on example.com also applies to api.example.com, and the permission has to cover the same hosts the rule does. Through v0.1.3 it didn't, and headers silently failed to apply on subdomains — see finding 18.

Why:

  • Bounded blast radius. If the extension is ever compromised — a bad update, a hijacked account, a supply-chain slip — the damage is limited to domains you've actually granted, not every site you visit.

  • Revocable per site. Deleting a profile, editing its domains, or importing a config that drops a domain all release that domain's permission — unless another profile still references it. Verified against chrome.permissions.getAll(): the grant is gone immediately, not deferred. This holds for a partially-held grant too: a domain carrying only the pre-0.1.4 apex permission releases what it has rather than being skipped because it does not hold the complete set (finding 20).

  • What you see is what's true. HeaderWright's own popup is the honest display: a green dot means the grant is currently held, a gray dot means it isn't and the headers will not apply. Both are read from Chrome at render time rather than remembered.

    One exception, as of v0.1.5. A green dot means the GRANT is held, which is not quite the same as the headers applying. If two profiles write the same header on overlapping domains, neither applies — but the grant is still held, so the dots stay green, the domains are still counted in the "domains granted" total, the status line still reads "applying", and the badge still reads ON. The per-profile collision marker on each card is the only surface that reports it, and it is what to read. Closing the gap on the other four means the status line and badge reporting how many profiles were skipped, which is new signal rather than a corrected one, so it is queued rather than patched (findings 21, 22 and 26).

    One caveat, found while verifying this: the site list under chrome://extensions → Details is not a reliable view of what is currently granted. It has been observed listing a domain that chrome.permissions.getAll() reports as not granted. Trust the popup, or the API, over that panel.

  • Lighter store review. Broad host permissions draw more scrutiny from the Chrome Web Store; asking for nothing until it's needed avoids that by construction, not by explanation.

This costs one extra permission prompt the first time you scope a profile to a new domain. That's the tradeoff, made deliberately.

A note on the prompt's wording: Chrome phrases every host grant as "read and change your data" on the site. That describes the permission class, not this extension — with declarativeNetRequest only, the browser applies the rules itself and no extension code observes any request. The manifest is the proof.

A note on prompts that don't appear: re-adding a domain you previously removed may produce no dialog at all. The grant is still acquired — Chrome appears to suppress the prompt for a host you have already consented to for this extension. Nothing is hidden from you when this happens: the popup's dot, chrome.permissions.contains() and chrome.permissions.getAll() all agree that the grant is held.

How far that suppression reaches is only partly established. Uninstalling the extension does clear it — reinstalling and granting the same host prompts again, observed twice. Whether it is scoped to the browsing session or persists indefinitely for an install that stays in place is not yet verified; settling it needs a fresh browser profile, and it is recorded here as open rather than guessed at. The practical consequence either way is that the absence of a prompt is not evidence that a grant was skipped — read the dot.

Development

No build step — the extension/ directory is the extension. Load it unpacked from chrome://extensions with Developer mode on.

Run the selftests with node test/selftest.mjs from the repo root (no dependencies). The suite covers rule construction and the canonical export format, and ends with a check-count tripwire: adding or removing checks requires updating EXPECTED_CHECKS in the same commit.

The suite verifies construction, not application — a rule can be built correctly and still no-op on a domain without a permission grant, so release verification also includes a manual smoke test against a live endpoint. Those steps are written down in test/SMOKE.md rather than left to memory.

Defects found so far, what caused them, and what changed are recorded in FINDINGS.md — including the ones introduced by earlier fixes and the limitations that are known rather than solved.

Versioning

Each minor version adds exactly one feature; patch versions are fixes only, never features.

License

MIT — see LICENSE.

About

A small Chrome extension for setting, appending, and removing HTTP request headers by profile. Built entirely on Manifest V3's declarativeNetRequest — no webRequest permission, so nothing in it can read your traffic. Permissions are requested per-site, not all at once at install.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

HeaderWright

Chrome Web Store

A small, dependency-free Chrome extension for setting, appending, and removing HTTP request headers, built on Manifest V3's declarativeNetRequest API only. No webRequest permission, ever. HeaderWright receives no request or response events: it hands declarative rules to Chrome, and Chrome applies them internally. That's verifiable by reading the manifest rather than trusting a description.

What it does

  • Header profiles: set / append / remove request headers
  • Per-profile domain scoping (subdomains included)
  • Master on/off toggle with a badge showing that toggle's state, plus a distinct state when Chrome rejects a rule registration
  • Deterministic JSON import/export — canonical key-sorted, byte-stable output, so configs can be shared and versioned in git

What it deliberately does not do (yet)

  • No response header modification — a later version
  • No account, no sync, no backend — profiles live in local extension storage
  • No telemetry

Install

Install from the Chrome Web Store

To run from source instead, see Development.

Permissions

HeaderWright does not request access to any site at install time. When you add a profile scoped to a domain, the browser prompts for permission on that domain and its subdomains only — one extra click per new domain, not once for everything.

Subdomains are part of the grant because they are part of the rule: a profile on example.com also applies to api.example.com, and the permission has to cover the same hosts the rule does. Through v0.1.3 it didn't, and headers silently failed to apply on subdomains — see finding 18.

Why:

  • Bounded blast radius. If the extension is ever compromised — a bad update, a hijacked account, a supply-chain slip — the damage is limited to domains you've actually granted, not every site you visit.

  • Revocable per site. Deleting a profile, editing its domains, or importing a config that drops a domain all release that domain's permission — unless another profile still references it. Verified against chrome.permissions.getAll(): the grant is gone immediately, not deferred. This holds for a partially-held grant too: a domain carrying only the pre-0.1.4 apex permission releases what it has rather than being skipped because it does not hold the complete set (finding 20).

  • What you see is what's true. HeaderWright's own popup is the honest display: a green dot means the grant is currently held, a gray dot means it isn't and the headers will not apply. Both are read from Chrome at render time rather than remembered.

    One exception, as of v0.1.5. A green dot means the GRANT is held, which is not quite the same as the headers applying. If two profiles write the same header on overlapping domains, neither applies — but the grant is still held, so the dots stay green, the domains are still counted in the "domains granted" total, the status line still reads "applying", and the badge still reads ON. The per-profile collision marker on each card is the only surface that reports it, and it is what to read. Closing the gap on the other four means the status line and badge reporting how many profiles were skipped, which is new signal rather than a corrected one, so it is queued rather than patched (findings 21, 22 and 26).

    One caveat, found while verifying this: the site list under chrome://extensions → Details is not a reliable view of what is currently granted. It has been observed listing a domain that chrome.permissions.getAll() reports as not granted. Trust the popup, or the API, over that panel.

  • Lighter store review. Broad host permissions draw more scrutiny from the Chrome Web Store; asking for nothing until it's needed avoids that by construction, not by explanation.

This costs one extra permission prompt the first time you scope a profile to a new domain. That's the tradeoff, made deliberately.

A note on the prompt's wording: Chrome phrases every host grant as "read and change your data" on the site. That describes the permission class, not this extension — with declarativeNetRequest only, the browser applies the rules itself and no extension code observes any request. The manifest is the proof.

A note on prompts that don't appear: re-adding a domain you previously removed may produce no dialog at all. The grant is still acquired — Chrome appears to suppress the prompt for a host you have already consented to for this extension. Nothing is hidden from you when this happens: the popup's dot, chrome.permissions.contains() and chrome.permissions.getAll() all agree that the grant is held.

How far that suppression reaches is only partly established. Uninstalling the extension does clear it — reinstalling and granting the same host prompts again, observed twice. Whether it is scoped to the browsing session or persists indefinitely for an install that stays in place is not yet verified; settling it needs a fresh browser profile, and it is recorded here as open rather than guessed at. The practical consequence either way is that the absence of a prompt is not evidence that a grant was skipped — read the dot.

Development

No build step — the extension/ directory is the extension. Load it unpacked from chrome://extensions with Developer mode on.

Run the selftests with node test/selftest.mjs from the repo root (no dependencies). The suite covers rule construction and the canonical export format, and ends with a check-count tripwire: adding or removing checks requires updating EXPECTED_CHECKS in the same commit.

The suite verifies construction, not application — a rule can be built correctly and still no-op on a domain without a permission grant, so release verification also includes a manual smoke test against a live endpoint. Those steps are written down in test/SMOKE.md rather than left to memory.

Defects found so far, what caused them, and what changed are recorded in FINDINGS.md — including the ones introduced by earlier fixes and the limitations that are known rather than solved.

Versioning

Each minor version adds exactly one feature; patch versions are fixes only, never features.

License

MIT — see LICENSE.

About

A small Chrome extension for setting, appending, and removing HTTP request headers by profile. Built entirely on Manifest V3's declarativeNetRequest — no webRequest permission, so nothing in it can read your traffic. Permissions are requested per-site, not all at once at install.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

HeaderWright

Chrome Web Store

A small, dependency-free Chrome extension for setting, appending, and removing HTTP request headers, built on Manifest V3's declarativeNetRequest API only. No webRequest permission, ever. HeaderWright receives no request or response events: it hands declarative rules to Chrome, and Chrome applies them internally. That's verifiable by reading the manifest rather than trusting a description.

What it does

  • Header profiles: set / append / remove request headers
  • Per-profile domain scoping (subdomains included)
  • Master on/off toggle with a badge showing that toggle's state, plus a distinct state when Chrome rejects a rule registration
  • Deterministic JSON import/export — canonical key-sorted, byte-stable output, so configs can be shared and versioned in git

What it deliberately does not do (yet)

  • No response header modification — a later version
  • No account, no sync, no backend — profiles live in local extension storage
  • No telemetry

Install

Install from the Chrome Web Store

To run from source instead, see Development.

Permissions

HeaderWright does not request access to any site at install time. When you add a profile scoped to a domain, the browser prompts for permission on that domain and its subdomains only — one extra click per new domain, not once for everything.

Subdomains are part of the grant because they are part of the rule: a profile on example.com also applies to api.example.com, and the permission has to cover the same hosts the rule does. Through v0.1.3 it didn't, and headers silently failed to apply on subdomains — see finding 18.

Why:

  • Bounded blast radius. If the extension is ever compromised — a bad update, a hijacked account, a supply-chain slip — the damage is limited to domains you've actually granted, not every site you visit.

  • Revocable per site. Deleting a profile, editing its domains, or importing a config that drops a domain all release that domain's permission — unless another profile still references it. Verified against chrome.permissions.getAll(): the grant is gone immediately, not deferred. This holds for a partially-held grant too: a domain carrying only the pre-0.1.4 apex permission releases what it has rather than being skipped because it does not hold the complete set (finding 20).

  • What you see is what's true. HeaderWright's own popup is the honest display: a green dot means the grant is currently held, a gray dot means it isn't and the headers will not apply. Both are read from Chrome at render time rather than remembered.

    One exception, as of v0.1.5. A green dot means the GRANT is held, which is not quite the same as the headers applying. If two profiles write the same header on overlapping domains, neither applies — but the grant is still held, so the dots stay green, the domains are still counted in the "domains granted" total, the status line still reads "applying", and the badge still reads ON. The per-profile collision marker on each card is the only surface that reports it, and it is what to read. Closing the gap on the other four means the status line and badge reporting how many profiles were skipped, which is new signal rather than a corrected one, so it is queued rather than patched (findings 21, 22 and 26).

    One caveat, found while verifying this: the site list under chrome://extensions → Details is not a reliable view of what is currently granted. It has been observed listing a domain that chrome.permissions.getAll() reports as not granted. Trust the popup, or the API, over that panel.

  • Lighter store review. Broad host permissions draw more scrutiny from the Chrome Web Store; asking for nothing until it's needed avoids that by construction, not by explanation.

This costs one extra permission prompt the first time you scope a profile to a new domain. That's the tradeoff, made deliberately.

A note on the prompt's wording: Chrome phrases every host grant as "read and change your data" on the site. That describes the permission class, not this extension — with declarativeNetRequest only, the browser applies the rules itself and no extension code observes any request. The manifest is the proof.

A note on prompts that don't appear: re-adding a domain you previously removed may produce no dialog at all. The grant is still acquired — Chrome appears to suppress the prompt for a host you have already consented to for this extension. Nothing is hidden from you when this happens: the popup's dot, chrome.permissions.contains() and chrome.permissions.getAll() all agree that the grant is held.

How far that suppression reaches is only partly established. Uninstalling the extension does clear it — reinstalling and granting the same host prompts again, observed twice. Whether it is scoped to the browsing session or persists indefinitely for an install that stays in place is not yet verified; settling it needs a fresh browser profile, and it is recorded here as open rather than guessed at. The practical consequence either way is that the absence of a prompt is not evidence that a grant was skipped — read the dot.

Development

No build step — the extension/ directory is the extension. Load it unpacked from chrome://extensions with Developer mode on.

Run the selftests with node test/selftest.mjs from the repo root (no dependencies). The suite covers rule construction and the canonical export format, and ends with a check-count tripwire: adding or removing checks requires updating EXPECTED_CHECKS in the same commit.

The suite verifies construction, not application — a rule can be built correctly and still no-op on a domain without a permission grant, so release verification also includes a manual smoke test against a live endpoint. Those steps are written down in test/SMOKE.md rather than left to memory.

Defects found so far, what caused them, and what changed are recorded in FINDINGS.md — including the ones introduced by earlier fixes and the limitations that are known rather than solved.

Versioning

Each minor version adds exactly one feature; patch versions are fixes only, never features.

License

MIT — see LICENSE.

About

A small Chrome extension for setting, appending, and removing HTTP request headers by profile. Built entirely on Manifest V3's declarativeNetRequest — no webRequest permission, so nothing in it can read your traffic. Permissions are requested per-site, not all at once at install.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

HeaderWright

Chrome Web Store

A small, dependency-free Chrome extension for setting, appending, and removing HTTP request headers, built on Manifest V3's declarativeNetRequest API only. No webRequest permission, ever. HeaderWright receives no request or response events: it hands declarative rules to Chrome, and Chrome applies them internally. That's verifiable by reading the manifest rather than trusting a description.

What it does

  • Header profiles: set / append / remove request headers
  • Per-profile domain scoping (subdomains included)
  • Master on/off toggle with a badge showing that toggle's state, plus a distinct state when Chrome rejects a rule registration
  • Deterministic JSON import/export — canonical key-sorted, byte-stable output, so configs can be shared and versioned in git

What it deliberately does not do (yet)

  • No response header modification — a later version
  • No account, no sync, no backend — profiles live in local extension storage
  • No telemetry

Install

Install from the Chrome Web Store

To run from source instead, see Development.

Permissions

HeaderWright does not request access to any site at install time. When you add a profile scoped to a domain, the browser prompts for permission on that domain and its subdomains only — one extra click per new domain, not once for everything.

Subdomains are part of the grant because they are part of the rule: a profile on example.com also applies to api.example.com, and the permission has to cover the same hosts the rule does. Through v0.1.3 it didn't, and headers silently failed to apply on subdomains — see finding 18.

Why:

  • Bounded blast radius. If the extension is ever compromised — a bad update, a hijacked account, a supply-chain slip — the damage is limited to domains you've actually granted, not every site you visit.

  • Revocable per site. Deleting a profile, editing its domains, or importing a config that drops a domain all release that domain's permission — unless another profile still references it. Verified against chrome.permissions.getAll(): the grant is gone immediately, not deferred. This holds for a partially-held grant too: a domain carrying only the pre-0.1.4 apex permission releases what it has rather than being skipped because it does not hold the complete set (finding 20).

  • What you see is what's true. HeaderWright's own popup is the honest display: a green dot means the grant is currently held, a gray dot means it isn't and the headers will not apply. Both are read from Chrome at render time rather than remembered.

    One exception, as of v0.1.5. A green dot means the GRANT is held, which is not quite the same as the headers applying. If two profiles write the same header on overlapping domains, neither applies — but the grant is still held, so the dots stay green, the domains are still counted in the "domains granted" total, the status line still reads "applying", and the badge still reads ON. The per-profile collision marker on each card is the only surface that reports it, and it is what to read. Closing the gap on the other four means the status line and badge reporting how many profiles were skipped, which is new signal rather than a corrected one, so it is queued rather than patched (findings 21, 22 and 26).

    One caveat, found while verifying this: the site list under chrome://extensions → Details is not a reliable view of what is currently granted. It has been observed listing a domain that chrome.permissions.getAll() reports as not granted. Trust the popup, or the API, over that panel.

  • Lighter store review. Broad host permissions draw more scrutiny from the Chrome Web Store; asking for nothing until it's needed avoids that by construction, not by explanation.

This costs one extra permission prompt the first time you scope a profile to a new domain. That's the tradeoff, made deliberately.

A note on the prompt's wording: Chrome phrases every host grant as "read and change your data" on the site. That describes the permission class, not this extension — with declarativeNetRequest only, the browser applies the rules itself and no extension code observes any request. The manifest is the proof.

A note on prompts that don't appear: re-adding a domain you previously removed may produce no dialog at all. The grant is still acquired — Chrome appears to suppress the prompt for a host you have already consented to for this extension. Nothing is hidden from you when this happens: the popup's dot, chrome.permissions.contains() and chrome.permissions.getAll() all agree that the grant is held.

How far that suppression reaches is only partly established. Uninstalling the extension does clear it — reinstalling and granting the same host prompts again, observed twice. Whether it is scoped to the browsing session or persists indefinitely for an install that stays in place is not yet verified; settling it needs a fresh browser profile, and it is recorded here as open rather than guessed at. The practical consequence either way is that the absence of a prompt is not evidence that a grant was skipped — read the dot.

Development

No build step — the extension/ directory is the extension. Load it unpacked from chrome://extensions with Developer mode on.

Run the selftests with node test/selftest.mjs from the repo root (no dependencies). The suite covers rule construction and the canonical export format, and ends with a check-count tripwire: adding or removing checks requires updating EXPECTED_CHECKS in the same commit.

The suite verifies construction, not application — a rule can be built correctly and still no-op on a domain without a permission grant, so release verification also includes a manual smoke test against a live endpoint. Those steps are written down in test/SMOKE.md rather than left to memory.

Defects found so far, what caused them, and what changed are recorded in FINDINGS.md — including the ones introduced by earlier fixes and the limitations that are known rather than solved.

Versioning

Each minor version adds exactly one feature; patch versions are fixes only, never features.

License

MIT — see LICENSE.

About

A small Chrome extension for setting, appending, and removing HTTP request headers by profile. Built entirely on Manifest V3's declarativeNetRequest — no webRequest permission, so nothing in it can read your traffic. Permissions are requested per-site, not all at once at install.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages