AI-Created Software

Last Updated: October 16, 2026

The following is AI-generated (aka vibe-coded) software. The software is specific to my needs and my system and may not work for you or your system. Support is limited, but if you report a bug I'll certainly look into it. All code is provided in a zip file for you to review should you choose.

Header Injector icon

Header Injector

Version: 2.5.4

A Chrome extension (Manifest V3) that adds custom HTTP request headers to the domains you choose, and shows you which of your rules are actually firing.

Useful for hitting a staging API that needs an auth token, testing feature flags behind a header, or faking a X-Forwarded-For against your own dev server. Rules are applied by Chrome’s declarativeNetRequest engine, so headers are set before the request leaves the browser and nothing in your page code changes.

Features

  • Per-rule switches. Keep eight rules on file and run only the two you need.
  • Match counts. Each rule shows how many requests it has hit and when, so a typo’d header or a filter that matches nothing is visible instead of silent.
  • Precise domain matching. A domain rule matches that host and its subdomains, not any URL that happens to contain the string, and not a look-alike host such as example.com.evil.net.
  • Only from pages on this domain. On by default for new rules, so other sites you visit can’t make the browser send your token for you.
  • Import and export. Save a set of rules as JSON, commit it next to the project it belongs to, hand it to a teammate.
  • Scheduled backup. Every n days, into a folder of your choosing, keeping as many earlier backups as you like, with Restore to put one back.
  • Rule manager. A full-page view with filtering, editing and duplication.
  • Duplicate a rule and edit the copy, rather than retyping a long token.
  • One master switch to pause everything without deleting anything. The toolbar icon greys out and the badge clears while paused.

Install

An unpacked, developer-mode extension. There is no store listing.

  1. Open chrome://extensions
  2. Turn on Developer mode (top right)
  3. Click Load unpacked and select this folder
  4. Pin the extension so the toolbar icon and badge stay visible

After editing any file, hit the reload arrow on the extension’s card.

Using it

The popup is for quick work: add a rule, flip one on or off, glance at what’s firing. Manage all rules opens the full manager, which adds filtering, import, export and the last URL each rule matched.

A rule is three things:

Field Example Notes
Domain or URL pattern api.staging.example.com See matching, below
Header name Authorization Case-insensitive
Header value Bearer eyJhbGci… May be empty
Only from pages on this domain on See below

Matching

Domain is the default and covers most cases. It uses requestDomains, which matches the host and any subdomain of it, on any port and any path. So example.com matches https://api.example.com/v1 but not https://other.test/?ref=example.com.

URL pattern is the escape hatch, using Chrome’s urlFilter syntax:

Pattern Matches
||localhost:3000^ localhost on port 3000 only
|https://api.example.com/v2 one path prefix
*/graphql any host, that path

|| anchors the host, ^ marks the end of the host or port, * matches anything, | anchors the start or end of the URL. Type a port or a wildcard into the domain field and the rule is converted to a URL pattern for you, with a note saying what it became. A leading *. is simply dropped, since a domain already covers its subdomains.

A URL pattern is a prefix match, so ||example.com on its own would also match example.com.evil.net, and ||localhost:3000 would match port 30001. A bare ||host or ||host:port is saved with a ^ on the end to stop that. Other patterns are saved as typed, with a warning when they could match another site: one with no || or |https:// at the start matches any URL that contains it, and one with no ^ or / after the host matches look-alike hosts.

Rules apply to main frames, sub frames, XHR/fetch and scripts.

Only from pages on this domain

Without this, an injected header works like a cookie: any page you visit can load https://api.example.com/... (a script tag or a fetch is enough) and the browser attaches your Authorization header to it. With it on, the header goes only on requests made by pages on the rule’s domain or its subdomains, plus addresses you open yourself (typing, a bookmark, a link) as long as they are plain GET navigations. That’s the same deal as a SameSite=Lax cookie: a form on another site can’t POST to the API with your token.

