●KEYS — v2.1.238 adds a keybindingFlavor setting. Set it to readline and Ctrl+W deletes back to the previous whitespace, just as in Bash. The classic default is unchanged●PLUGINS — Plugin marketplaces can now define a headersHelper that mints HTTP headers, such as a short-lived token, on each catalog fetch. Installing shows the command and asks before running it●RUNNER — self-hosted-runner gained defer-shutdown-max-min. On SIGTERM it keeps serving attached sessions, then parks whatever is left after that many minutes and exits●MEMORY — Unbounded memory growth in long interactive sessions is fixed. Subagent tool results are now released once they leave the recent display window●MCP — mcp list and mcp get now show disabled servers as Disabled instead of connecting to them for a health check, so a server you turned off no longer starts just to be listed●PRICING — Claude Sonnet 5's introductory $2 per million input and $10 output ends August 31, with standard $3 and $15 pricing from September 1. Nine days to go●KEYS — v2.1.238 adds a keybindingFlavor setting. Set it to readline and Ctrl+W deletes back to the previous whitespace, just as in Bash. The classic default is unchanged●PLUGINS — Plugin marketplaces can now define a headersHelper that mints HTTP headers, such as a short-lived token, on each catalog fetch. Installing shows the command and asks before running it●RUNNER — self-hosted-runner gained defer-shutdown-max-min. On SIGTERM it keeps serving attached sessions, then parks whatever is left after that many minutes and exits●MEMORY — Unbounded memory growth in long interactive sessions is fixed. Subagent tool results are now released once they leave the recent display window●MCP — mcp list and mcp get now show disabled servers as Disabled instead of connecting to them for a health check, so a server you turned off no longer starts just to be listed●PRICING — Claude Sonnet 5's introductory $2 per million input and $10 output ends August 31, with standard $3 and $15 pricing from September 1. Nine days to go
Switching to headersHelper in Claude Code broke auth for project-scoped catalogs only
Moving a private plugin catalog to headersHelper worked at user scope and failed under the project directory. The cause was credential non-inheritance. Here are two working helpers, measured execution costs, and what unattended runs need.
Three mornings that week opened with the same failure notification from an unattended run. Each one traced back to an expired token for the private repository that hosts my own plugin catalog.
As an indie developer I keep one shared set of skills and plugins across both the app side and the site side of my work, stored in a private repository and pulled from several machines and projects. The token lived directly in a config file. Every rotation meant remembering where it was, editing it, and discovering the file I forgot the morning an unattended run fell over.
Claude Code v2.1.238, released on August 20, added headersHelper to plugin marketplaces, which lets that arrangement go away. A command runs every time the catalog is fetched and returns HTTP headers, so you can hand over a freshly minted, short-lived token instead of a long-lived one.
The migration should have taken half an hour. Instead, a helper that worked perfectly at user scope stopped authenticating the moment it moved under a project directory, and I burned half a day there. The reason makes complete sense in hindsight. I did not see it coming.
What was actually painful about the token in a config file
With a catalog in a private repository, authentication is needed twice: once to fetch the catalog JSON, and again to fetch the archives it points at. Both used a fixed token.
The annoying part of a fixed token is not the expiry itself. It is that the only channel telling you it expired is an incident. A long-lived token gives no warning at all, and then several machines and several projects fail to fetch at the same moment.
There is also the file. Putting a token in a config file that lives in a repository turns that file into something you can leak. Even at the scale a single indie developer works at, forgetting to keep a project-level .mcp.json out of git is a real possibility.
headersHelper addresses both structurally rather than procedurally.
Where headersHelper actually plugs in
You can attach headersHelper to a URL marketplace definition and to individual catalog entries. The command you name prints a JSON object of headers to stdout, and those headers ride along with the request.
Placement
When it runs
Typical use
URL marketplace
Every catalog fetch, plus same-origin archive fetches
Protecting the catalog as a whole
Catalog entry
Only when that plugin is installed or updated
Splitting distribution per entry
The important detail is that an entry-level helper does not run silently. claude plugin install and claude plugin update display the command and ask [y/N]. You can skip the prompt with -y, but skipping is a decision a human makes once.
If you run anything unattended, check this first. Without an explicit -y, a non-interactive path sits waiting for confirmation. I missed this initially and only noticed when the task hit its timeout.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦You will know exactly which scope to place a headersHelper in when you move a private plugin catalog to short-lived tokens
✦You will catch the class of helper that works on your machine and dies once distributed, before you ship it to anyone
✦You will be able to choose between a static store read and per-request RS256 minting from measured cost (5 ms vs 21 ms) instead of guesswork
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
The helper worked at user scope and failed under a project. I assumed it was the trust dialog and spent a long time looking in that direction.
The real cause was elsewhere. A headersHelper that comes from a project, a plugin, or an agent file runs without the inherited credential environment variables. Helpers at user, managed, or claude.ai scope run from the Claude config directory, under different assumptions entirely.
The intent is to close the path where a config file committed to a repository walks out the door still holding your keys. That reasoning holds up. The catch is that if your helper reads a token from an environment variable, the difference is invisible for as long as you only test at user scope.
To see the behavior directly, I ran the same script with the inherited environment stripped.
# A naive helper that reads the token from the environmentcat << 'EOF' > headers-env.sh#!/usr/bin/env bashset -euo pipefail: "${CATALOG_TOKEN:?CATALOG_TOKEN is not set}"printf '{"Authorization":"Bearer %s"}\n' "$CATALOG_TOKEN"EOFchmod +x headers-env.sh# (1) With the environment present - passesCATALOG_TOKEN=demo123 ./headers-env.sh# => {"Authorization":"Bearer demo123"}# (2) With inheritance stripped - approximates a project-scoped helperenv -i HOME="$HOME" PATH="$PATH" ./headers-env.sh; echo "exit=$?"# => headers-env.sh: line 3: CATALOG_TOKEN: CATALOG_TOKEN is not set# => exit=1
Strip the environment with env -i and the same script exits 1. That is what happened on my machine, and matching that symptom against the project-scope failure is what finally pointed me at the cause.
One line, run before distributing, would have saved the half day. Write the helper, then confirm it survives without an inherited environment. That was the expensive lesson here.
Move credential retrieval inside the helper
The fix is unglamorous: stop depending on the environment and have the helper pull from a credential store itself.
cat << 'EOF' > headers-store.sh#!/usr/bin/env bashset -euo pipefail# The helper reads from a store rather than the environmentTOKEN=""# Prefer the macOS keychain when it existsif command -v security >/dev/null 2>&1; then TOKEN="$(security find-generic-password -a "$USER" -s claude-catalog -w 2>/dev/null || true)"fi# Fallback for environments without a keychain (CI, containers)if [ -z "$TOKEN" ]; then STORE="${HOME}/.config/claude-catalog/token" TOKEN="$(cat "$STORE" 2>/dev/null || true)"fiif [ -z "$TOKEN" ]; then echo "catalog token not found (keychain / ~/.config/claude-catalog/token)" >&2 exit 1fiprintf '{"Authorization":"Bearer %s"}\n' "$TOKEN"EOFchmod +x headers-store.sh# Confirm it passes with no inherited environmentenv -i HOME="$HOME" PATH="$PATH" ./headers-store.sh; echo "exit=$?"# => {"Authorization":"Bearer ..."}# => exit=0
Set chmod 600 on the fallback file. Naming the locations you searched in the stderr message pays off more than it looks like it should: a helper that silently returns nothing looks identical to an authentication failure from the caller's side, and separating the two costs real time.
The command -v security branch exists because I wanted one helper for both a macOS workstation and a container. Writing it keychain-only means editing it again on the container side every time.
Mint short-lived tokens per request
Swapping a fixed token into a store reduces the pain of rotation but leaves the lifetime problem intact. Since the helper runs on every fetch anyway, minting something short-lived right there is the natural move.
What I settled on for repository distribution is a signed JWT valid for five minutes, generated fresh each time.
The 300-second exp accounts for the catalog fetch and the archive fetches running back to back. Shorten it too far and the token expires mid-retrieval.
CATALOG_SIGNING_KEY can override the key path, but the part that matters is that the default is a fixed path under the home directory. Given the previous section, the helper has to work from its defaults alone, with no environment variable arriving.
Measure the cost before choosing
The helper runs on every fetch, and the number of fetches grows with the number of plugins, so measuring once makes the decision easy. Twenty runs each, on a Linux container with OpenSSL 3:
Helper
20 runs
Per run
Notes
Static token read from a store
102 ms
~5 ms
Process spawn and a file read
RS256 JWT signed per request
428 ms
~21 ms
2048-bit key, openssl dgst
Sixteen milliseconds apart. With dozens of plugins installed, that is not a difference anyone feels. Signing cost was the thing I had been wary of going in, and it turned out not to be the thing that matters.
What does matter is what happens if you call an external service to issue the token. A full network round trip lands in a code path that runs on every fetch, and the order of magnitude changes. If you can sign locally, sign locally.
My working rule:
If the only consumers are you and your own machines, a static read from a store is enough
If consumers multiply, or you want revocation to mean something, sign short-lived tokens locally
Reserve an external issuing service for cases where immediate revocation is genuinely required
Sometimes you want to know which scope's helper actually answered. The fetch path parses the helper's stdout directly, so mixing debug output into it breaks the request. Anything you add belongs on stderr.
# stdout stays pure JSON; diagnostics go to stderrecho "[headers-store] scope=$(basename "$PWD") store=keychain" >&2
$PWD reflects where the helper was launched from, which distinguishes a user-scope run from one under a project directory. Adding that single line ended my confusion about scopes for good. One caution: never write the token itself to stderr. It lands in logs.
Trust approval is now a precondition
One more change matters for unattended setups. A headersHelper in a project's .mcp.json, and inline MCP servers written into project or --add-dir agent files, now require that folder's trust dialog to have been approved. Running with claude -p is treated the same way.
On my side this bit exactly once, in a freshly created checkout used for CI. A single human approval clears it permanently, so it is not an ongoing obstacle. But if your setup recreates the folder each time, it bites each time.
I moved the working checkout to a fixed path. A design that treats folders as disposable does not sit well with trust granted per folder, and adjusting the execution environment is the cheaper side to change.
Placement
Trust dialog
Credential env vars
User / managed / claude.ai scope
Not required
Runs from the Claude config directory
Project .mcp.json
Must be approved
Not inherited
Inline MCP in an agent file
Must be approved
Not inherited
Having this table on day one would have saved me the half day. Once you know the assumptions differ by scope, the diagnosis takes minutes.
What to do next
If you keep your own catalog in a private repository with the token written into a config file, write one helper and run env -i HOME="$HOME" PATH="$PATH" ./headers-store.sh against it. Pass that, and the helper works at any scope. Fail it, and there is still an environment dependency hiding somewhere.
Doing that check before the migration matters more than the migration itself. I did it in the wrong order and paid half a day for the privilege. If this saves someone else the same afternoon, it was worth writing down.
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.