code-yeongyu / code-yeongyu/senpi
[Bug]: stale ANTHROPIC_API_KEY captured by the multi-session RPC host silently overrides claude-sdk-oauth and surfaces only as "Credit balance is too low"
- Dominant language
- TypeScript
- Stars
- 429
- Forks
- 98
- Avg merge
- 5h 3m
- Merged PRs (30d)
- 526
Description
## Summary
A stale `ANTHROPIC_API_KEY` in the environment of the detached multi-session RPC host silently overrides the `claude-sdk-oauth` provider's subscription OAuth for **every session served by that host**. All affected sessions fail with a bare `Credit balance is too low` assistant message (usage all-zero), while nothing in the UI or logs indicates that API-key billing was used instead of the configured OAuth path. Because the host daemonizes and is reparented to systemd, the poisoned environment survives `unset`, new terminals, `claude /login`, and credential rotation — only killing the host recovers.
## Environment
- omo 5.0.0-0.beta.26 (engine: senpi 2026.8.28-2), bun global install
- `CLAUDE_AGENT_SDK_VERSION=0.3.241`, entrypoint `sdk-ts`
- Linux x64
- `settings.json`: `defaultProvider: claude-sdk-oauth`, `claudeSdkOauthProvider: { enabled: true, tokenInjection: "ambient" }`
- Ambient credentials: `~/.claude/.credentials.json`, `subscriptionType: "max"` (rate limit tier `default_claude_max_20x`) — no API-key billing intended
## What happened (timeline, KST)
1. `21:41` — a shell that happened to have `ANTHROPIC_API_KEY` exported (sourced from a project `.env`) triggered the spawn of the multi-session RPC host (`cli-main.js --mode rpc --multi-session --listen unix:///tmp/senpi-rpc-host-internal-*/host.sock`, daemonized via `modes/rpc/host-lifecycle.js`, PPID 1). The host captured that environment permanently.
2. `21:48–21:55` — sessions served by this host worked, but were silently billed against the API key's console org credits (25K–33K token calls visible in session JSONL).
3. `21:56` — the console org's credit balance ran out. **Four live sessions failed simultaneously**, and every new session failed instantly with `Credit balance is too low` (assistant message, `usage` all zero, `stopReason: "stop"`, diagnostics `claude_sdk_oauth_session_continuity { kind: "bootstrap", reason: "registry_miss" }`). 10+ session files affected.
4. `22:17–22:29` — `claude login`, credential refresh, and account rotation had no effect: the failing path never consults OAuth while the env var is present in the host.
5. Meanwhile a session hosted by a *different, older* senpi process without the env var kept working on the same machine with identical config/credentials — which is what made this so confusing to diagnose.
## Root cause
- The Claude Agent SDK child (`@anthropic-ai/claude-agent-sdk-linux-x64/claude --output-format stream-json`) inherits the RPC host's environment. With `ANTHROPIC_API_KEY` present, the SDK prefers it over ambient OAuth, so requests bill console API credits instead of the Max subscription.
- Verified empirically with two SDK children spawned minutes apart from different hosts:
- child of the poisoned RPC host: env contains `ANTHROPIC_API_KEY=sk-ant-api03-…` → `Credit balance is too low`
- child of a clean host: no key in env → OAuth, works
- Calling `POST /v1/messages` directly with that key returns HTTP 400 `"Your credit balance is too low to access the Anthropic API"` — the exact string surfaced in-session.
- The amplifier is the host architecture: the first client to spawn the RPC host donates its (possibly dirty) environment to **all** future sessions, indefinitely, because the daemon outlives every shell.
## Repro
```bash
export ANTHROPIC_API_KEY=
omo # first launch spawns the detached RPC host, which inherits the env
# -> every session through this host: "Credit balance is too low", despite valid Max OAuth
unset ANTHROPIC_API_KEY
omo # still broken; only `pkill -f 'mode rpc'` (host + host-lifecycle) recovers
```
## Suggested fixes
1. When `claude-sdk-oauth` is the selected provider with `tokenInjection: "ambient"` (i.e. OAuth is the user's intent), scrub `ANTHROPIC_API_KEY` from the SDK child's environment (or the RPC host's at startup) unless the user explicitly opted into API-key auth.
2. Failing that, surface the resolved credential path (api-key vs oauth) in the status line and/or attach it to provider errors, so `Credit balance is too low` on a subscription account is diagnosable without dumping `/proc//environ`.
3. Consider documenting (or fixing) that the multi-session RPC host pins the environment of whichever client first spawned it.
## Workaround
- `pkill -f 'modes/rpc/host-lifecycle'` + `pkill -f 'mode rpc'`, then relaunch from a clean shell.
- Defensive alias: `alias omo='env -u ANTHROPIC_API_KEY omo'`.
Contributor guide
Research direction
Read modes/rpc/host-lifecycle.js and cli-main.js, then trace how the detached RPC host passes its environment to the Claude Agent SDK child and how provider selection is applied. Reproduce with and without ANTHROPIC_API_KEY, checking whether claude-sdk-oauth still uses OAuth and whether the resulting error identifies the credential path.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- bun, typescript
- Domain
- authentication, backend, cli
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100