Detect a shadowing claude-use install, and survive what it writes - #17
Merged
Conversation
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
force-pushed
the
fix/stale-install-detection
branch
from
September 12, 2026 06:31
1775aa8 to
d378a64
Compare
Mearman
marked this pull request as ready for review
September 12, 2026 06:35
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A month-old claude-use was shadowing the installed one on PATH, and nothing said so.
claude-use identity listfailed withnaming a pattern this source hasn't contained since 838b99c widened it to allow
@. The installed v2.1.0 at~/.local/bin/claude-uselists 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.
doctorreports whichclaude-usea bare command name resolves to. It scans PATH for the filename a bareclaude-usewould hit, reusing thefindPathShadowscanshim enablealready runs forclaude, and compares the first hit against the running executable. An earlier entry winning is afail: 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 awarn(absolute-path andnpxinvocations are legitimate), and a shadowedclaudeshim is awarn(the launcher is still reachable asclaude-use run).refinePathShadowkeeps a symlink to the same real file from reading as a shadow of itself.identity listsurvives one unreadableidentity.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 todoctor. OnlySyntaxErrorandConfigValidationErrorare absorbed; a permission error still propagates.Identity enumeration ignores resync farm directories.
isIdentityDirectoryNamestates 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.*.doctorwas reporting each retained farm as an identity with a missingidentity.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: