Skip to content

Detect a shadowing claude-use install, and survive what it writes - #17

Merged
Mearman merged 2 commits into
mainfrom
fix/stale-install-detection
Sep 12, 2026
Merged

Detect a shadowing claude-use install, and survive what it writes#17
Mearman merged 2 commits into
mainfrom
fix/stale-install-detection

Conversation

@Mearman

@Mearman Mearman commented Sep 12, 2026

Copy link
Copy Markdown
Member

A month-old claude-use was shadowing the installed one on PATH, and nothing said so. claude-use identity list failed with

Invalid configuration in ~/.claude-use/identities/joseph.mearman@exadev.io/identity.json:
  name: Invalid string: must match pattern /^[A-Za-z0-9][A-Za-z0-9._-]*$/

naming a pattern this source hasn't contained since 838b99c widened it to allow @. The installed v2.1.0 at ~/.local/bin/claude-use lists that identity fine; a v1.5.0 binary reached through a wrapper script in an earlier PATH directory does not. The identity.json was written by the new binary and read by the old one.

Three changes, none of which would have prevented that mismatch (a schema widening is forward-only, and an old binary's copy of the regex is frozen), but which together make it visible and survivable.

doctor reports which claude-use a bare command name resolves to. It scans PATH for the filename a bare claude-use would hit, reusing the findPathShadow scan shim enable already runs for claude, and compares the first hit against the running executable. An earlier entry winning is a fail: it invalidates the rest of the report, since every other finding describes the binary that produced it rather than the one being invoked. Not on PATH at all is a warn (absolute-path and npx invocations are legitimate), and a shadowed claude shim is a warn (the launcher is still reachable as claude-use run). refinePathShadow keeps a symlink to the same real file from reading as a shadow of itself.

identity list survives one unreadable identity.json. It was aborting the whole listing, so a single bad file hid every other identity. The bad entry now prints inline with its reason and a pointer to doctor. Only SyntaxError and ConfigValidationError are absorbed; a permission error still propagates.

Identity enumeration ignores resync farm directories. isIdentityDirectoryName states the rule the old "no identity.json means not an identity" check was only implying: an identity name must start with a letter or digit, so a leading dot is always .<identity>.scratch.*/.<identity>.previous.*. doctor was reporting each retained farm as an identity with a missing identity.json, which on any install with retained farms was enough on its own to make it exit non-zero.

Verified against the real environment that reproduced this:

PATH resolution:
  [FAIL] claude-use: `claude-use` on PATH resolves to ~/.dotfiles/bin/.local/bin/claude-use, not this
         running executable, ... Every command you type runs that one instead, at whatever version it
         happens to be — including the checks in this report, which describe this executable.

readJson throws on a schema violation, and listIdentities only tolerated
a missing file, so a single unreadable identity.json aborted
`claude-use identity list` outright and hid every other identity at the
moment they most needed to be visible.

The file need not be corrupt to land there. An identity.json written by
a claude-use whose naming rule has since widened is rejected by an older
binary's own copy of IdentitySchema, so an install that is merely behind
turns a valid file into a fatal one.

listIdentities now reports that entry as its own
UnreadableIdentityListEntry carrying the flattened reason, discriminated
from a good entry by which of identity/problem is present, and the list
command prints it inline and points at `doctor` for the detail. Only the
two failures a file's own content can produce are absorbed, a SyntaxError
from JSON.parse and a ConfigValidationError from the schema; a
permission error still propagates.

A wholly absent identity.json stays a silent skip, since that is what
keeps a resync's retained superseded farms out of the listing. That rule
was doing the job only indirectly, so isIdentityDirectoryName now states
it: an identity name must start with a letter or digit, so a leading dot
can only be a `.<identity>.scratch.<suffix>` or
`.<identity>.previous.<suffix>` directory, which is skipped outright
rather than depending on whether a crash happened to leave a readable
identity.json inside it.
Nothing anywhere answered whether the claude-use producing a report is
the one a shell reaches. An abandoned install directory or a hand-written
wrapper script from an earlier channel sitting ahead of the current
install on PATH keeps working at whatever version it was frozen at, so
every command runs the old binary while the new one sits shadowed and
never invoked. Nothing looks broken until a config file written by the
newer version trips the older one's own validation, and even then the
error names a rule the current source no longer contains.

doctor now scans PATH for the filename a bare claude-use resolves to,
reusing the findPathShadow scan shim enable already runs for claude, and
compares the first hit against the running executable's own PATH-visible
location. An earlier entry winning is a failure rather than a warning
because it invalidates the rest of the report: every other finding
describes the binary that produced it, which in that state is not the
binary the user's commands reach. Not being on PATH at all stays a
warning, since an absolute-path or npx invocation is legitimate, and an
enabled claude shim being shadowed stays a warning too, since the
launcher is still reachable as `claude-use run`.

findPathShadow compares directories, so refinePathShadow collapses the
verdict back to ok when both names turn out to resolve to the same real
file through a symlink. commandFilename generalises
claudeTargetFilename's Windows .exe rule so the PATH scan looks for the
filename PATH would actually hold rather than restating that condition.

doctor's identity enumeration also adopts isIdentityDirectoryName, so a
resync's own scratch and superseded-farm directories stop being reported
as identities with a missing identity.json. On a healthy install with any
retained farm at all, that noise alone was enough to make doctor exit
non-zero and be useless as the consistency gate it is meant to be.
@Mearman
Mearman force-pushed the fix/stale-install-detection branch from 1775aa8 to d378a64 Compare September 12, 2026 06:31
@Mearman
Mearman marked this pull request as ready for review September 12, 2026 06:35
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 12, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
🔒 Security Review Completed 2026-09-12T06:39:39.915535Z d378a64 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@Mearman
Mearman merged commit 75ae588 into main Sep 12, 2026
30 checks passed
@Mearman
Mearman deleted the fix/stale-install-detection branch September 12, 2026 06:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant