Headsmith

An HTTP header editor for Chrome. Add, modify and remove request and response headers, grouped into profiles and scoped to the sites you name.

Free and MIT licensed. Chrome Web Store listing pending review.

It cannot read your traffic

This is the point of the project, so it goes first.

Headsmith is built entirely on Chrome's declarativeNetRequest API. It hands the browser a list of rules and the browser applies them. Headsmith is never invoked for a request: it does not receive the URL, the headers, the body, or the response.

That is not a promise about our conduct — it is the shape of the API. There is no code path that could log your browsing, because none of our code runs when a request is made.

Reading traffic would require the webRequest permission. Headsmith does not request it, and a check in the build pipeline fails if that ever changes.

It asks for no sites when you install it

No “read and change all your data on all websites” prompt, because at install it has been granted nothing at all.

When a profile names a domain, Chrome asks about that domain and nothing else. Every site you have allowed is listed inside the extension and can be withdrawn there — granting is never one-way.

The honest limit. A profile scoped by URL text or a regular expression instead of a domain can match any site, so those ask for broader access — and say so before asking, with a nudge to name a domain instead.

The Site access panel listing two allowed domains, each with a Remove button, plus Allow all sites and Revoke all site access controls.
Site access: what is granted, and how to take it back.

What it does

Request and response headers

Set, append or remove either, with Chrome's own constraints surfaced as you type rather than at apply time.

Profiles

Group rules, switch between them, enable them individually, or pause everything with a keyboard shortcut.

Precise scoping

By domain, URL substring, regular expression and request type, with per-profile exclusions and a global never-modify list.

Credentials kept apart

Values that look like credentials never live in a profile. Session-only by default, or an AES-GCM vault behind a passphrase.

Fails closed

If a credential cannot be resolved, the operation is dropped rather than sent empty — an Authorization header with nothing after it is worse than none.

No network access

No analytics, no telemetry, no remote fonts or scripts. A CI check scans the built extension and fails on any network primitive.

The header editor showing an X-Environment header set to staging and an Authorization header whose value is masked.
Headers, with credentials stored outside the profile.
The scope editor showing domain, URL-contains and regex fields, with a note on how many rules the profile costs.
Scope, with the cost of each choice shown.

You can check the build yourself

The Web Store signs the package itself, from a key the developer never holds — so a store signature says the bytes came from a developer account, and nothing about where they came from.

Every Headsmith release closes that gap. The build is reproducible, so you can rebuild it and compare:

git clone https://github.com/bcollard/headsmith.git && cd headsmith
git checkout v1.3.2
nvm use            # the Node version affects the output
npm ci
node scripts/verify-reproducible.mjs ~/Downloads/headsmith-1.3.2.zip

And each release carries a provenance attestation binding those exact bytes to a commit:

gh attestation verify headsmith-1.3.2.zip --repo bcollard/headsmith

The shipped bundle is deliberately not minified. Publishing with provenance is worth little if the thing being attested is an unreadable chunk.

Questions worth answering up front

I set a response header and DevTools does not show it.
It is almost certainly working. The Network panel reports response headers as they arrived from the server, before extensions modify them. To see the real value, open the console on the page and run (await fetch(location.href)).headers.get('Your-Header') — that request is same-origin, so every header is visible to it.
Do I need a wildcard for subdomains?
No. A domain already covers everything beneath it, so example.com matches api.example.com too. Chrome accepts spellings like *.example.com and then silently matches nothing; the editor flags those and offers to correct them.
Why not use activeTab instead of host permissions?
It cannot work for this kind of extension. Header rules need host access at the moment a request is made, and activeTab grants it on a click — which happens after the page has already loaded, and is revoked by the next navigation.
Can it merge cookies, or edit one CSP directive?
No, and it says so rather than shipping something that half works. declarativeNetRequest cannot read an existing header value, so a true merge is not possible — only replacing a whole value.
Where do my credentials go?
Never into a profile. In session mode they live in memory Chrome clears when it closes; in vault mode they are AES-GCM encrypted under a key derived from your passphrase, held only while unlocked. There is no plaintext-on-disk option.
How do I check that what I installed matches the source?
Download the release, run gh attestation verify to confirm it came from this repository and commit, then rebuild from the same tag and compare the bytes — the commands are above. The shipped bundle is not minified, so it can also simply be read.
The toolbar popup showing a profile name, request headers and scope fields in a narrow layout.
The toolbar popup: headers and scope without leaving the page.

Install

Not yet on the Chrome Web Store — the listing is in review. Until then, from source:

git clone https://github.com/bcollard/headsmith.git && cd headsmith
npm ci
npm run build

To check it is doing something, run npm run echo for a local server that shows the headers it actually received, then scope a profile to localhost.