freeze: compare Git Bash paths by cygpath's drive spelling (#3623)

Git Bash mounts %TEMP% at /tmp and converts HOME through its mounts, so
'~/x' and 'D:\...\x' named one file in two spellings and the hook denied
a legit edit (windows-free-shard 2 on #3758).

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
garrytanandcapy-ai[bot] committed 2026-10-31 21:54:22 +00:00
1 parent 2034fbb61d
commit 4a28416dbc
2 files changed
+46

No files matched your search

+8
View File
@@ -318,6 +318,14 @@ _resolve_path() {
else
_p="$_lead$_tail"
fi
# Git Bash spells one folder several ways through its mounts (/tmp is
# %TEMP%, and HOME converts through them); cygpath -m gives the one drive
# spelling Claude Code reports.
if [ -n "$_p" ] && [ "$GSTACK_HOOK_IS_WINDOWS" = 1 ] && command -v cygpath >/dev/null 2>&1; then
_p=$(cygpath -m "$_p" && printf x) || return 1
_p="${_p%"$_FREEZE_NL"x}"
case "$_p" in ''|*"$_FREEZE_NL"*|*"$_FREEZE_CR"*) return 1 ;; esac
fi
fi
[ -n "$_p" ] || _p="/"
gstack_hook_normalize_path "$_p"
+38
View File
@@ -1643,6 +1643,44 @@ describe('check-freeze.sh', () => {
});
});
// Git Bash reaches one folder through several mounts (/tmp is %TEMP%, and
// HOME converts through them), so '~/x' and 'D:\\...\\x' can name one file
// in two spellings; the hook compares cygpath's drive spelling.
test.skipIf(process.platform === 'win32')('Windows bash: a folder reached through a Git Bash mount compares by its drive spelling', () => {
withTree(({ base, home, src, outside }) => {
const bin = path.join(base, 'bin');
fs.mkdirSync(bin);
const mnt = path.join(base, 'mnt');
fs.writeFileSync(path.join(bin, 'cygpath'), [
'#!/bin/bash',
'case "$2" in',
' "$FAKE_MNT"/in/*) printf "%s\\n" "$FAKE_IN${2#"$FAKE_MNT"/in}" ;;',
' "$FAKE_MNT"/out/*) printf "%s\\n" "$FAKE_OUT${2#"$FAKE_MNT"/out}" ;;',
' *) printf "%s\\n" "$2" ;;',
'esac',
'',
].join('\n'), { mode: 0o755 });
const decideWithCygpath = (filePath: string): string => {
let decision = '';
withFreezeDir(src + '/', (stateDir) => {
const r = spawnSync('bash', ['-c', 'OSTYPE=msys; . "$0"', FREEZE_SCRIPT], {
input: JSON.stringify(freezeInput(filePath)),
env: { ...process.env, ...freezeEnv(stateDir, { HOME: home }), PATH: bin + path.delimiter + (process.env.PATH ?? ''), FAKE_MNT: mnt, FAKE_IN: home, FAKE_OUT: outside },
cwd: base,
timeout: 5000,
});
expect(r.status).toBe(0);
const raw = r.stdout.toString().trim();
decision = raw === '{}' ? 'allow' : (JSON.parse(raw).hookSpecificOutput?.permissionDecision ?? raw);
});
return decision;
};
expect(decideWithCygpath(mnt + '/in/proj/src/x.ts')).toBe('allow');
expect(decideWithCygpath(mnt + '/out/proj/src/x.ts')).toBe('deny');
expect(decideWithCygpath(src + '/x.ts')).toBe('allow');
});
});
// Linux and macOS read a leading '//' as '/', so neither a '//' path nor a
// link to '//' may borrow a UNC boundary, and a link to '/' followed by
// '/' stays a plain absolute path.