Headers helper
How a plugin's MCP entry gets a credential without one ever being written to disk — what we put in the config, what runs, and what comes back. This is also what makes one entry per connected account affordable, because otherwise each entry needs its own browser trip.
The value we write
One field on the shared config, renamed by each dialect writer. The command is the same for both agents — the credential does not differ by agent, only the key name does.
~/.claude.json
"mcpServers": {
"linear-lu-1": {
"type": "http",
"url": "https://api.superset.sh/mcp/plugins/superset/linear?connection=conn-work",
"headersHelper": "/Users/you/.superset/bin/superset auth mcp-headers"
}
}
~/.codex/config.toml
[mcp_servers.linear-lu-1]
url = "https://api.superset.sh/mcp/plugins/superset/linear?connection=conn-work"
http_headers_helper = "/Users/you/.superset/bin/superset auth mcp-headers"
The path is the managed shim at ~/.superset/bin/superset,
not the bare word superset. The agent runs this command
itself, and its environment is not the Paradev terminal's — a bare name
would resolve only when the agent happened to inherit our PATH.
It sits under auth rather than a group of its own, because
what it prints is the CLI's own credential and nothing about MCP. It is
hidden from --help: the audience is an agent reading a
config we wrote, not a person at a prompt.
What runs, and what it returns
superset auth mcp-headers
{"Authorization":"Bearer sk_live_…"}
Stdout is the whole contract: a JSON object of string keys to string values, nothing else. The credential comes from the auth the CLI already holds, which is why there is nothing new to provision and nothing new to rotate.
Where the account list comes from
One entry per account needs the account list, and every writer of agent config runs where that list is not reachable: the desktop's is the main process, and the host-service's reconcile runs at session start in a terminal. So the renderer pushes what it can see into a cache beside the install ledger, and both writers read it from there.
~/.superset/plugins/connections.json
{
"connections": [
{ "connector": "linear", "connectionId": "conn-work", "externalUserId": "lu-1" },
{ "connector": "linear", "connectionId": "conn-personal", "externalUserId": "lu-2" }
]
}
A cache, never a source of truth. An absent or unparseable file reads as no connections, which is the one-entry-per-connector shape — wrong only until the next sync, and never a failed write. The renderer watches the whole set rather than hooking the connect and disconnect mutations, so a connection made on the web or from another machine lands here too.
Both writers fill it from connectors.status and nowhere
else, because the entry names are built from
externalUserId and only that procedure carries it. A writer
that fell back to connection ids would rename every entry the other had
just named, and each rename orphans whatever the agent stored against the
old name.
How the two agents differ
| Claude Code | Codex | |
|---|---|---|
| Config key | headersHelper |
http_headers_helper |
| When it runs | Fresh on every connection | Cached, and re-run when the server rejects — token refresh is built in |
| Limits | Not documented | 10s timeout, 64KB of output |
| Alternatives it also supports |
${VAR} expansion inside headers
|
env_http_headers,
bearer_token_env_var
|
Codex's is the stronger implementation: it keys a cache on a refresh
epoch and calls the helper again on an insufficient_scope
challenge, so an expired credential repairs itself mid-session. Claude
re-runs per connection, so a session started before a rotation keeps the
old value until it reconnects.
Why a helper rather than a header
The config could carry "Authorization": "Bearer …"
directly, and both agents would accept it. Three reasons not to:
-
A long-lived secret at rest in
~/.claude.jsonand~/.codex/config.toml, readable by every process the user runs — and profile sharing copiesmcpServersbetween accounts, so the token travels with it. - Rotation becomes ours to own, across every machine that ever synced.
-
Claude deliberately reads some credential variables as
empty inside a remote
urlorheaders—ANTHROPIC_API_KEY,AWS_BEARER_TOKEN_BEDROCK,NPM_TOKENand others — so the env-var route has a silent failure mode where the server simply receivesBearer.
What it unlocks
Each config entry authorizes separately: Claude stores credentials keyed
<name>|<hash of the config>, so N entries means N
browser round-trips, and any change to a URL or name orphans that
entry's token. One developer machine was measured at five plugin entries
with one ever authorized.
Splitting a connector into one entry per account would have multiplied exactly that. With the helper no entry needs OAuth at all, which is why the two changes shipped together rather than in sequence.
State
| Piece | |
|---|---|
headersHelper on the shared config, renamed per
dialect
|
done |
superset auth mcp-headers |
done |
| One entry per account, pinned, named by external user id | done |
Desktop and host-service both passing connections
and the helper path
|
done |
| A config rewrite when an account connects or disconnects | done |
| The helper omitted on a cloud box, which needs none | done |
The CLI writing MCP entries from plugins install,
connect, enable,
disable, uninstall and
sync
|
done |
The CLI hangs off syncPlugins(), the one function every
plugin mutation there already called to converge skills — so install,
connect, enable, disable, uninstall and plugins sync all
gained MCP entries at once, and each reaps what it should. It also
refreshes the account cache from the API wherever it has a client, which
is what lets a machine the desktop never ran on split a two-account
connector at all. plugins sync itself stays offline and
reads the cache.
Worth deciding
- Which credential does the helper print? Today the CLI's own bearer. A short-lived token minted for this purpose would narrow the blast radius, at the cost of a mint endpoint and an expiry the agents have to survive — which Codex does by design and Claude does by reconnecting.
-
Which credential does a cloud box use? None of ours: the
firewall adds one naming the workspace, and the CLI there resolves its
bearer to the empty string. So the helper is omitted entirely when
SUPERSET_SANDBOX_WORKSPACE_IDis set, rather than written and left to printBearerat every call. - Does the helper belong on non-plugin entries too? Nothing else we write needs Paradev auth today, so this stays scoped to the plugin proxy.
Keys and behaviours verified against each agent's own source and docs
rather than assumed: Claude's headersHelper from the MCP
documentation, Codex's http_headers_helper from
codex-rs/config/src/mcp_types.rs and
rmcp-client/src/http_headers.rs. Written from this
worktree, 2026-10-01; revised 2026-10-02 once the wiring landed.