fix: sweep pre-rename ~/.codev/bin shims that break claude on Windows#204
Open
quickbeard wants to merge 1 commit into
Open
fix: sweep pre-rename ~/.codev/bin shims that break claude on Windows#204quickbeard wants to merge 1 commit into
claude on Windows#204quickbeard wants to merge 1 commit into
Conversation
Upgraded users report `claude` dying with:
Starting Claude Code...
Error: Failed to change directory to <cwd>\claude
That string is OpenCode's (`$0 [project]` resolves an unrecognized
positional against cwd and chdir's into it), never Claude Code's. Two
0.4.0 renames combine to reach it:
- #194 moved `codev` from the hub to the CoDev Code agent, so a shim
body of `codev claude %*` now hands `claude` to an OpenCode fork as
its project dir.
- #195 moved the shim dir to ~/.codev-hub/bin without migrating the
old one, leaving those exact shims on PATH.
So `claude` hits the current shim, the hub prints its banner and spawns
`claude` with only ~/.codev-hub/bin stripped — and the legacy shim is
still on PATH to catch it. repairShims can't help: it only reads
shimDir(). Neither installShims nor unhook has ever touched the old dir.
cmd.exe is the exposure. rc files and PowerShell profiles share a
sentinel across the rename and get rewritten in place, and the profile's
`function claude {...}` shadows PATH outright. cmd.exe has no profile and
no aliases — it resolves purely from the registry PATH, where the old
installer wrote %USERPROFILE%\.codev\bin permanently.
Add sweepLegacyShims(), called best-effort beside repairShims() before
dispatch: delete the legacy shims (including the pre-0.4 codev-code one),
drop the emptied dir and its ~/.codev parent, and remove it from the
user-scope Windows PATH. Deleting the files is the load-bearing half and
fixes every platform on the spot; the PATH edit only stops the dir
lingering. The sweep returns early when it finds none of our shims, which
keeps it off the PowerShell path — it runs on every startup, and a spawn
per command to re-report a done migration would tax every command.
Also strip both dirs in stripShimDirFromPath (case-insensitively on
Windows) as a second layer: it saves the invocation when deletion is
blocked, and when the process's own PATH still carries the entry the
sweep just removed from the registry.
Verified against a reproduced broken install: shims deleted with an
unmigrated ~/.codev/auth.json preserved, and with the sweep blocked
(read-only dir) and the legacy dir first on PATH, `codevhub claude`
resolves past the surviving shim to the real claude.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
quickbeard
added a commit
that referenced
this pull request
Jul 17, 2026
The eager merge regressed Windows CI. tls.getCACertificates("system") is
synchronous and costs ~20ms on macOS but ~300ms+ on Windows, where it
blocks the event loop — and loggedFetch ran it before the first request
of every command, including in tests.
Windows per-file durations vs the same runner on #204:
backend.test.ts 172ms -> 511ms (+197%)
TaskList.test.tsx 7,644ms -> 19,541ms (+156%)
ConfigApp.test.tsx 10,091ms -> 17,240ms (+71%)
InstallApp.test.tsx 57,903ms -> 87,657ms (+51%)
Everything slowed, including TaskList, which never fetches — the stalls
starved Ink's render timers, and the three tests with the tightest budgets
failed. Not a test-only problem: every networked command on Windows would
have eaten the same stall.
Invert it. Run the request first; only when it fails with a cert error do
we merge and retry once (fetchTrustingSystemCa). A cert error is a precise
signal that the user is one of the affected minority, so the happy path
now costs exactly zero and only affected users pay. applySystemCaCertsOnce
returns null once it has run, bounding this at one retry per process — a
chain that stays untrusted surfaces its error instead of looping. Replay
is safe: a TLS handshake fails before any body is sent, and every call site
passes a replayable body (string/URLSearchParams/FormData/Buffer).
certHint is now pure for the same reason: reading the store to word a
sentence would put the stall back on the error path, and the retry already
read it.
Tests pin both halves — a successful request never touches the OS store,
and a cert failure merges, retries once, then gives up. Re-verified
end-to-end: with the root in the OS store login still succeeds (HTTP 200);
without it the error still carries the remedy; the store is read only on
the failure path.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
quickbeard
added a commit
that referenced
this pull request
Jul 17, 2026
…#205) * fix: trust the OS certificate store so login works behind TLS proxies Users behind a corporate proxy hit this at the Login step: Login failed: fetch failed (self-signed certificate in certificate chain) Node verifies TLS against its own bundled Mozilla CA snapshot and never consults the OS trust store (tls.rootCertificates: "fixed at release time... identical on all supported platforms"). A TLS-intercepting proxy (Zscaler/Netskope/Fortinet) or HTTPS-scanning AV re-signs every chain with a corporate root that MDM/GPO installed into the *OS* store — so the user's browser works and we fail. That per-machine variation is why only some users are affected. The error code pins it: SELF_SIGNED_CERT_IN_CHAIN is an untrusted self-signed ROOT in the presented chain (interception), not a self-signed leaf, which reports DEPTH_ZERO_SELF_SIGNED_CERT instead. Merge the OS store into Node's default CA set via tls.getCACertificates("system"), which reads that store even without the --use-system-ca flag — so affected users need no configuration. Load-bearing details: - Merge "default" + "system", never "bundled" + "system". "default" already folds in NODE_EXTRA_CA_CERTS; narrowing it would silently drop the certs of users who fixed this the documented way. - Call it from loggedFetch, not at startup: it costs ~25ms (a native OS-store read) and commands that never open a socket shouldn't pay. Every network path goes through loggedFetch (16 call sites, zero raw fetch), so first-request is the cheapest and completest hook. - Every failure mode is a no-op, including an empty system store. Trust config isn't ours to have opinions about, and a bad merge would break users whose certs already work. - The APIs need Node >=22.19/24.5 (our floor is 22.5), hence tlsApi.supported(). Accessed off the default import, never destructured — a named ESM import of a missing builtin export is a link-time error on older Node. Also add describeNetworkError: unwrap Node's bare `fetch failed` (the real reason hides on err.cause) and append a remedy for cert codes. Node only volunteers its own --use-system-ca hint for DEPTH_ZERO / UNABLE_TO_VERIFY_LEAF / UNABLE_TO_GET_ISSUER, which pointedly excludes SELF_SIGNED_CERT_IN_CHAIN — the corporate-proxy case it most exists for prints nothing. When a cert error survives the merge the root isn't in the OS store either, so the hint names NODE_EXTRA_CA_CERTS rather than --use-system-ca, which would be a dead end. Verified by driving the real loggedFetch against a proxy-shaped chain (leaf + untrusted self-signed root): with the root in the OS store login now succeeds (HTTP 200) where it previously failed with the exact reported error; without it, the error carries an actionable remedy. Note the blast radius: this covers our own process only. `npm i -g` during install and the agents themselves are separate processes behind the same proxy and need NODE_EXTRA_CA_CERTS / npm's cafile of their own. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: merge the OS CA store only after a cert failure, not up front The eager merge regressed Windows CI. tls.getCACertificates("system") is synchronous and costs ~20ms on macOS but ~300ms+ on Windows, where it blocks the event loop — and loggedFetch ran it before the first request of every command, including in tests. Windows per-file durations vs the same runner on #204: backend.test.ts 172ms -> 511ms (+197%) TaskList.test.tsx 7,644ms -> 19,541ms (+156%) ConfigApp.test.tsx 10,091ms -> 17,240ms (+71%) InstallApp.test.tsx 57,903ms -> 87,657ms (+51%) Everything slowed, including TaskList, which never fetches — the stalls starved Ink's render timers, and the three tests with the tightest budgets failed. Not a test-only problem: every networked command on Windows would have eaten the same stall. Invert it. Run the request first; only when it fails with a cert error do we merge and retry once (fetchTrustingSystemCa). A cert error is a precise signal that the user is one of the affected minority, so the happy path now costs exactly zero and only affected users pay. applySystemCaCertsOnce returns null once it has run, bounding this at one retry per process — a chain that stays untrusted surfaces its error instead of looping. Replay is safe: a TLS handshake fails before any body is sent, and every call site passes a replayable body (string/URLSearchParams/FormData/Buffer). certHint is now pure for the same reason: reading the store to word a sentence would put the stall back on the error path, and the retry already read it. Tests pin both halves — a successful request never touches the OS store, and a cert failure merges, retries once, then gives up. Re-verified end-to-end: with the root in the OS store login still succeeds (HTTP 200); without it the error still carries the remedy; the store is read only on the failure path. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
The report
Users on Windows hit this when launching
claude:The literal word
claudeappended to their project path is the tell. That string is OpenCode's, never Claude Code's — OpenCode's default command is$0 [project], so an unrecognized positional gets resolved against cwd andchdir'd into (tui.ts, same shape as anomalyco/opencode#2269). Nothing in@anthropic-ai/claude-codeemits it. The spaces in the path are incidental.Root cause
Two 0.4.0 renames combine on any upgraded machine:
codevfrom the hub to the CoDev Code agent — so a pre-0.4 shim body ofcodev claude %*no longer re-enters the hub, it handsclaudeto an OpenCode fork as its project dir.~/.codev/bin→~/.codev-hub/binwithout migrating the old one, leaving those exact shims on PATH.The chain:
claude→ current shim →codevhub claude→ hub prints its banner and spawnsclaude, stripping only~/.codev-hub/bin→ the legacy shim is still on PATH and catches it →codev claude→ agent chdirs into./claude. That's why the banner and the error appear together, exactly once.repairShims()was written for precisely this hazard, but only readsshimDir()— the legacy dir is invisible to it. NeitherinstallShimsnorunhookhas ever touched the old dir, so it persists indefinitely.cmd.exe is the exposure. rc files and PowerShell profiles share a sentinel across the rename and get rewritten in place by the next
installShims, and the profile'sfunction claude {...}shadows PATH outright — those users never notice. cmd.exe has no profile and no aliases; it resolves purely from the registry PATH, where the old installer wrote%USERPROFILE%\.codev\binpermanently. Any fix touching only rc files leaves cmd.exe broken.The fix
sweepLegacyShims(), called best-effort besiderepairShims()before dispatch: delete the legacy shims (including the pre-0.4codev-codeone), drop the emptied dir and its~/.codevparent, and remove it from the user-scope Windows PATH.stripShimDirFromPathnow strips both dirs (case-insensitively on Windows) as a second layer: it saves the invocation when deletion is blocked (read-only dir, permissions) and when the process's own PATH still carries the entry the sweep just removed from the registry.installShimsrewrites it in place, and once the shims are gone it points at nothing.Verification
Beyond the unit tests, I drove the built bundle against a reproduced broken install:
~/.codev/auth.json→ shims deleted,bin/removed,auth.jsonand its parent correctly preserved.codevhub clauderesolved past the surviving shim to the realclaude. Counterfactual on that identical PATH: resolvingclaudedirectly reaches the legacy shim — confirming the strip is what saved it.pnpm fix/typecheck/test(936 passing) /build && node dist/index.js --versionall green.Notes for reviewers
codevhubcommand once to trigger the sweep.claudeitself doesn't route through the hub in the failing chain, socodevhub --versionis enough — worth a release note.origin/feature/agent-readiness(680884a) independently strips the legacy dir for readiness detection; it'll now overlap withstripShimDirFromPathand should be reconciled on merge.repairShimsandsweepLegacyShimslook redundant but each only sees its own dir, and the OpenCode error string is a reliable fingerprint for this bug in future reports.🤖 Generated with Claude Code