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

Status Open
Maintainer reply None cached
Activity 0 comments · opened Sep 3, 2026

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 login completed successfully at 2026-09-03T10:14:01Z (Login successful. in the log). No "both credentials active" notification was shown.

Environment / context

  • The ANTHROPIC_API_KEY lives in a project .env.local. It exists for one unrelated production purpose: a Supabase Edge Function that translates injury reports, hard-coded to claude-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.json and settings.local.json, and 1,337 lines of shell history.
  • Runs were launched via claude -p from 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

  1. Have an active Claude subscription, logged in via OAuth.
  2. Place ANTHROPIC_API_KEY=<key> in a project .env / .env.local (e.g. for an unrelated production service).
  3. From a shell where that variable is in the environment, run claude -p "<some long task>" in that project.
  4. 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:

  1. claude -p refuses to start when an environment ANTHROPIC_API_KEY would override an active subscription credential, unless an explicit opt-in flag (e.g. --billing=api) is passed.
  2. claude -p prints the billing source on stderr before the first request — e.g. billing: API key (…FgAA) — subscription credential present but not used.
  3. The -p JSON result includes a billing_source field alongside total_cost_usd.
  4. 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 -p recommended 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 .env ANTHROPIC_API_KEY silently used instead of Max OAuth, ~US$52 — closed as "not planned", no staff response, marked stale
  • #53728 — silent ANTHROPIC_API_KEY precedence 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.

View original on GitHub ↗