Skip to content

[Bug]: Active Claude managed account is materialized into the user's default CLAUDE_CONFIG_DIR — sessions launched outside Orca bill the selected account #16016

Closed

Description

@bygama

Operating system

Windows (11 Pro, 10.0.26200)

Orca version

v1.4.182

Details

Short summary:
On the host runtime, an active Claude managed account is materialized into the resolved default CLAUDE_CONFIG_DIR — the same directory the user's own Claude CLI uses outside Orca. Every Claude process resolving that directory then bills the selected account, including sessions Orca did not launch. The WSL runtime already avoids this by injecting a per-account CLAUDE_CONFIG_DIR instead.

What happens

syncForCurrentSelection writes the managed account's credential straight into the resolved runtime paths, then rewrites that directory's .claude.json identity field:

  • src/main/claude-accounts/runtime-auth-service.ts:422-423 — const paths = this.pathResolver.getRuntimePaths() → this.writeRuntimeCredentials(credentialsJson)
  • src/main/claude-accounts/runtime-auth-service.ts:437 → writeRuntimeOauthAccount() (:1586-1601), which writes existing.oauthAccount into getRuntimePaths().configPath

The pre-swap state is snapshotted to claude-runtime-auth/system-default-auth.json and restored only on selection-change paths (:144, :249-253, :309-339, :429). I could not find any quit-time handler or startup reconciliation in the service, so if the app exits without a selection change, the foreign credential stays in the user's default directory.

The consequence is not Orca-scoped: the swap is a property of the directory, not of the panes Orca spawns. A claude session started from any terminal, a cron job, an editor extension, or another tool resolving the same directory bills the selected account for as long as the selection is active. In our case personal sessions billed a work account for hours; we found it through usage numbers, not through anything in the UI.

The WSL runtime already does the right thing

:619-624 returns { configDir: managedAuthPath, envPatch: { CLAUDE_CONFIG_DIR: wslLinuxAuthPath }, stripAuthEnv: true } — per-account isolation, nothing shared touched. The rationale is stated in-tree at :293:

"WSL managed accounts are isolated by their Linux CLAUDE_CONFIG_DIR; materializing into Windows ~/.claude would mix two auth stores."

That reasoning applies verbatim to the host path. Host is the outlier, not the norm.

Reproduction (Windows, no CLAUDE_CONFIG_DIR override)

  1. Add two Claude managed accounts, A (personal) and B (work).
  2. Select B as the active account in Orca.
  3. Outside Orca, in a plain terminal, run claude and send any prompt.
  4. The usage is billed to B. %USERPROFILE%\.claude\.credentials.json holds B's credential and .claude.json's oauthAccount is B.
  5. Kill Orca (Task Manager / crash) without clearing the selection. B's credential and identity remain in the default directory.

What I'd expect

  1. Never write managed credentials into a config dir the user also uses outside Orca. The active selection should resolve to the account's own managedAuthPath and be injected per spawn — the mechanism at :619-624, applied to host.
  2. Failing that: reconcile at startup and restore on quit, so a crash cannot leave a foreign credential behind.

Why this is not #6921 / #14254

#6921 (per-worktree binding) is explicitly additive — "Unassigned worktrees are unchanged". It adds an opt-in path that avoids materialization; it does not remove materialization for the global selection. Every user who does not bind every worktree keeps this behavior. Its implementation PR (#6963) was withdrawn by its author on 2026-08-21, so it is not a scheduled fix either. #14254 was my own feature request for Codex-parity per-pane selection; this is the defect underneath it, filed separately because it stands on its own and does not require that feature to land.

Precedent: the Codex equivalents of exactly this class are tracked as their own open issues — #8612 ("Orca silently redirects system Codex state away from ~/.codex") and #5370 ("Orca's private CODEX_HOME causes an auth-revocation war with the user's ~/.codex").

Note on scope: an earlier report of mine claimed the swap leaves oauthAccount stale. That was wrong — :437 does write it. The billing and crash-persistence behavior above is unaffected by that correction.

Open question: on entry, captureSystemDefaultSnapshotForManagedEntry (:1136-1159) preserves the prior snapshot when the runtime credential equals the managed one, which covers a crash-leftover of the same account. If Orca restarts with a different account selected, the leftover credential differs from the newly-managed one and takes the force: true branch — is the foreign credential then recorded as the system default? I have not reproduced this and may be misreading the flow.

Activity

  1. fbaltor commented on Sep 10, 2026

    @fbaltor

    Corroborating the shared-runtime credential mechanism on NixOS/Linux x86_64, Orca 1.4.199, Claude Code 2.1.267, using a headless Orca server and a remote laptop client.

    Our reproduction confirms an existing session's displayed account changes after switching the managed account. We have not verified cross-account billing or credential persistence after a crash, so this is narrower than the full reproduction in the issue body.

    Steps and observed result

    1. Register two Claude subscription accounts, A and B, on the server using orca-ide account add --agent claude.
    2. Select A in the remote client's AI Provider Accounts settings.
    3. In an Orca terminal on the server, launch plain claude. Run /status: it shows A. Keep this process open.
    4. Select B in Orca. The UI warns to restart live Claude terminals before continuing old sessions.
    5. Launch another claude session: /status shows B.
    6. Return to the older session, dismiss its previous status screen, and run /status again. The same session ID now shows B, without restarting it or running /login there.

    Read-only /proc inspection confirmed both test Claude processes had the same home directory, with CLAUDE_CONFIG_DIR and CLAUDE_SECURESTORAGE_CONFIG_DIR unset. Neither process had ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, or CLAUDE_CODE_OAUTH_TOKEN in its launch environment. No credential values are included here.

    Source confirmation in v1.4.199

    • runtime-paths.ts resolves the host runtime to the shared default Claude directory when no override is inherited.
    • runtime-auth-preparation.ts returns shared paths for the host branch, while the managed WSL branch supplies a per-account config directory.
    • runtime-auth-sync.ts writes both runtime credentials and account metadata for the selected account.
    • The Linux test in runtime-auth-service-deselect-restore.test.ts, beginning at line 180, explicitly asserts replacement of the shared credentials and metadata when selecting different accounts. We inspected this test; we did not execute it.

    The account-switching documentation describes existing Codex sessions retaining their original account and then presents Claude switching as identical. That led us to expect isolation, which this host implementation does not provide. At minimum, the documentation should distinguish the two behaviors; ideally, host launches should receive separate account-specific credential directories without rewriting the default login.

    We support fixing the shared-directory behavior tracked here. The changed /status identity is evidence of shared account state, not proof of which cached token a subsequent inference request uses or which subscription it charges.

  2. johnmarktaylor91 commented on Sep 14, 2026

    @johnmarktaylor91

    Additional evidence from a Linux server-mode install (Orca 1.4.201, Claude Code 2.1.270).

    I quarantined ~/.claude/.credentials.json three times in one evening and it came back each time, byte-identical (same SHA-256) to the token of the active managed Claude account. A 2-second poller caught the third recreation at 22:41:40Z: no claude process had been started in the preceding minute; the only new process was Orca's own Codex app-server spawn one second earlier, so the write comes from the Orca main process itself, not from a launched CLI.

    Consequence on a shared host: any plain claude invocation that reaches the default config directory silently bills the managed account. I now route typed claude through a wrapper that requires CLAUDE_CONFIG_DIR, but a way to disable the materialization (or scope it to Orca-launched processes only) would remove the hazard at the source.

  3. added theissue type on Sep 19, 2026
  4. Justin3546 commented on Sep 20, 2026

    @Justin3546

    Confirming on macOS host runtime, v1.4.206 (Darwin 27.0.0). Same root cause, with one outcome that is worse than the Windows report and one gap in the restore path.

    Setup that makes it sharp

    Several sibling Claude config dirs, each holding a different account, selected per-process with CLAUDE_CONFIG_DIR:

    ~/.claude             work org  (team plan)
    ~/.claude-personal    personal org (max plan)
    

    So ~/.claude is not "the default directory", it is a specific account. Both use the same email and differ only by organization, which is also why #21182 bites here: nothing in the UI distinguished them.

    What happened

    Added both as managed accounts. Within the same minute, ~/.claude/.claude.json was rewritten with the active managed account's oauthAccount identity. More importantly, the displaced account was left fully signed out, not merely mis-billed:

    $ CLAUDE_CONFIG_DIR=~/.claude claude auth status
    { "loggedIn": false, "authMethod": "none", ... }
    

    The Windows report describes foreign credentials billing the wrong account. On macOS the original credential is moved, so the directory is left with no usable login at all. Recovery was a manual claude auth login; nothing in Orca offered to put it back.

    The snapshot is captured but not restored on account removal

    claude-runtime-auth/system-default-auth.json was written at the moment of the swap, containing credentialsJson, configOauthAccount, keychainCredentialsJson, scopedKeychainCredentialsJson, legacyKeychainCredentialsJson and capturedAt. Capture works.

    Removing all managed Claude accounts did not restore it. Afterwards the account list is empty, claude-accounts/ is empty, the Orca Claude Code Managed Credentials keychain item is gone, and the displaced directory was still signed out until I logged in by hand. The snapshot file remains on disk, unrestored.

    So the restore gap is wider than "app quits without a selection change". Removing the managed accounts is also a path that leaves the resolved default clobbered.

    The Codex path in the same build does the right thing

    codex-runtime-home/home                       <- isolated home
    codex-runtime-home/system-default-auth.json   <- snapshot
    claude-runtime-auth/                          <- snapshot only, no isolated home
    

    Managed Codex accounts never touched the real ~/.codex or ~/.codex-firm; both kept their original account_id. That is the same isolation this issue notes WSL already does for Claude via envPatch: { CLAUDE_CONFIG_DIR: ... }. Host Claude looks like the only one of the three without an isolated home.

    Suggested fixes, in preference order:

    1. Give host Claude an isolated home per managed account and inject CLAUDE_CONFIG_DIR, as WSL Claude and host Codex already do. Never write into the resolved default dir.
    2. Failing that, restore the snapshot when the last managed account is removed, and reconcile at startup when a snapshot exists with no active selection.
    3. At minimum, detect that the resolved default dir already holds a signed-in account that is not managed, and warn before taking it over.
  5. OrcaWin commented on Oct 11, 2026

    @OrcaWin
    Collaborator

    Thanks for the careful analysis. This is fixed on main by #26801: managed Claude accounts now live in per-account folders that Orca's launches point to, so the default CLAUDE_CONFIG_DIR is no longer rewritten.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions