#3410 · found by /ork:verify · 2026-08-10

Two layouts. The scanners knew one.

A security suite that gates git push reported 49 failures locally and zero in CI. Every failure was this repo's own source, found a second time inside a live worktree. Fixing it produced a worse bug for a few minutes: a scan of zero files that printed "3 passed".

The original defect

test-jq-injection.sh          -not -path "$REPO_ROOT/.worktrees/*"     excluded
                              .claude/worktrees/                      NOT excluded

test-json-island-breakout.sh  dirs if d not in {".git","node_modules",".worktrees"}
                              ← prunes by NAME; .claude/worktrees' leaf is
                                "worktrees", so it is walked into

.claude/worktrees/ is CC's native layout and the default since #3315 retired ork's own provisioning. CI's clean clone has no worktrees, so it stayed green while local dev went red — and a security gate that teaches its operator to ignore it is worse than one that is absent.

Two bugs I introduced fixing it

1. paths[0] is not "this checkout"

git worktree list puts the main worktree first, which is a different tree whenever the test runs from a worktree. Skipping paths[1:] made the scanned tree its own prune root.

2. The mandated layout NESTS worktrees inside the repo

<repo>/.worktrees/<task> means the main checkout is an ancestor of this one. Prefix-matching against it pruned everything:

repo_targets | wc -l   →  0
suite output           →  "3 passed, 0 failed"

A scan of nothing finds nothing and reports clean. Only test-json-island-breakout.sh caught it, because it already asserts its own scan is non-vacuous — "scanner matched NO JSON islands — the scan below is vacuous, not clean". That one assertion is the entire reason this did not merge green.

Fix

exclusions DERIVED from `git worktree list`, not literals
  skip the tree being scanned
  skip any checkout that CONTAINS it        ← the ancestor rule
  keep both literals as a backstop for UNregistered debris git will not list

+ vacuity guard in test-jq-injection.sh: fail if the target list
  collapses below 100 scripts

Controls, both directions

remove the ancestor check:
   target list collapsed to 0 shell scripts — the scan below would be
    vacuous, not clean (check worktree_prunes)          3 passed, 1 failed
restore:
   target list is live (359 shell scripts in scope)     4 passed, 0 failed

Verified in BOTH environments — the point of the bug

wherejq-injectionisland-breakout
primary checkout4/4 · 359 scripts5/5 · 30 islands
git worktree4/4 · 359 scripts5/5 · 24 islands

Passing in one environment is what created this bug in the first place, so a single green run would not have been evidence.

Third instance of one root cause today. #3391 was context-gate switching limit regimes between a checkout and a worktree. The branch guard needs an explicit path because it cannot see the shell's cwd (#3411). Now two security scanners walking into the wrong layout. The repo has two worktree layouts and its tooling keeps knowing about one — which is why the exclusion here is derived rather than a second hardcoded literal. Adding .claude/worktrees beside .worktrees would have fixed today and set up a fourth instance.