fix: freeze/guard/investigate deny every edit on Windows (drive-letter paths fail the absoluteness test) - #2876
Open
kevingrasso32 wants to merge 1 commit into
Open
kevingrasso32 wants to merge 1 commit into
kevingrasso32 wants to merge 1 commit into
Conversation
Claude Code on Windows hands Edit/Write a drive-letter absolute path.
check-freeze.sh tested absoluteness with `case "$FILE_PATH" in /*)`, which
a native path does not match, so cwd was prepended and the comparison ran
against a mangled path:
[freeze] Blocked: /c/dev/proj/.claude/worktrees/lane/C:\dev\proj\...\
validation_ui/ref_picker.py is outside the freeze boundary
Nothing normalised `\` before the prefix comparison either. Because freeze
is fail-closed by design there was no softer failure mode than a hard deny,
on EVERY edit — including files sitting inside the frozen directory. /guard
and /investigate register the same hook, so all three skills were unusable
on Windows; the session that found this had to fall back to editing through
Bash to make any progress.
Add gstack_hook_normalize_path to the shared hook-extract.sh, and run both
sides of the boundary comparison through it:
C:\dev\x -> /c/dev/x (drive letter lowercased, \ -> /)
c:/dev/x -> /c/dev/x (mixed separators, either drive case)
\\srv\sh\x -> //srv/sh/x (UNC host prefix preserved)
/cygdrive/c/x -> /c/x (Cygwin mount prefix names the same drive)
The state file may legitimately hold either dialect — /freeze writes it with
`pwd`, which answers /c/dev/... under Git Bash — so normalising one side was
never enough. _resolve_path also re-normalises after `pwd -P`, which answers
in the platform's own dialect, and the slash squeeze now preserves a leading
`//` so a UNC host is not collapsed into a local path.
Two constraints shaped the implementation:
Builtins only, no subshell. Process creation under Windows bash measured
~0.7-1.5s on the reporting machine, so a tr/cut version added seconds to
every Edit. The function returns its result in GSTACK_HOOK_PATH rather than
on stdout — which is the hook's decision channel — and a `$(...)` call site
would itself cost a fork. Kept bash 3.2 compatible (no ${v,,}) for macOS
system bash.
Windows detection is lexical where it can be. The drive-letter and UNC
shapes are recognised by shape, not by OS, so the rewrite behaves identically
everywhere and the regression tests are meaningful on POSIX CI. Separator
rewriting for a path with NO Windows shape is gated on OSTYPE, because `\` is
a legal POSIX filename character: rewriting it there would turn one
in-boundary file named 'b\..\..\etc\x' into the out-of-boundary path /etc/x
and flip a decision the hook is supposed to make on the literal name.
/careful shares hook-extract.sh but has no equivalent defect: the shared
extractor is field-agnostic, and careful reads `command` and never resolves a
path. check-freeze.sh is the only consumer of file_path in the repo.
Tests: 7 new cases in test/hook-scripts.test.ts (the reported repro, a
native-form boundary, mixed separators and drive case, src vs src-old, UNC
host isolation, and a POSIX-only backslash-safety case). Four of them fail
against the pre-fix hook; the deny-side ones guard against over-permissive
normalisation. Full file: 108 pass, 1 skip, 0 fail.
Worth flagging for CI: test/hook-scripts.test.ts is excluded from the curated
`test:windows` lane by the "raw /tmp/ path" filter in
scripts/test-free-shards.ts, which is why this reached users. The new tests
are written to catch it on POSIX CI for that reason.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Merging to
After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here |
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 bug
On Windows, Claude Code hands
Edit/Writea drive-letter absolute path.freeze/bin/check-freeze.shtests absoluteness with:C:\dev\proj\src\x.pydoes not match/*, so the hook treats it as relative and joins it ontopwd. Nothing normalises\before the prefix comparison either, so the boundary check runs against a mangled path:Because freeze is fail-closed by design, there is no softer failure mode than a hard deny — on every edit, including files inside the frozen directory.
/guardand/investigateregister the same hook, so all three skills are unusable on Windows; the session that hit this had to fall back to editing through Bash to make any progress.The fix
Add
gstack_hook_normalize_pathto the sharedcareful/bin/hook-extract.sh, and run both sides of the boundary comparison through it:Normalising one side was never enough: the state file may legitimately hold either dialect, since
/freezewrites it withpwd, which answers/c/dev/...under Git Bash.Two constraints shaped the implementation:
Builtins only, no subshell.
carefulsources this file on every Bash tool call, and process creation under Windows bash measured ~0.7-1.5s on the reporting machine. The function returns its result inGSTACK_HOOK_PATHrather than on stdout — which is the hook's decision channel — and a$(...)call site would itself cost a fork. Kept bash 3.2 compatible (no${v,,}) for macOS system bash.Windows detection is lexical where it can be. The drive-letter and UNC shapes are recognised by shape, not by OS, so the rewrite behaves identically everywhere and the regression tests are meaningful on POSIX CI. Separator rewriting for a path with no Windows shape is gated on
OSTYPE, because\is a legal POSIX filename character: rewriting it there would turn an in-boundary file namedb\..\..\etc\xinto the out-of-boundary path/etc/xand flip a decision the hook is supposed to make on the literal name./carefulshareshook-extract.shbut has no equivalent defect: the shared extractor is field-agnostic, and careful readscommandand never resolves a path.check-freeze.shis the only consumer offile_pathin the repo.Verification, and an honest limit on it
The 7 new cases in
test/hook-scripts.test.tscover the reported repro, a native-form boundary, mixed separators and drive case,srcvssrc-old, UNC host isolation, and a POSIX-only backslash-safety case. Four fail against the pre-fix hook; the deny-side ones guard against over-permissive normalisation.I could not get a meaningful signal from that file on Windows, and I want to be straight about it rather than quote a green number. Measured on this branch's merge base (v1.87.0.0), unmodified:
check-freeze.shcases specifically: 0 of 15 pass on unmodified upstream, 0 of 19 with this change. The whole file is non-functional on Windows — which is expected and already known to this repo:scripts/test-free-shards.ts:124excludes files containing a raw/tmp/path from the Windows-safe subset, commented "they would fail onwindows-latestno matter how the runner shards them." That exclusion is also why this defect reached users, and it is why the new tests are written to catch it on POSIX CI. POSIX CI is the right judge of these tests; my local Windows run is not.So I verified the behaviour directly instead, driving the real hook with real payloads, both directions, against the installed v1.87.0.0 + this change:
C:\dev\proj\validation_ui\ref_picker.py{})C:\dev\proj\kg\loader.pyc:/dev/proj/validation_ui/x.pyvalidation_ui-old/x.pyThe last one matters: it shows the normalisation is not simply permissive.
One note for whoever reviews: building the test payload by hand is easy to get wrong, because a single backslash inside the JSON string is an invalid escape and the hook then fails closed with a parse error that looks exactly like a boundary denial. Generate the payload with a JSON serialiser.
Composition with v1.87.0.0
This is cherry-picked onto current
main, not the v1.68.2.0 it was written against. It composes with the newergstack_hook_state_rootwork in the same file: normalisation runs atcheck-freeze.sh:125, ahead of the absoluteness test at :129, and the state-root resolver is untouched.