Worktree write-gate HARD BLOCKs legitimate edits when process.cwd() is the worktree but dash.basePath is the project root
Problem
When a one-shot execution (not auto-mode live) runs from inside a milestone worktree cwd, the worktree write-gate produces false-positive HARD BLOCKs on legitimate edits. The agent's process.cwd() is the worktree, but dash.basePath (set at discussion start) is the canonical project root. Each false-positive block counts as a tool-call failure, compounds into cost spikes (10x average), and triggers downstream schema-validation caps.
Root Cause
bootstrap/register-hooks.js:974 passes dash.basePath ?? discussionBasePath to shouldBlockWorktreeWrite. dash.basePath is set at discussion start and does NOT reflect the agent's actual process cwd.
bootstrap/write-gate.js:877 (pre-fix):
if (isAutoLive && isGsdWorktreePath(effectiveBasePath))
return { block: false };This bypass requires BOTH isAutoLive AND isGsdWorktreePath(effectiveBasePath). When a one-shot command runs inside a worktree cwd:
effectiveBasePathis the canonical project root (fromdash.basePath)isAutoLiveisfalse(not in auto-mode loop)- The bypass does not fire
absTargetresolves to<projectRoot>/<relativePath>— not inside.gsd/worktrees/- The gate correctly blocks based on its stale view, but the write is legitimate
PR #5982 (issue #5686, merged) fixed the case where effectiveBasePath is the worktree, but did NOT consider process.cwd().
Expected Behavior
A write/edit/multi_edit call should be allowed when the agent is operating inside any milestone worktree, regardless of whether isAutoLive is true. Path containment is the property that matters; liveness is orthogonal.
Concrete Fix Suggestion
In bootstrap/write-gate.js around line 877, replace:
if (isAutoLive && isGsdWorktreePath(effectiveBasePath))
return { block: false };with:
// Caller is operating inside a worktree path — either the supplied
// `effectiveBasePath` resolves to a milestone worktree, or the actual
// process cwd is a worktree cwd. Either signal means the write is
// happening inside the worktree context, so let it through. This does
// NOT require `isAutoLive` because path containment is the property we
// care about; liveness is orthogonal. (Fix for false-positive HARD BLOCK
// when a one-shot command runs inside the worktree cwd.)
if (isGsdWorktreePath(effectiveBasePath) || isGsdWorktreePath(process.cwd()))
return { block: false };Additionally, consider passing process.cwd() from register-hooks.js:974 for explicit verification rather than relying on the global:
const wtGuard = shouldBlockWorktreeWrite(
event.toolName,
event.input.path,
dash.basePath ?? discussionBasePath,
isAutoActive(),
dash.currentUnit?.type,
process.cwd(), // new: actual current cwd
);Environment
- GSD version: 1.2.0 (binary md5 bc3f0a18859981b0159e68fda8042125)
- Model: MiniMax-M3
- Units:
execute-task/M001-8obe9r/S02/T02,execute-task/M001-8obe9r/S02/T01
Reproduction Context
A milestone using git.isolation: worktree was executing S02 task units. The worker had cd-ed into .gsd/worktrees/M001-8obe9r/ but the discussion was started with the canonical project root as basePath. Relative writes like edit("iii-engine/package.json") were blocked with: HARD BLOCK: Worktree isolation is configured (git.isolation: worktree) but auto-mode is not running and the target "iii-engine/package.json" is not inside .gsd/worktrees/<MID>/.
Forensic Evidence
- 6 HARD BLOCK errors in
execute/task-M001-8obe9r-S02-T02(one per attempt) - Cost spike: $0.593 vs $0.060 average (10x)
- Cost spike: $0.268 in T01 vs $0.060 average
- Agent recovered by setting
GSD_DISABLE_WORKTREE_WRITE_GUARD=1and editing via Python - Same tasks succeeded only after disabling the guard or using absolute worktree paths
Auto-generated by /gsd forensics
Source: gsd-build/gsd-2