It’s on by default for new rules. Turn it off when a page on a different domain needs the header, such as app.example.com calling api.example.com. For a URL pattern, the domain is the host the pattern starts with, so the pattern must start with one (||api.example.com^, |https://api.example.com/).

Rules whose header name looks like a credential (Authorization, Cookie, or anything with token, key, auth, secret, session or passw in it) are marked any page in the list while the option is off.

Match counts

The count next to a rule is how many requests it has modified since the browser started. It comes from onRuleMatchedDebug, which Chrome only reports for unpacked extensions — pack this and the counts stay empty while the rules themselves keep working. Counts live in session storage and reset when Chrome restarts; Clear counts resets them by hand.

If a rule shows “Rejected”, Chrome refused it. The message says why, and the other rules keep working: rules are re-applied individually when a batch fails, so one bad rule can’t take the rest down with it.

Import and export

Export writes every rule, including the disabled ones, plus the master switch and the backup schedule, to a JSON file. Import offers to add the rules to what you have or replace everything; Replace all also restores the master switch and backup schedule from the file.

Imported rules get the same checks as the form. A rule with a header name Chrome won’t accept, or that Chrome won’t let extensions change, is skipped, and the import dialog lists each skipped rule by number with the reason.

{
  "format": "header-injector/rules",
  "version": 1,
  "exportedAt": "2026-09-08T12:00:00.000Z",
  "rules": [
    { "enabled": true, "matchType": "domain", "match": "api.example.com",
      "key": "Authorization", "value": "Bearer …", "sameSite": true }
  ],
  "settings": {
    "enabled": true,
    "backupSchedule": { "enabled": true, "everyDays": 3, "keep": 3,
      "folder": "Header Injector Backups" }
  }
}

Rule ids are deliberately left out so an imported file can never collide with rules you already have. A bare array of rules is also accepted, as is the old {"domain": …} field name.

Exports contain header values in plain text. They are usually credentials. Treat the file accordingly and keep it out of version control.

Scheduled backup

The Scheduled backup panel in the full manager saves the same file Export does, on its own, into a folder inside your downloads folder:

Header Injector Backups/header-injector-backup-2026-09-06-143052.json
  • Switch — turns the schedule on. Turning it on makes the first backup straight away.
  • Backup folder — relative to the browser’s downloads folder. Defaults to Header Injector Backups, can be several levels deep (Backups/Header Injector), and is created if it doesn’t exist. Browsers only let extensions save inside the downloads folder, so a location elsewhere on disk isn’t possible.
  • Every (days) — how often to back up, from 1 to 365.
    The extension checks once an hour whether a backup is due, so if the browser was closed when one fell due, it’s made within the hour of the browser opening again.
  • Earlier backups to keep — how many backups to hold on to besides the latest one. With 3, you have the latest backup plus the three before it.
  • Back up now — makes a backup immediately and applies the same cleanup.
  • Restore — reads a backup or export file and, once you confirm, replaces all your rules, the master switch and the backup schedule with what’s in it. The confirmation shows when the file was saved and what it holds. Older exports that don’t carry settings replace only the rules.

Old backups are cleared only after a new one has finished saving. The count works in calendar days:

  • If an earlier day has several backups, only the newest from that day is kept, and it counts as one.
  • A day with no backups counts as nothing, so a gap of a week doesn’t use up the count.
  • Other backups from the same day as the latest are replaced by it.

So with a keep of 3, backups on Monday (two), Wednesday, Thursday, Friday and Saturday leave you with Saturday’s plus Friday’s, Thursday’s and Wednesday’s; both of Monday’s go.

A backup is the same file Export writes: every rule, including disabled ones, plus the master switch and the backup schedule itself.
Put one back with Restore (or Import → Replace all, which does the same). Backups are skipped while there are no rules, so an empty setup never pushes out a good backup.

Cleanup counts backups in every folder, so after you change the backup folder, the ones in the old folder still count and are removed in turn. It only touches files the extension saved itself that are still listed in the browser’s download history: clearing download history leaves those older backups on disk for good, and files you move or rename are never deleted. Each backup shows up in the browser’s download list like any other download.

Files

manifest.json     Permissions, worker, icons, options page
background.js     Service worker: rule sync, match tracking, toolbar state, backups
popup.html/.js    Quick add, per-rule switches, live match counts
options.html/.js  Full manager: filter, edit, duplicate, import, export, backup schedule
lib/store.js      Rule model, validation, storage, import/export, backup retention  (pure + tested)
lib/dom.js        DOM builders, icons, relative times, toasts
lib/ui.css        Design tokens and shared components
icons/            Toolbar and store artwork, plus icon.svg source
tools/            make_icons.py, regenerates every PNG from one definition

Regenerating the icons

tools/make_icons.py renders all eight PNGs on a 2048px master and downsamples them, writing straight into icons/.
Needs Pillow and NumPy:

pip install pillow numpy && python3 tools/make_icons.py

Sizes 16 and 32 are drawn from a simplified two-line variant with a solid arrowhead, because the three-line mark and its thin chevron blur together that small. Keep both the active and -off sets: background.js names all eight paths and setIcon throws if one is missing.
icons/icon.svg is the vector source for the large variant.

lib/store.js holds everything that decides what a rule means, so the worker and both pages can’t drift apart. The top half is pure and has no dependency on the chrome namespace, which makes it testable outside the browser.

Upgrading to 1.3.0 (security fixes)

  • Wildcard and port rules stop at the end of the host. Patterns saved as ||host or ||host:port gain a ^ the first time 1.3 reads them.
  • *.example.com rules work. Earlier versions saved them as ||*.example.com, which Chrome refuses, so they showed as “Rejected”. They now become ||example.com^, which covers the same subdomains (and example.com itself).
  • Existing rules keep sending their header from any page. Only new rules start with “Only from pages on this domain” on. Edit a rule to turn it on.

Upgrading from 1.2

Stored rules migrate automatically the first time 1.3 reads them.
Each gains an id and an on switch, and the old domain field is re-read: plain domains become domain matches, anything with a wildcard becomes a URL pattern.

Matching got stricter. Version 1.2 wrapped every domain as *domain* and substring-matched it against the whole URL. If you were relying on that — a rule whose “domain” was really a path fragment, say — it will stop matching. Switch that rule to URL pattern mode and write the pattern explicitly.

Permissions

Permission Why
declarativeNetRequest Registers the header rules with Chrome’s network engine
declarativeNetRequestFeedback Reports which rules matched, for the counts
storage Saves rules locally, and match counts for the session
downloads Saves scheduled backups and removes old ones
alarms Wakes the extension once an hour to see whether a backup is due
<all_urls> modifyHeaders rules need host access to the sites they touch

Nothing leaves your machine. Rules are in chrome.storage.local, counts are in chrome.storage.session, and neither syncs to your Google account.

Limitations

  • Request headers only. Response headers would need a responseHeaders block in the rule action.
  • Set, not append or remove. Each rule overwrites a header of that name.
  • Some headers are off limits. Chrome won’t let extensions touch Host, Connection and friends; the form rejects those names at entry.
  • CORS preflight. Adding a non-simple header to a cross-origin request triggers a preflight the server has to allow.
  • No profiles yet. Rules are one flat list.
    Import/export covers the switch-between-environments case for now.
  • Match counts are best-effort. They are buffered and written in batches, so a burst of requests may take a moment to show up.

Troubleshooting

Backups land in the downloads folder as download.json Another extension that renames downloads is overriding the backup’s name. Chrome forgets the name an extension asks for whenever any extension renames downloads, so that extension has to put it back. Download Router does this; for others, turn them off or check their settings.

Debugging

  • Service worker logs: chrome://extensions → Service worker on the card

  • What is actually registered: run this in the service worker console

    chrome.declarativeNetRequest.getDynamicRules().then(console.table)
  • Confirm the header on the wire in DevTools → Network → the request → Headers → Request headers. DevTools sometimes shows provisional headers for cached requests, so do a hard reload.

Tests

The pure half of lib/store.js has a test suite covering normalisation, the v1 migration, id stability, the import parser and backup retention. With Node 18+:

cp lib/store.js lib/store.mjs && node test_store.mjs && rm lib/store.mjs
(1 votes, average: 5.00 out of 5)

Leave a Reply

Your email address will not be published. Required fields are marked *

Notify me of followup comments via e-mail.