fix(router-gate-v4): Smoke 5 REAL fix — path-normalization separator bug
Smoke 5 restart-test (chistaa session) refuted stale-process hypothesis and
identified the real bug: Stream A's pathNormalize() returned OS-native paths
(backslashes on win32) while DEFAULT_PROTECTED_PATTERNS regexes are forward-slash
only.
Trace confirmation:
Stream A pathNormalize('~/foo/bar.jsonl') on win32:
BEFORE: 'c:\\users\\admin\\foo\\bar.jsonl' — backslashes
AFTER: 'c:/users/admin/foo/bar.jsonl' — forward slashes
isProtectedPath now matches → Bash/PowerShell hooks block correctly.
Root cause: path.resolve() + fs.realpathSync() on Windows produce backslashes,
caseFold lowercases them but doesn't change separators. DEFAULT_PROTECTED_PATTERNS
in shell-content-rules.mjs are forward-slash regexes (e.g. /(^|\/)\.claude\/projects/i).
defaultPathNormalize fallback in shell-content-rules.mjs DID normalize separators,
which is why my emergency commit 1072fae4 unit-tests passed but live behavior
failed — live hooks use resolvePathNormalize() which returns Stream A's
buggy implementation.
Fix:
- path-normalization.mjs: append .split('\\').join('/') to pathNormalize output.
- path-normalization.test.mjs: +1 RED→GREEN test for win32 separator normalization.
Why previous commit 1072fae4 was incomplete:
- Added pattern to protected list ✓
- Added enforce-read-path-deny.mjs ✓ (Read tool — works because hook uses
defaultPathNormalize directly, not resolvePathNormalize)
- Did NOT detect Bash/PowerShell path-normalize integration bug (debug script
bypassed Stream A by passing defaultPathNormalize directly).
Side observation (recorded as Stream H TODO by chistaa session):
- extractPathArgs/pathDenyOverlay — non-reading path in non-first position is
not checked fully. Independent latent bug, separate fix.
Regression: 1715/1715 vitest tools GREEN (+1 separator test).
Critical: re-run Smoke 5 in clean session — expected PASS all 6 vectors now.
This commit is contained in:
@@ -94,5 +94,8 @@ export function pathNormalize(target, opts = {}) {
|
||||
if (e && e.code && e.code !== 'ENOENT') throw e; // surface real FS errors; fail-close handled by caller
|
||||
real = resolved; // ENOENT — best-effort resolved path for unknown-state files
|
||||
}
|
||||
return caseFold(real, platform);
|
||||
// Smoke 5 integration fix (2026-05-30): normalize ALL separators to forward slashes
|
||||
// regardless of platform. DEFAULT_PROTECTED_PATTERNS regexes are forward-slash only.
|
||||
// Without this, win32 path.resolve + realpath returns backslashes and patterns miss.
|
||||
return caseFold(real, platform).split('\\').join('/');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user