No supported way to enumerate config roots: `.claude-*` naming is a convention only, and a sibling `<config-dir>.lock` artifact is itself a DIRECTORY that a name scan accepts
Summary
There is no supported way for machine-local tooling to ask Claude Code which config roots exist on a machine. CLAUDE_CONFIG_DIR is documented as a per-process override, and the docs' own example name (alias claude-work='CLAUDE_CONFIG_DIR=~/.claude-work claude') is arbitrary, so any tool that needs to reach every login has to guess at a naming convention and scan for it.
That guess fails in a specific and silent way. Alongside one of my config directories sits a sibling named <config-dir>.lock, and it is a directory, not a file. A name scan for .claude-account-* therefore returns it as a config root. My hook installer copied files into it and wrote a settings.json into it, printed success, and governed nothing. A health check separately read it as a config root, found no wiring in it (of course -- nothing wires a lock artifact), and charged a required failure against an otherwise healthy machine.
Nothing about either outcome looked like a mistake. One read as a successful install. The other read as a broken machine.
I am not asking for the lock artifact to be treated as a bug. I am asking for a supported way to enumerate config roots, or a documented guarantee I can safely scan against.
Why tooling needs to enumerate at all
I run several concurrent sessions against one repo, under several logins on one machine: the default ~/.claude plus additional roots pointed at by CLAUDE_CONFIG_DIR. Some safety hooks have to be installed at user scope in every root, because:
- A project-scope hook lives in a git-tracked file, so it exists on one branch and is absent from the other worktrees until each merges it.
- A hook wired only into
~/.claudeleaves every other login ungoverned. That is not theoretical for me: a session running under an ungoverned login checked its own branch out inside another session's linked worktree and swapped that session's files mid-task. The guard that would have blocked it was simply not installed there.
So "write into every config root on this machine" is the operation. There is no supported way to name the set.
What happens
Environment: Windows 11, PowerShell 7, Claude Code 2.1.220, desktop app plus the VS Code extension, several logins on one machine.
Home directory contents (names as they appear):
.claude directory
.claude-account-1 directory
.claude-account-2 directory
.claude-account-2.lock directory <- not a config root
.claude-account-3 directory
.claude-account-2.lock is a directory. Creation timestamps show it appeared well after the config directory it sits beside, so it is a runtime artifact rather than something my launcher scripts created. I did not confirm which component writes it; the mkdir-as-lock idiom that produces a directory named <target>.lock is the same family as the orphaned .lock directories reported in #80772. Whatever writes it, the load-bearing fact for tooling is that it is a directory whose name matches the same prefix the real config roots use.
Minimal repro:
- Run one login on the default
~/.claudeand a second withCLAUDE_CONFIG_DIRpointed at~/.claude-account-2. Use them normally for a while. - List
~. A sibling~/.claude-account-2.lockis present, and it is a directory. - Enumerate config roots the only way available to third-party tooling, by name:
Get-ChildItem -Directory -Filter '.claude-account-*'. It returns two hits for one real root. - Write
settings.jsoninto the.lockhit and register a hook there. It is accepted by the filesystem and never read by the client.
Actual: a name scan cannot distinguish a config root from a same-prefix artifact, so an installer writes real configuration into a non-config directory and reports success.
Expected: either a supported enumeration that returns exactly the roots the client itself would use, or a documented rule for recognising one, so tooling can be right rather than lucky.
The trap in the obvious repair
The first fix that comes to mind is to accept a candidate directory only if it contains settings.json or a hooks/ directory. That check is self-confirming and must not be used: settings.json and hooks/ are exactly what a third-party installer writes. Run the installer once against a wrong directory and that directory qualifies as a config root forever after.
This is not hypothetical. The .lock directory that prompted all of this contained a settings.json and nothing else, because an earlier install had put it there. The evidence of the bug was the thing that would have made the naive check pass.
What I had to build instead
A resolver that accepts a candidate only on markers the client creates and third-party tooling never does (projects/, sessions/, .claude.json), rejects any name ending in .lock, accepts ~/.claude on existence alone because it is documented by exact name, lets an explicit operator-supplied path bypass the heuristic, and returns every rejected candidate with its reason so callers can print it. A silent skip reads exactly like a candidate that was never there, which is the failure mode this whole exercise exists to prevent.
In a public, MIT, de-identified repo of multi-session Claude Code tooling:
scripts/coord/_common.ps1--Resolve-CcxClientConfigRoot, the resolver and the long comment explaining why the marker list excludessettings.jsonandhooks/scripts/worktree/install-gate.ps1-- the hook installer that was writing into the lock directorybin/ccx-doctor.ps1-- the health check that charged a required failure against itscripts/worktree/install-selfheal.ps1,scripts/worktree/sessions.ps1-- other callers that scan all roots
Cost
- One installer silently governed nothing while printing success. The gate it installs exists to prevent concurrent sessions from corrupting each other's worktrees, so "installed but inert" is the worst possible state for it: strictly worse than not installed, because a visible absence gets fixed.
- One health check produced an invented red. A reader who chases a false failure once learns to discount the next one, which is a real cost on a tool whose only job is to be believed.
- About 50 lines of resolver, a test fixture, a documented rejection-reporting path through four call sites, and the time to work out that the obvious marker check was self-confirming.
Ask, in order of preference
- A supported enumeration. Something like
claude config roots --jsonthat returns the roots the client itself would use, so tooling stops guessing. This also helps the machine-readable-diagnostics direction generally. - Failing that, a documented recognition rule. State which files or directories the client guarantees to create inside a config root, so a third party has a stable, non-self-confirming marker. Also document that sibling scratch or lock artifacts may be created next to a config directory, and how they are named, so a scan can exclude them on purpose rather than by luck.
- Failing that, relocate the artifact. If the
<config-dir>.lockdirectory were created inside the config directory or in a separate state directory rather than as a sibling, no name-prefix scan could pick it up, and this class of mistake would not be reachable.
Any one of the three is sufficient. The first is the one that stops other people rediscovering this.
The linked repo is my own tooling and is not endorsed by or affiliated with Anthropic; it is included only as corroboration for the specific code described above.
---
Corroborating code, if useful: the workarounds described above are implemented in
<https://github.com/wshallwshall/claude-multisession> (MIT, PowerShell 7 + Windows-first). Paths
cited in this report are relative to that repository. It is one user's tooling, not an endorsed or
official approach -- linked as evidence that the problem is real and what it costs to work around,
not as a recommendation.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