From 5130b04d5aedbe07ea769a10deb71ddf8a7bd318 Mon Sep 17 00:00:00 2001 From: Jeremy McSpadden Date: Tue, 24 Mar 2026 23:55:25 -0500 Subject: [PATCH] fix(write-intercept): close bare-relative-path bypass in STATE.md regex The previous regex `/[/\\]\.gsd[/\\]STATE\.md$/` required a path separator *before* `.gsd`, so a bare relative path like `.gsd/STATE.md` (no leading directory component) was not blocked. If the file doesn't exist yet, `realpathSync` throws and the bare path slipped through undetected. Fix: change both patterns to `(^|[/\\])` so paths starting with `.gsd/` are caught regardless of whether a separator precedes them. Caught during e2e team verification (write-intercept-e2e agent). Updated test to assert the bare path is now blocked. --- .../extensions/gsd/tests/write-intercept.test.ts | 9 ++++----- src/resources/extensions/gsd/write-intercept.ts | 8 +++++--- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/resources/extensions/gsd/tests/write-intercept.test.ts b/src/resources/extensions/gsd/tests/write-intercept.test.ts index 940295376..3e2147552 100644 --- a/src/resources/extensions/gsd/tests/write-intercept.test.ts +++ b/src/resources/extensions/gsd/tests/write-intercept.test.ts @@ -12,14 +12,13 @@ test('write-intercept: blocks unix .gsd/STATE.md path', () => { }); test('write-intercept: blocks relative path with dir prefix before .gsd/STATE.md', () => { - // The regex requires a path separator before .gsd — bare '.gsd/STATE.md' is not blocked - // but 'project/.gsd/STATE.md' is (has separator before .gsd) assert.strictEqual(isBlockedStateFile('project/.gsd/STATE.md'), true); }); -test('write-intercept: does NOT block bare .gsd/STATE.md without leading separator', () => { - // Regex requires [/\\] before .gsd — bare relative path has no such separator - assert.strictEqual(isBlockedStateFile('.gsd/STATE.md'), false); +test('write-intercept: blocks bare relative .gsd/STATE.md (no leading separator)', () => { + // (^|[/\\]) matches paths that start with .gsd/ — covers the case where write + // tools receive a bare relative path before the file exists (realpathSync fails). + assert.strictEqual(isBlockedStateFile('.gsd/STATE.md'), true); }); test('write-intercept: blocks nested project .gsd/STATE.md path', () => { diff --git a/src/resources/extensions/gsd/write-intercept.ts b/src/resources/extensions/gsd/write-intercept.ts index 63b648f2b..7eab9fbae 100644 --- a/src/resources/extensions/gsd/write-intercept.ts +++ b/src/resources/extensions/gsd/write-intercept.ts @@ -16,10 +16,12 @@ import { realpathSync } from "node:fs"; * - SUMMARY.md, KNOWLEDGE.md, CONTEXT.md — non-authoritative content */ const BLOCKED_PATTERNS: RegExp[] = [ - // STATE.md is the only purely engine-rendered file - /[/\\]\.gsd[/\\]STATE\.md$/, + // STATE.md is the only purely engine-rendered file. + // (^|[/\\]) matches both absolute paths (/project/.gsd/…) and bare relative + // paths (.gsd/STATE.md) so a path without a leading separator is also blocked. + /(^|[/\\])\.gsd[/\\]STATE\.md$/, // Also match resolved symlink paths under ~/.gsd/projects/ (Pitfall #6) - /[/\\]\.gsd[/\\]projects[/\\][^/\\]+[/\\]STATE\.md$/, + /(^|[/\\])\.gsd[/\\]projects[/\\][^/\\]+[/\\]STATE\.md$/, ]; /**