Changing the Anthropic key in Settings does not change which account specialists spend
- Dominant language
- TypeScript
- Stars
- 1
- Forks
- 0
- Avg merge
- 1h 21m
- Merged PRs (30d)
- 41
Description
Changing the Anthropic credential in Settings does not change which account specialists spend. Reported: "when I change it to another account, it's not picking up."
One symptom, two causes. Both are in the spawn.
## Cause 1 — a live specialist is never let go of
The credential is read once, at spawn, and written into the child's environment:
- `src/daemon/claude-session.ts:250` — `const apiKey = via ? null : (this.opts.apiKey?.() ?? undefined);`
- `src/daemon/claude-session.ts:285-322` — the result is spread into `spawn(..., { env })`.
`start()` is the only caller. `src/daemon/registry.ts:775` passes it as a thunk (`apiKey: () => this.getApiKey()`), so the value is fresh at spawn — but nothing re-spawns.
`setApiKey()` at `src/daemon/registry.ts:308-318` records the new key and turns the switch on. It touches nothing on the roster. Every hot specialist keeps the process holding the previous account's credential, for the life of that process. `clearApiKey()` (`registry.ts:376-381`) and `setApiKeyEnabled()` (`registry.ts:326-329`) have the same gap.
The codebase already solves this exact problem for `--model`, which is likewise fixed at spawn — `setModel()` at `src/daemon/registry.ts:1332-1357`: record the change, `session.stop()` with a `stoppedBecause`, and the next prompt revives the specialist on the new value, resuming the same transcript. `setRole()` (`:1393`) and `setReasoningEffort()` (`:1360`) follow the same shape.
## Cause 2 — the parked branch is unreachable, so a revived specialist can still pick up the old credential
`claude-session.ts:250` ends in `?? undefined`. `??` fires on `null` as well as `undefined`, and `registry.getApiKey()` (`registry.ts:304-306`) returns `string | null` — never `undefined`. So `apiKey` is a string or it is `undefined`; it is never `null`, and this branch at `claude-session.ts:312-316` can never run:
```ts
: apiKey === null
? { ANTHROPIC_API_KEY: "none", CLAUDE_CODE_OAUTH_TOKEN: "none" }
: {}
```
Consequences, both of which survive a fix to cause 1 because they happen at spawn:
1. **Parking does nothing** when `ANTHROPIC_API_KEY` or `CLAUDE_CODE_OAUTH_TOKEN` is exported in the shell the daemon was started from. The child inherits `...process.env` (`claude-session.ts:288`), so the exported credential is still there. The cockpit says "Specialists use this machine's Claude login" and they do not. `registry.ts:772-774` states the requirement being missed: *"a parked key must reach the process as no key at all, or the switch in Settings is a control that moves and changes nothing."*
2. **Swapping credential type leaves the old one set.** `credentialEnv()` (`src/daemon/anthropic-key.ts:77-79`) sets one variable and clears neither. Save an `sk-ant-oat…` setup token from account B while account A's key is exported as `ANTHROPIC_API_KEY`, and the child gets both — B on `CLAUDE_CODE_OAUTH_TOKEN`, A still on `ANTHROPIC_API_KEY`.
Note `ANTHROPIC_API_KEY: "none"` would be wrong even if it were reachable: "none" is not a sentinel the CLI recognises, it is a literal credential that 401s, and the CLI retries a 401 ten times with a doubling delay. Omit the variable instead — Node's `spawn` drops env entries whose value is `undefined` (verified on node v24.17.0).
`getApiKey()` cannot distinguish "no key at all" from "key parked" — both are `null` — and the usage panel depends on its current signature (`src/daemon/index.ts:86`). The spawn needs a three-state answer of its own.
## Acceptance criteria
- [ ] Saving a key in Settings lets go of running specialists; the next prompt revives each one on the new credential, resuming the same transcript.
- [ ] Removing the key and flipping the on/off switch do the same.
- [ ] A specialist that is mid-turn (`ClaudeSession.turnStartedAt !== null`) is not killed under it. It finishes the turn and is let go at turn-end.
- [ ] The roster row says why it stopped — the credential changed — not "stopped by you". Follows `entry.stoppedBecause`, as `setModel` does.
- [ ] A parked key reaches the child as no credential at all: neither `ANTHROPIC_API_KEY` nor `CLAUDE_CODE_OAUTH_TOKEN` is set in the child, even when one is exported in the daemon's environment.
- [ ] Saving a console key clears `CLAUDE_CODE_OAUTH_TOKEN` in the child; saving a setup token clears `ANTHROPIC_API_KEY`. Never both set at once.
- [ ] A bench that has never had a key of its own still leaves the child's environment exactly as the daemon found it.
- [ ] No variable is ever set to the string `"none"`.
- [ ] The cockpit note at `src/client/components/AnthropicKey.tsx:141-142` no longer says the key only reaches specialists started after it, because it will no longer be true.
## Out of scope
- OpenRouter/Gemini key (`routerKey`). It has no on/off switch and is read per-turn through `viaFor()`, so it does not have this problem. Do not add a switch.
- The `.env` discovery in `src/daemon/env-file.ts`. It is correct — it deliberately never merges into `process.env`, which is why a `.env` key parks fine today.
- Restarting specialists eagerly. Lazy revival on the next prompt is the established pattern (`registry.ts:1328-1330`); do not spend a turn's startup on a key change the developer may still be adjusting.
- The usage panel and `getApiKey()`'s existing signature. `src/daemon/index.ts:86` depends on it.
- Anything about which of the two variables the Claude CLI prefers when both are set. The fix is to never set both.
## Verification
```
pnpm typecheck
pnpm test tests/claude-session.test.ts tests/registry.test.ts tests/anthropic-key.test.ts
```
`tests/claude-session.test.ts:571` ("leaves the environment as it found it when no key is set") passes `apiKey: () => null` and asserts the exported key leaks through. Under the corrected three-state contract `null` means *parked* and must not leak. That test needs to move to the "no key at all" state, and a new one added for parked — do not simply delete it.
Manual check a green build will not catch: with a specialist running, save a different account's key in Settings, send it a prompt, and confirm the reply is billed to the new account.
Contributor guide
Research direction
Start with setApiKey(), clearApiKey(), and setApiKeyEnabled() in src/daemon/registry.ts, then trace ClaudeSession.start() and credentialEnv() in src/daemon/claude-session.ts and src/daemon/anthropic-key.ts. Run the listed registry, session, and credential tests, including the existing no-key environment test. Done means credential changes safely release specialists at turn-end, lazy revival uses the new state, and child processes never inherit or combine credential variables incorrectly.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- backend, devtools, security
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 70/100