A small, dependency-free Chrome extension (Manifest V3) that sets or removes
request headers on matching requests — including the Host header, which
pages themselves are never allowed to touch.
Built for testing gateways and virtual hosts locally: browse to
http://localhost:9999 while the gateway receives Host: acme.dev and routes
accordingly. It ships with exactly that rule so it works out of the box.
| Light | Dark |
|---|---|
![]() |
![]() |
- Open
chrome://extensions. - Enable Developer mode (top right).
- Click Load unpacked and select this folder.
- Pin the icon — the badge shows how many rules are active.
- Each rule reads like the header line it produces:
Host: acme.dev, appliedona URL filter. - set writes the header (adding it if absent); remove strips it.
- Rules apply to every request type — page loads, fetch/XHR, scripts, images, WebSockets.
- Changes apply as you type (about 300 ms after you stop). No save button.
- The switch in the top bar pauses everything; the per-rule checkbox disables one rule.
- If two enabled rules touch the same header on the same request, the rule higher in the list wins.
- Rows with problems are outlined in red (hover for the reason) and simply skipped — they never block other rules.
Plain text is a substring match against the full URL. Leave the filter empty to match every URL. Special tokens (full syntax):
| Filter | Matches |
|---|---|
| (empty) | every URL |
localhost:9999 |
any URL containing that text |
||acme.dev |
acme.dev and its subdomains |
|http:// |
URLs starting with http:// |
^ |
a separator: end of host/port, /, ?, or end of URL |
* |
any number of characters |
Tighter version of the example rule: ||localhost:9999^. Filters must be
ASCII — use punycode for international domains.
DevTools' Network panel can show headers as the page sent them, before the extension's rewrite. Trust the server, not DevTools. This repo includes an echo server for that:
python3 scripts/echo_server.py # listens on http://127.0.0.1:9999Open http://localhost:9999/ — the page and the terminal both report the
Host header that actually arrived (acme.dev with the stock rule). Hard
reload (⌘⇧R) if you suspect the cache.
For a fully automated check (scratch browser profile, real requests, on/off/on toggle):
scripts/verify.sh # headless; HEADFUL=1 to watchNote: verify.sh needs Chromium or Chrome for Testing — branded Google
Chrome ≥ 137 ignores --load-extension. Installing via Load unpacked
(above) works fine in any Chrome; this only affects the scripted check.
- HTTP/2 and HTTP/3 derive the
:authoritypseudo-header from the URL, soHostoverrides only affect plain-HTTP/1.1 traffic — which is exactly the local-gateway case. Other headers are unaffected by this. - The value is sent verbatim: if your gateway matches on host and port,
include the port (
acme.dev:8443). - Auto-apply means a valid prefix can be live for a moment while you type the rest of a value. Everything settles ~300 ms after you stop.
- Rules live in
chrome.storage.localand survive browser restarts.
manifest.json permissions and wiring
shared.js validation + DNR rule compilation (used by popup and worker)
background.js storage → declarativeNetRequest sync, badge
popup.html/css/js the UI
scripts/gen_icons.py regenerates icons/ (stdlib only)
scripts/echo_server.py local Host-header oracle
scripts/verify.sh end-to-end check
scripts/cdp.mjs DevTools-protocol helper used by verify.sh
The popup only reads/writes chrome.storage.local; the service worker owns
compiling that state into declarativeNetRequest dynamic rules (one storage
rule → one DNR rule, all 15 resource types listed explicitly — omitting
resourceTypes would silently exclude page navigations).
To debug matching, add "declarativeNetRequestFeedback" to permissions in
the manifest and use chrome.declarativeNetRequest.onRuleMatchedDebug in the
service-worker console (unpacked extensions only).

