Skip to content

guardrails: let worktree_escape declare sanctioned write paths - #2

Merged
choiyounggi merged 2 commits into
mainfrom
fix/worktree-escape-allowpaths
Aug 5, 2026
Merged

guardrails: let worktree_escape declare sanctioned write paths#2
choiyounggi merged 2 commits into
mainfrom
fix/worktree-escape-allowpaths

Conversation

@choiyounggi

Copy link
Copy Markdown
Owner

What went wrong

Running dev-loop's orchestrate skill produced two worktree_escape escalations in a single wave, both for a worker writing its status and plan files into <main>/.orchestration/. Each time, watch-status.sh aborted the whole watch (exit 5) and the coordinator had to approve, clear the record, and restart — while the workers were fine.

Neither plugin was misbehaving:

  • an orchestrator keeps shared coordination state inside the main checkout so one coordinator can watch one directory;
  • worktree_escape exists to stop a linked worktree from writing into that same checkout.

Together they made every coordination write read as corruption.

The fix

rules.worktree_escape.allowPaths — main-root-relative paths whose mention does not count toward the rule:

{ "rules": { "worktree_escape": { "mode": "ask", "allowPaths": [".orchestration"] } } }

A command whose only main-root reference is a declared path no longer fires. One that also touches the checkout still does — including when a single command does both (cp <main>/.orchestration/x <main>/src/y still asks). Absolute entries and anything containing .. are ignored, so the list cannot widen the rule past the main root. Same repo-over-global precedence as mode.

Generic on purpose. The guard ships no knowledge of any tool's directory layout — .orchestration appears only as a README example. "No org-specific rules" is this plugin's stated contract, so the caller that owns a channel is the one that declares it. Turning the rule off in worker sandboxes was the alternative and it loses the protection entirely.

A bash 3.2 trap found while testing

rest=${rest//"$main_root/$ap"/}     # WRONG

Bash 3.2 — which this script explicitly targets, and which macOS ships — takes the quoted / as the pattern/replacement delimiter, so this silently becomes "replace $main_root with $ap/". Instrumenting showed rest mutating from echo pwned > /path/repo/f into echo pwned > .orchestration//f, which is why the rule stopped firing for everything once allowPaths was set. The pattern is now built in its own variable, with a comment so it does not get re-inlined.

The tests caught this as "the rule no longer fires at all" rather than as a subtle mismatch — worth noting for anyone reviewing similar substitutions elsewhere in the file.

Verification

  • bats plugins/guardrails/tests plugins/memory-loop/tests77/77, was 73
  • plugins/guardrails/scripts/self-test.sh — 10/10 against the live guard, nothing executed
  • New tests: allowed write passes · checkout write still asks · one command touching both still asks · traversing and absolute entries ignored while a valid one still works · unconfigured behavior byte-for-byte unchanged

The dev-loop half — worker-guardrails.sh emitting allowPaths into each worker sandbox — is queued as a separate change in that repo; this PR is inert until something declares a path.

🤖 Generated with Claude Code

Running dev-loop's orchestrate skill produced two `worktree_escape`
escalations in one wave, both for a worker writing its status/plan files into
`<main>/.orchestration/`. Each plugin was behaving correctly in isolation: an
orchestrator keeps its shared coordination state inside the main checkout so
one coordinator can watch one directory, and this rule stops a linked worktree
from writing into that same checkout. Together they made every coordination
write look like corruption, and the coordinator had to approve, clear and
restart the watch each time.

Adds `rules.worktree_escape.allowPaths` — main-root-relative paths whose
mention does not count toward the rule. A command whose ONLY main-root
reference is a declared path no longer fires; one that also touches the
checkout still does, including when a single command does both. Absolute
entries and any containing `..` are ignored, so the list cannot widen the rule
past the main root. Same repo-over-global precedence as `mode`.

Generic on purpose. The guard ships no knowledge of any tool's directory
layout — `.orchestration` appears only as a README example — because
"no org-specific rules" is this plugin's stated contract; the caller that owns
a channel declares it.

One bash-3.2 trap found while testing: `${v//"$a/$b"/}` takes the quoted `/`
as the pattern/replacement delimiter, silently becoming "replace $a with $b/".
The tests caught it as the rule failing to fire at all. The pattern is now
built in its own variable, with a comment so it is not re-inlined.

Tests 73 -> 77: allowed write passes, checkout write still asks (including a
command touching both), traversing/absolute entries ignored, and unconfigured
behavior unchanged. self-test still 10/10.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
worktree_escape gains allowPaths. The marketplace distributes updates by
version, so the feature only reaches installed users once this moves.
@choiyounggi
choiyounggi merged commit f5f4fd0 into main Aug 5, 2026
4 checks passed
@choiyounggi
choiyounggi deleted the fix/worktree-escape-allowpaths branch August 5, 2026 09:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant