A session's CLAUDE_CONFIG_DIR names a different account than the one it authenticates and bills under, and nothing in the session can tell

Status Open
Reported on v2.1.233
Maintainer reply None cached
Activity 3 comments · opened Aug 17, 2026

*Identities are labelled (account A/D, org A/D) rather than quoted. They are real logins on one
machine; I can supply actual values privately if that helps diagnosis.*

Summary

A desktop-app-launched session runs with CLAUDE_CONFIG_DIR set to a config root that is not the
root it authenticates under. On this machine the environment names a root belonging to account A
(a corporate login, org A), while every account record the desktop app writes for its own sessions
names account D (a webmail login, a different org). The account owner confirms billing is
account D.

Everything downstream trusts the environment and is therefore confidently wrong:

  • the model's session context reports account A's email
  • claude auth status, spawned from the session's own shell, reports account A
  • a machine-local usage governor reading the same signal reports account A

Three independent surfaces, one wrong answer, no disagreement between them to signal a problem. The
session cannot determine which account it is operating under, and neither can any tool it runs.

I am not asking for the credential store to be exposed. I am asking that the session's effective
account be a stated fact, and that CLAUDE_CONFIG_DIR either govern authentication or stop being
presented as the session's config root.

Environment

  • Windows 11 Pro 26200, Claude Code 2.1.233
  • Desktop app + VS Code extension + CLI, several concurrent sessions against one repo
  • 5 CLI config roots in $HOME: ~/.claude, ~/.claude-account-1 .. ~/.claude-account-4,

resolving to three distinct accounts across three organizations

  • Desktop-managed per-session roots under

%APPDATA%\Roaming\Claude\{claude-code-sessions,local-agent-mode-sessions}\...\.claude\

Repro A -- the environment names one account, the session authenticates as another

In a desktop-launched session:

$ echo "$CLAUDE_CONFIG_DIR"
C:\Users\<user>\.claude-account-1

$ claude auth status
{"loggedIn":true,"authMethod":"claude.ai","apiProvider":"firstParty",
 "email":"<account A>","orgId":"<org A>","orgName":"...","subscriptionType":"max"}

The model's own context likewise carries account A's email, and nothing else about identity.

Now count the account records the desktop app writes for its sessions:

$ grep -rhoE '"emailAddress"[[:space:]]*:[[:space:]]*"[^"]+"' \
    "$APPDATA/Claude/claude-code-sessions" "$APPDATA/Claude/local-agent-mode-sessions" \
  | sed 's/.*: *"//;s/"//' | sort | uniq -c

    143 <account D>

143 of 143 name account D. Zero name account A, which is the account the environment, the CLI,
and the model's context all report. The owner independently confirms account D is the billing
account.

So CLAUDE_CONFIG_DIR is being set to a root that does not govern the session's authentication,
while every consumer -- including Claude Code's own auth status -- treats it as authoritative.

Expected: a session can name the account it authenticates and bills under, and the environment
does not advertise a different one.
Actual: the environment advertises account A, the session bills account D, and no surface
available to the session reports the discrepancy.

Repro B -- the default config root reports logged in with no identity

Independently reproducible, and it removes the obvious fallback for tooling that suspects the
environment. Three ways of naming the same directory:

$ CLAUDE_CONFIG_DIR='C:/Users/<user>/.claude' claude auth status
{"loggedIn":true,"authMethod":"claude.ai","apiProvider":"firstParty","email":null,"orgId":null,"orgName":null,"subscriptionType":"max"}

$ CLAUDE_CONFIG_DIR='C:\Users\<user>\.claude' claude auth status
{"loggedIn":true,...,"email":null,"orgId":null,"orgName":null,"subscriptionType":"max"}

$ CLAUDE_CONFIG_DIR="$HOME/.claude" claude auth status
{"loggedIn":true,...,"email":null,"orgId":null,"orgName":null,"subscriptionType":"max"}

~/.claude/.credentials.json is present and subscriptionType is populated (max) in the same
response whose identity fields are null, so this is not a plain "not signed in" state. That root's
.claude.json contains no emailAddress key at all, while the other roots' do.

Expected: an identity, or loggedIn: false.
Actual: loggedIn: true with a null identity -- an answer no caller can act on.

Repro C -- unset and explicit-default disagree about the same directory

$ env -u CLAUDE_CONFIG_DIR claude auth status
{"loggedIn":true,...,"email":"<account B>","orgId":"<org B>",...}

$ CLAUDE_CONFIG_DIR="$HOME/.claude" claude auth status
{"loggedIn":true,...,"email":null,"orgId":null,"orgName":null,...}

Unset should mean the default root, documented as ~/.claude. Naming it explicitly yields a null
identity; leaving it unset yields account B, the identity of a different root. I did not diagnose
which resolution path produces that; I am reporting that the two disagree.

Why this is load-bearing, not cosmetic

I run a machine-local usage governor that reads account consumption and, at thresholds, tells
sessions to protect work in progress. It has to know which account a session bills to, so it read
the only signal available -- CLAUDE_CONFIG_DIR -- and has therefore been grading account A's
consumption while the sessions it governs spend account D's. It has been reporting a percentage for
the wrong account, at a threshold that triggers protective action, for as long as it has been
running.

Nothing in the product could have told it otherwise. That is the shape of the defect: not a wrong
number, but an unanswerable question with a confident-looking answer sitting where the answer should
be.

Concurrent sessions on this machine also report different accounts from each other, depending on
which root each was launched with, so comparing usage between sessions silently compares different
accounts.

What would fix it

  1. Put the session's effective account in the session's context -- email, organization id and

name, subscription type, and the config root actually governing authentication. A session should
be able to state which account it is on, and to decline to guess when it cannot.

  1. Add the same fields to the hook stdin payload, so machine-local governors, auditors and

schedulers stop reverse-engineering identity from undocumented state.

  1. Do not set CLAUDE_CONFIG_DIR to a root that does not govern the session's authentication.

If the desktop app manages a per-session root, the environment should name that root, or make
clear that the variable is not the auth source.

  1. Never return loggedIn: true with a null identity. Report the identity, or report not logged

in.

  1. Document how the default config root resolves when CLAUDE_CONFIG_DIR is unset.

Item 3 is the actual bug. Items 1 and 2 would have made it visible in minutes instead of weeks.

Related

  • #83951 -- no supported way to enumerate config roots (same multi-root setup, from the outside)
  • #83952 / #82323 -- hooks across multiple config roots: no receipt for which file governs
  • #84868 -- session listing conflates idle / not observable / gone
  • #87155 -- desktop tray icons indistinguishable when one instance per account is running

View original on GitHub ↗

3 Comments

wshallwshall · 13 days ago

Two corrections and one substantial strengthening, from further work on the same machine with two other sessions measuring independently.

Correcting the count in Repro A

The 143 in the original report is the number of files containing an emailAddress record, which is inflated by .claude.json.backup.* siblings. The number of distinct per-session .claude.json files is 30, and claude-code-sessions holds zero of them -- the population lives entirely under local-agent-mode-sessions.

The ratio is unchanged and is the load-bearing part: every record names the same account, and zero name the account that the environment, claude auth status, and the model's context all report.

Correcting an implication I should not have left standing

Those per-session roots are not a live lookup path. None of the 30 corresponds to a currently running session (checked against three live session ids). They are historical. So they establish which account the app spends; they do not give a running session anywhere to read its own identity from.

The stronger finding: no local source names the account being spent

Another session on this machine traced every branch of our usage tool's account resolver against a live session. Labelling orgs rather than naming them:

CLAUDE_CODE_ENTRYPOINT   claude-desktop    -> takes the desktop branch, correctly
session id               resolved          -> correct
desktop-record lookup    EMPTY             -> no desktop record for this session
CLI config-root lookup   org A             -> what our tooling reports
app-current-org fallback org B             -> a third answer
ACTUALLY BEING SPENT     org D             -> NO BRANCH RETURNS IT

