Shared claude daemon leaks ANTHROPIC_AUTH_TOKEN from first session into all later sessions on the machine — silent wrong-account auth/billing

Status Open
Reported on v2.1.215
Maintainer reply None cached
Activity 1 comment · opened Jul 20, 2026

Summary

ANTHROPIC_AUTH_TOKEN (and by extension any auth-bearing environment variable) present in the environment of the first session that spawns the shared claude daemon is silently inherited by every subsequent daemon-spawned session on the machine — including sessions started later in completely different terminals and repositories whose environments do not contain the variable. Because ANTHROPIC_AUTH_TOKEN overrides the stored Keychain OAuth login without any approval prompt (the "use this API key?" prompt only covers ANTHROPIC_API_KEY), those sessions authenticate and bill against the wrong account with no visible indication.

Environment

  • Claude Code v2.1.215 (native binary, ~/.local/share/claude/versions/2.1.215)
  • macOS (Darwin 25.5.0), zsh, iTerm2
  • Logged in via claude.ai OAuth (Max plan), credentials in macOS Keychain (Claude Code-credentials)

What happened

  1. 09:44 — launched claude in project A from an iTerm tab in which ANTHROPIC_AUTH_TOKEN=sk-ant-oat01-… had been exported (interactive export; the variable exists in no dotfile, launchctl, direnv, or .env).
  2. 11:41 — that session spawned the shared per-user daemon (claude daemon run --origin transient …, state in /tmp/cc-daemon-501/…). The daemon inherited the token into its environment.
  3. 11:58 and 12:09 — two new sessions were started in a different repository, from a different terminal window whose environment contained no ANTHROPIC variables. Both sessions nevertheless authenticated with the inherited ANTHROPIC_AUTH_TOKEN.
  4. 12:33–12:42 — both sessions began failing with API Error: Request rejected (429) · This request would exceed your account's rate limit. The limit was real — for the token's account, which was over its usage plan — while the Keychain OAuth account (Max) was nowhere near its limits. Very confusing to debug.

Evidence

Inspecting live processes (ps eww <pid>) shows the token in the exec-time environment of:

  • the daemon itself (claude daemon run --origin transient --spawned-by {"label":"claude","cwd":"<project A>","pid":…})
  • its pre-spawned workers (claude bg-spare --bg-spare /tmp/cc-daemon-501/<id>/spare/<id>.claim.sock and the matching bg-pty-host processes)
  • a session process for the unrelated repository, running as a child of the daemon (…/versions/2.1.215 --session-id <id> --fork-session --resume <other-repo transcript> …)

Meanwhile:

  • the terminal shells for the affected repo contain no ANTHROPIC variables;
  • ~/.claude.json has customApiKeyResponses: null — no key was ever approved, because the ANTHROPIC_AUTH_TOKEN path never asks;
  • the CLI launcher is a plain symlink to the versioned binary (no wrapper injecting anything);
  • shell snapshots (~/.claude/shell-snapshots/…) capture functions/aliases only, not env, so nothing on disk reveals the token's presence.

So the inheritance chain is: first session's shell → first session → daemon → spare workers → all later daemon-spawned sessions, regardless of repo/terminal. The daemon snapshots whatever environment the first session of the day happened to have, and reuses it for everyone.

The UI displayed the wrong auth identity

Both affected sessions displayed the correct OAuth organisation (the Keychain/~/.claude.json oauthAccount) at startup — while their requests were authenticating with the inherited env token. Neither session transcript contains any compaction, resume, or fork markers, so there was no mid-session re-host: the wire auth was the env token from the first request, and the account shown in the UI never reflected it. The mismatch only became visible ~35–55 minutes in, when the token's account ran out of quota and both sessions started returning 429s within a minute of each other. The identity display appears to be derived from the stored OAuth credentials rather than from the credential actually attached to outgoing requests.

Why this is serious

  • Silent cross-account billing: sessions bill an account the user never selected for them, with no prompt and no indication in the UI. In multi-account setups (personal Max + client/work API tokens) this leaks usage — and potentially conversation traffic — onto the wrong organisation's account.
  • Undiagnosable from the user's side: the variable exists only in the process tree. Checking dotfiles, launchctl, direnv, .env files, and the launching terminal's environment all come up empty. ps eww on the daemon is not something users will think to do; worse, ps shows only exec-time env, so even inspecting the originating shell is misleading.
  • Misleading errors: the resulting 429s reference "your account's rate limit", which does not match what the user sees in their subscription usage.

Suggested fixes

  1. Scrub auth-bearing variables (ANTHROPIC_AUTH_TOKEN, ANTHROPIC_API_KEY, ANTHROPIC_CUSTOM_HEADERS, etc.) from the environment the daemon passes to spare workers / spawned sessions, and re-resolve credentials per session from the session's own launch context (or the credential store).
  2. Alternatively (or additionally), surface the auth source prominently: if a session is authenticating via an env-var token rather than the stored login, say so at session start and in /status, and apply the same explicit-approval flow that ANTHROPIC_API_KEY gets to ANTHROPIC_AUTH_TOKEN.
  3. Consider logging the auth source (oauth vs env-token) in the session transcript so incidents like this are diagnosable after the fact.

Workaround

Unset the variable in the originating shell first, then kill the daemon and its bg-spare/bg-pty-host workers so they respawn with a clean environment, then rotate the exposed token.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