#6474·gsd-2

Worktree write-gate HARD BLOCKs legitimate edits when process.cwd() is the worktree but dash.basePath is the project root

Author: AReid987Created Jun 20, 2026Updated Jun 22, 2026

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):

javascript
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:

  1. effectiveBasePath is the canonical project root (from dash.basePath)
  2. isAutoLive is false (not in auto-mode loop)
  3. The bypass does not fire
  4. absTarget resolves to <projectRoot>/<relativePath> — not inside .gsd/worktrees/
  5. 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:

javascript
if (isAutoLive && isGsdWorktreePath(effectiveBasePath))
    return { block: false };

with:

javascript
// 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:

javascript
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=1 and editing via Python
  • Same tasks succeeded only after disabling the guard or using absolute worktree paths

Auto-generated by /gsd forensics