claude -p silently bills a project .env ANTHROPIC_API_KEY while a valid subscription credential is active — documented safeguards do not fire in non-interactive mode
Summary
claude -p (non-interactive mode) silently billed 14 runs to a project .env ANTHROPIC_API_KEY while a valid, freshly-restored subscription OAuth credential was present on the machine.
The behaviour itself is documented. What is not working is the safeguard the documentation promises. Both documented protections are interactive-session features, and claude -p has no prompt surface — so headless automation, the mode most likely to run long and unattended, is the mode with zero billing consent.
Documented safeguards that did not fire
Manage API key environment variables in Claude Code states:
"Claude Code prioritizes environment variable API keys over authenticated subscriptions." "During initial setup, if an API key is detected in your environment variables, Claude Code will ask you to confirm which authentication method you want to use." "After successful login, you'll see a notification if both credentials are active, alerting you to the potential for unexpected API charges."
In this incident:
- No confirmation prompt appeared before or during any of the 14 billed runs.
claude logincompleted successfully at2026-09-03T10:14:01Z(Login successful.in the log). No "both credentials active" notification was shown.
Environment / context
- The
ANTHROPIC_API_KEYlives in a project.env.local. It exists for one unrelated production purpose: a Supabase Edge Function that translates injury reports, hard-coded toclaude-haiku-4-5. Its spend has been US$1–2/month for months. - It was never exported for Claude Code. Audited and confirmed zero matches in:
~/.zshrc,~/.zprofile,~/.zshenv,~/.bashrc,~/.bash_profile,~/.profile,/etc/{zshenv,zprofile,zshrc,profile}, launchd agents/daemons,~/.claude/settings.json, project.claude/settings.jsonandsettings.local.json, and 1,337 lines of shell history. - Runs were launched via
claude -pfrom a project directory.
Timeline (UTC) — from ~/.claude/daemon.log
2026-09-02T17:22:17.559Z [supervisor] auth: proactive refresh starting
2026-09-02T17:22:17.642Z [supervisor] auth: proactive refresh failed, signalling re-auth required
2026-09-02T17:22:17.658Z [supervisor] auth: headless daemon cannot complete OAuth - run `claude auth login` to refresh
2026-09-02T17:22:17.659Z [supervisor] auth: no token found, will re-check keychain every 30s
2026-09-02T23:00:49.657Z [supervisor] auth: no token found, will re-check keychain every 30s
2026-09-03T00:40:13.700Z [supervisor] auth: token found via keychain re-check <-- subscription restored
2026-09-03T01:55:00Z first billed `claude -p` run starts <-- 1h15m AFTER restore
...
2026-09-03T10:14:01Z `claude login` -> "Login successful." (no notification shown)
2026-09-03T10:19:00Z last billed run ends
~/.claude/daemon-auth-status.json still records {"status":"auth_required","since":1788369737658} (= 2026-09-02T17:22:17Z), i.e. 7h18m with no subscription token, resolved only by manual intervention.
The key point: the subscription credential was valid and present for the entire billed window. This was not a fallback after expiry — the environment key was preferred over a working subscription, silently.
Impact
| | |
|---|---|
| Billed runs | 14 (claude -p, primarily Opus 5) |
| Window | 2026-09-03 01:55–10:19 UTC |
| Attributed spend | US$42.95 (Console → Claude Code → Usage, API-key identity) |
| Charged to card | US$22.58 via two auto-recharge invoices |
| Normal monthly spend on this key | US$1–2 |
Additionally: total_cost_usd is printed in each -p run's JSON result, but nothing in the output indicates which billing path produced it. There is no way to tell a subscription-metered figure from a real charge by reading the output.
Steps to reproduce
- Have an active Claude subscription, logged in via OAuth.
- Place
ANTHROPIC_API_KEY=<key>in a project.env/.env.local(e.g. for an unrelated production service). - From a shell where that variable is in the environment, run
claude -p "<some long task>"in that project. - Observe: the run completes with no prompt, no notification, no indication of billing source. Charges land on the API key.
Expected behaviour
At minimum one of:
claude -prefuses to start when an environmentANTHROPIC_API_KEYwould override an active subscription credential, unless an explicit opt-in flag (e.g.--billing=api) is passed.claude -pprints the billing source on stderr before the first request — e.g.billing: API key (…FgAA) — subscription credential present but not used.- The
-pJSON result includes abilling_sourcefield alongsidetotal_cost_usd. - The documented "both credentials active" notification fires in headless/background contexts — or the documentation stops claiming that it does.
Option 2 is the minimum that would have prevented this entirely, and it costs one line of output.
Related issues (same failure mode)
- #37686 —
claude -precommended to a Max subscriber, unintended API billing, US$1,800+ in two days - #86723 — Routines billed to metered API credits while subscription headroom remained, US$1,122.83 over two months
- #39903 — Max subscribers billed via API key on subagent dispatch, US$152
- #58083 — project
.envANTHROPIC_API_KEYsilently used instead of Max OAuth, ~US$52 — closed as "not planned", no staff response, marked stale - #53728 — silent
ANTHROPIC_API_KEYprecedence shadows Max subscription auth - #44669, #12352, #45572, #50994 — same class of report
This report is filed as a distinct case because it combines #58083's condition (a key in a project .env, provisioned for an unrelated workload) with #37686's execution path (claude -p), and documents that the safeguards published in the help center did not operate.
Support outcome so far
Support responded that the precedence is "a known, intentional design choice, not a defect," and that consumed API credits are non-refundable "regardless of cause," explicitly including cases where "a tool defaulted to API-key billing instead of a subscription." Support also confirmed in writing that the subscription token was restored before the billed runs began and that the environment variable was not manually configured by the user, and directed the remaining items here.
The refund is not the point of this issue. The point is item 2 above: one line of output before the first request would close this entire class of report.