Org D was identifiable only by value, from the app's own on-disk usage samples: org D's window was 4 minutes old and climbing while org A had recorded no sample in 45+ minutes, during which the session worked continuously. The account owner then confirmed org D.

Org D is recorded in no CLI config root on this machine. Enumerating config roots would not have found it, which is what distinguishes this from #83951 -- the answer is in none of them.

The mechanism, stated plainly

CLAUDE_CONFIG_DIR selects a settings tree. It does not re-authenticate a desktop-hosted session: the app spends its own token, which is held in OS safeStorage and is not readable from disk. So the variable is a session-specific signal about a different question than the one every consumer is using it to answer -- which is exactly why the wrong answer looks so trustworthy.

One consequence worth stating for anyone else building tooling on this: a fix that conditions the billing question on whether the app happened to write a session record is conditioning a billing question on a logging artifact. Whether a record was written cannot change which token was spent.

What none of us established

Nobody here has verified which credential actually signs a given session's requests. The evidence above makes org D the natural reading and the owner confirms it, but that is an inference plus an owner statement, not a measurement of the signing credential. Treated as a floor: attribution is unverified locally, and was measured wrong in at least three sessions today.

The practical impact stands regardless. Our usage governor spent the day grading an account these sessions do not spend -- reporting weekly 97% for org A while the account actually being billed sat at weekly 15% and its 5-hour window climbed from 60% to 73% in twenty minutes, unmonitored. The window that can stop work was the one nothing was watching.

wshallwshall · 13 days ago

One further datum, measured directly, and a correction to my previous comment.

The config file names two different accounts, in two different keys

The pinned config root's own .claude.json contains both accounts:

oauthAccount.accountUuid              -> account A   (alongside account A's org uuid and email)
cachedUsageUtilization.accountUuid    -> account D

Account D is the account the owner confirms is billed, and the account every per-session desktop record names. Account A is what the identity block says, and what claude auth status reports for this root.

So the client itself wrote account D's usage cache into a config root whose identity is account A. Everything needed to notice the contradiction sits in one file under two keys, and nothing compares them. A consumer reading oauthAccount to answer "which account am I?" gets one answer; a consumer reading cachedUsageUtilization.accountUuid gets the other. Both are Claude Code's own writes.

Stated precisely, because it would be easy to over-read: that cached block is roughly 96 hours old, so it is not a live signal, and I am not claiming it is an intended source of truth or that it tracks the current session. What it establishes is structural -- the two fields can and do name different accounts within a single config file. That is this same defect at rest rather than in motion.

Correcting my previous comment

I wrote there that the account being spent is recorded in no CLI config root. That is wrong as stated. It is recorded in one -- in a field nobody would think to read for identity, next to a field that says something else.

What I do not have

I do not have a reliable discriminator for when this fires. A candidate condition was proposed and tested on this machine today and then withdrawn by its author, because the test used to establish the healthy case could not distinguish "reporting the right subject" from "faithfully reporting the wrong subject" -- both produce well-formed, internally consistent output. I mention it only so the absence is explicit: the reproductions in the issue body stand on their own, but I cannot yet tell you which sessions are affected and which are not.

wshallwshall · 1 day ago

Still relevant. A session can run with CLAUDE_CONFIG_DIR naming one account's config root while authenticating and billing under a different account, and nothing inside the session distinguishes the two.

The consequence is specific: any tooling that reads the environment to decide which account a session belongs to gets a confident, wrong answer, and usage attributed to the wrong pool is invisible from inside the session that spent it.

This is the same multi-account gap as #83951 (no supported way to enumerate config roots) and #90188 (no view across the accounts on one machine). Each of the three is usable on its own, but they share a root: config roots are a convention with no first-party way to enumerate or attribute them.

Still happy to supply the real account and org values privately if that helps diagnosis.