fix(gate): recognise the interpreter flag by RULE and read rule 3d's victim quote-aware (BACKLOG #1097, #1064) - #343
fix(gate): recognise the interpreter flag by RULE and read rule 3d's victim quote-aware (BACKLOG #1097, #1064)#343wshallwshall wants to merge 10 commits into
Conversation
…injection vocabulary (#1106) BACKLOG #1106's own defect, found one setting over, by the enumeration the item asked for. RE-CONFIRMED FIRST, AND THE ITEM NO LONGER HOLDS AT THIS BASE. `sign_private_key` and `sign_private_key_password` both redact on both serializers; e756608 (PR #300) closed that on 2026-08-09. Reverting that arm still turns four tests red, so it is pinned rather than incidental. The ledger banner is stale and is Session A's to correct. WHAT THE ENUMERATION FOUND. Driving every settings key the package writes into or reads out of `spec.settings` through the REAL `_is_secret_setting` -- 189 keys from three independent sources (runtime emission over all 23 spec-returning factories, AST writes, AST reads) -- leaves 54 credential-shaped keys: 20 matched, 34 not. Thirty-three of the 34 are paths, identifiers, keyword names, mode selectors or public certificates, each already documented as such. One is not: http_auth_user -> 'SYNTHETIC-...-DIGEST' verbatim, BOTH serializers http_auth_password -> '***' proxy_user -> '***' the SAME object, the SAME class proxy_password -> '***' and the `env()` FALLBACK DEFAULT rode along with it, while the identical `env()` on `proxy_user` emitted a bare `{'env': key}`. `with_http_digest` renames parameter `user` into setting `http_auth_user` -- the same parameter-to-setting boundary `with_signing` crosses, which is the whole of #1106. It is the sixth member of a five-member class: `username`, `basic_user`, `proxy_user`, `ws_username` and `credential_username` are all classified defence-in-depth on the stated ground that a username names a principal and can leak directory structure. WHY BOTH GUARDS READ GREEN OVER IT, WHICH IS THE PART THAT MATTERS. Neither had ever probed a username parameter. `CREDENTIAL_ISH` -- the tuple deciding WHERE a sentinel is injected -- was ("password","secret","token","key","credential","passphrase"), and four of the five classified usernames match none of it; `credential_username` matches only by accident, through "credential". Widening it by the single word `user` turned the outcome guard red on exactly `with_http_digest -> http_auth_user` and on nothing else, and brought 16 previously-unprobed username parameters across 11 factories into both guards. That is the fifth instance of one class -- a control whose domain is narrower than its surface -- and the first to sit in the VOCABULARY rather than in a set of things. So the vocabulary is no longer asserted by review: `test_the_injection_vocabulary_covers_every_key_the_engine_itself_classifies` derives it from the engine's own `_SECRET_SETTING_KEYS` and fails if the engine ever classifies a key whose shape the injector cannot see. Measured, each mutation restored byte-identical afterwards: dropping `user` again names all six usernames; un-classifying `http_auth_user` reddens three tests; reverting the #1106 arm reddens four. Baseline at 48f8712 was 11745 passed / 855 skipped / 0 failed (reproduced twice); this branch is 11763 / 855 / 0. Collected node ids 12588 -> 12606 -- the two new tests plus the 16 new username parameter cases, with no removals attributable here. `http_auth_user` joins `_NON_ROTATABLE_SECRET_SETTING_KEYS` beside the other five usernames: you rotate the principal's password, not the principal, so it needs no rotation row. Every probe value in the tests is an obviously-synthetic literal built in the test itself.
…pellings (#1097)
The worktree gate does not blank the quoted argument of an interpreter flag, it recurses
into it, because that span is code that runs. WHICH flags counted was a fixed list of
literals -- `-c|-lc|-ec|-Command|-EncodedCommand` plus `/[ckCK]` -- matched
case-SENSITIVELY. Every spelling the list did not carry was a route around the whole gate:
the argument reverted to an ordinary quoted span, was blanked, and its contents became
invisible to rules 3, 3b, 3c and 3d alike.
Established by DRIVING THE REAL BINARIES (pwsh 7.6.4, Windows PowerShell 5.1, Git Bash
5.2.37, cmd.exe on Windows 10.0.26200), never from documentation:
* PowerShell binds a parameter by any unambiguous PREFIX of its name, so every spelling
from -C to -Command runs, on BOTH hosts.
* `-Cm`, `-Cmd`, `-Cnd`, `-Comd` do NOT run. That negative BOUNDS the family -- prefixes
of the parameter name, never any letter cluster -- and is pinned by a test that must
stay ALLOW, so a fix cannot widen into `-C[a-z]*`.
* Matching is case-INsensitive; the old patterns were case-SENSITIVE, so plain lowercase
`-command` was a bypass sitting beside the one spelling that was covered.
* Both hosts accept the `/` sigil for the same parameter (`/c`, `/Com`, `/COMMAND`); the
old `/[ckCK]` reached `/c` alone and only DOUBLE-quoted, so the verdict on `/c '...'`
turned on the quote character rather than on what ran.
* A POSIX shell's command flag is an open-ended CLUSTER: `-c`, `-lc`, `-ec`, `-xc`,
`-euc`, `-euxc`, `-ic` all run. The list held three.
* cmd.exe accepts its switches CONCATENATED: `/Q/C`, `/q/c`, `/s/c`, `/V:ON/C`.
A LONGER LIST WOULD HAVE THE SAME SHAPE AS THE DEFECT. What is committed is the generating
rule for the family -- a sigil then a prefix of the parameter name in any case, a shell
cluster ending in the command letter, a cmd switch run ending in /c or /k -- with the
prefix alternation BUILT rather than typed. Section 11's "prefer 'at least' to an
enumeration", applied to a matcher.
Measured on a synthetic governed primary, before -> after, feeding the hook real PreToolUse
payloads (nothing executed): 42 of 52 spellings of `git reset --hard` flipped ALLOW -> DENY;
the other 10 already denied. The four host-refused spellings and `bash -cl` stay ALLOW.
RED-FIRST IN BOTH DIRECTIONS. The new tests are 52 failed / 24 passed against the committed
gate and 76 passed after the fix. Three separate mutants then re-reddened the direction each
one breaks: reverting the matcher to the old literal list reproduces the pre-fix count
exactly; dropping `(?i)` reddens the case family; widening to `[-/]c[a-z]*` reddens exactly
the four bound cases, which is the false-deny direction no bypass test can surface.
THE TWO DEFECTS COMPOSE. `cd ../../.. && git reset --hard` from a nested worktree names the
primary, and teaching the resolver to see that was the whole of #1061 -- wrapping it in a
flag the matcher did not carry put it straight back out of reach. Measured from a nested
worktree: `pwsh -Com`, `pwsh -command`, `pwsh /Com` and `bash -xc` all ALLOWed while the
same command spelled `pwsh -Command` denied. Pinned, with the full spelling as the control.
FALSE-DENY COST, measured rather than assumed over 18 legitimate commands: one flips --
`grep -vc "git checkout main"`, a search whose PATTERN spells a git command. That class
already existed for `-c` (`grep -c "git checkout main"` has always denied), so this adds
members rather than creating it. `git -C "<path>"`, `tar -C "<dir>"`, `ssh box "..."`,
`sed -e "..."`, `pytest -k "..."`, a commit subject line and a worktree's own rebase are
all unchanged.
NOT CLAIMED, and recorded beside the rule: `-EncodedCommand` stays a literal and is not
prefix-expanded (its argument is base64, so recursion reaches nothing readable, and
expanding it would sweep in `sed -e`); a cluster with letters AFTER the command letter
(`bash -cl`) is uncovered, because the only rule that catches it also matches `-Comd` and
would delete the bound; `-File <script>` is not in the command at all; nesting stays one
level; and a quoted argument SPANNING LINES is matched by neither the old list nor the new
rule -- both multi-line forms deny today via the raw-line scan, an accident #1086 would
remove, and no test on this gate can tell the two mechanisms apart, so it was left alone
rather than fixed against a green nobody could watch fail.
$GateVersion bumped. The INSTALLED copy at ~/.claude/hooks/ is now stale against this
source by design: reinstalling from a checkout is the owner's call, and
tests/test_gate_installed_parity.py is expected to report the difference until they make it.
…beside it (#1097)
The previous commit replaced a hand-typed list of flag spellings with a generating rule and was
right to. It stopped one axis short: the rule it committed reads
$psFlag = "[-/](?:$psPrefixes|encodedcommand)"
and `[-/]` is a HAND-TYPED TWO-MEMBER CLASS inside a fix whose whole purpose was to stop hand-typing
enumerations. The prefix was generated; the sigil beside it was not.
Rather than fix only the named axis, this audits the others by DRIVING THE REAL BINARIES (pwsh 7.6.4,
Windows PowerShell 5.1, Git Bash 5.2.37, cmd.exe on Windows 10.0.26200). Three axes were open:
* SIGIL. `pwsh --command`, `--Com` and `--c` all run -- the double dash is the ORDINARY POSIX
spelling on the platform this repo targets. So do three UNICODE dashes PowerShell's argument
parser accepts as dash-equivalents: U+2013, U+2014, U+2015, singly on both hosts and DOUBLED on
pwsh 7. Five characters and two lengths, not two characters and one.
* A SECOND COMMAND-TAKING PARAMETER. `pwsh -h` lists `-CommandWithArgs | -cwa` beside
`-Command | -c`. `-cwa`, `-CWA` and the full name each run arbitrary code, and none is reachable
from prefixes of `command`. A generated prefix family cannot generate a parameter it was never
told about, so the gate now carries the command-taking parameter SET and a binding rule per
member -- prefix-binding for `command`, exact for `commandwithargs`/`cwa`/`encodedcommand`.
* SEPARATOR. cmd.exe does not require whitespace before its argument: `cmd /c"..."` and
`cmd /cecho ran` both execute. PowerShell and a POSIX shell DO require it, measured through a
real shell so the argv is faithful (pwsh exits 64, bash exits 1, on every attached spelling).
So `\s+` is kept for those two branches and relaxed on cmd's alone.
THE SIGIL IS SPELLED WITH A BACKREFERENCE, not `{1,2}`, because the measurement is sharper than
`{1,2}`: a dash-family character doubles only WITH ITSELF, three or more is refused, a mixed pair is
refused, and a slash never doubles. `\u` escapes rather than literals keep the file pure ASCII --
U+2015 does not encode in cp1252 at all, and it raised UnicodeEncodeError on this box's console
while this change was being written.
RED-FIRST, AND IN BOTH DIRECTIONS. The new tests are 96 failed / 71 passed against the gate as of the
previous commit, and 243 passed after, alongside that commit's 76 unchanged. Eight mutants, each
restoring the gate byte-identically (sha256 verified): reverting the sigil to `[-/]` reddens 79 rows,
dropping the second parameter 20, re-imposing cmd's whitespace 6. In the FALSE-DENY direction --
which no bypass test can surface -- widening the sigil to `{1,2}` reddens 3, to `+` reddens 11, the
prefix to `c[a-z]*` reddens 48, the alias to a prefix ladder reddens 2, and relaxing the separator
across all three branches reddens 7. Each reddens a bounded-ness test and nothing else.
ONE BOUND WAS A NO-OP AND WAS REPLACED RATHER THAN KEPT. The separator bound was first written as
`pwsh -Comd"..."`, which the across-the-board relaxation left GREEN: the quote there does not sit
immediately after any prefix of `command`. The attached form `pwsh -Com"..."` does. It reads as a new
MUST-ALLOW set, so the test says why permitting it is safe: measured through a real shell, every one
of those spellings is REFUSED by its host, so there is no reachable behaviour behind them.
The bound that makes the prefix rule safe is untouched: `-Cm`, `-Cmd`, `-Cnd` and `-Comd` are refused
by pwsh under every one of the five sigils and both lengths (36 combinations), and stay ALLOW.
FALSE-DENY COST, measured over a corpus rather than assumed: a real long option beginning with `c`
(`grep --color`, `--count`, `docker --config`, `curl --cacert`, `cmake --config`, `sort --check`) is
refused by the mandatory whitespace-then-quote, and an en dash in ordinary prose is not adjacent to a
name. Hook latency is unchanged within noise on four payload shapes, including two built to force
backtracking.
NOT CLAIMED, and one residual is NEW. Recorded beside the rule: `powershell "<script>"` with NO FLAG
AT ALL runs on Windows PowerShell 5.1 (pwsh 7 does not), and no flag matcher closes it -- it needs a
matcher keyed on the interpreter's NAME, a different rule whose false-deny profile has to be measured
on its own, with `ssh box "git checkout main"` sitting next to it. Inherited and unchanged: `-File`,
a base64 `-EncodedCommand` payload, a cluster with letters after the command letter, nesting past one
level, and a quoted argument spanning lines.
$GateVersion bumped. The INSTALLED copy at ~/.claude/hooks/ is stale against this source BY DESIGN:
reinstalling from a checkout is the owner's call, and tests/test_gate_installed_parity.py reports the
difference until they make it.
… is INVERTED on the Bash path
An eight-agent adversarial adjudication of the #1097 rebuild returned NEEDS_WORK. Its premise is
sound and no verdict moved DENY to ALLOW anywhere, so the change is kept; these are its blockers.
Each was re-measured here rather than taken from the report.
THE SIGIL AXIS WAS MEASURED THROUGH THE WRONG INSTRUMENT, and the candidate shipped that error as a
green test. This hook scans BOTH tool names through one matcher, and a Bash tool call goes through
Git Bash, which applies MSYS argument conversion before the child sees the string. Measured with an
argv printer through Git Bash 5.2.37:
typed /c -> child gets C:/ does NOT run
typed //c -> child gets /c RUNS
typed /Command -> child gets C:/Program Files/Git/Command does NOT run
typed //Command -> child gets /Command RUNS
typed ///Command -> child gets //Command does NOT run
So on that path the gate DENIES the spelling that cannot run and ALLOWS `//Command`, `//c` and
`//Com`, which do. `test_the_sigil_run_is_bounded` had `//` as a MUST-ALLOW row, justified by "a
slash never doubles" -- true of a pwsh spawned from a PowerShell parent, false of the path this hook
governs. That row made the inversion a REQUIREMENT: a new permit in a security test is a
specification change wearing the clothes of coverage, and it would have forced a later fix to delete
a green test. Removed. `///` STAYS, measured on the same path not to run.
The BYPASS is inherited from the matcher being replaced and is unchanged here, so it is DISCLOSED in
the residual list rather than closed. Closing it means matching a leading `//`, which opens a
UNC-shaped false-deny surface (`ls //server/share/c "..."`) needing its own measurement -- a separate
change with a separate risk profile, not a line slipped into this one.
A FALSE MEASURED CLAIM, CORRECTED. The separator comment asserted the mandatory whitespace was "the
ONLY thing refusing -Cm/-Cmd/-Cnd/-Comd". Rebuilt the pattern with `\s*` on all three branches and
ran the four strings: all four still fail to match, captured group empty. They are refused because no
prefix of `command` ENDS where those flags end, so the alternation never reaches the separator. The
file's own next paragraph already gave the correct reason, so it contradicted itself four lines
later. Kept as a worked example rather than deleted: the false version named a real bound and a real
risk, and only tying it to an instrument showed it was attached to the wrong mechanism.
A GUARD THAT CANNOT FAIL IS NOT A GUARD. `ls /usr/src/c "some file.txt"` sat in the false-deny list
labelled "the shape the relaxed cmd separator has to survive". Its payload carries no git token, so
it is green under any width of $cmdExeFlag, including one widened to match everything -- the exact
defect the neighbouring docstring congratulates itself for removing. It is now the CONTROL for a real
test, beside a second control isolating the trigger to the `/c` ending.
THAT REPLACEMENT SURFACED A NEW FALSE DENY, pinned rather than hidden:
ls /usr/src/c "git checkout main" main ALLOW -> this build DENY
ls /usr/src/lib "git checkout main" ALLOW both (control)
Any POSIX path whose last component is `c` matches $cmdExeFlag, so the `\s*` relaxation recurses into
the quoted span after it. The new test asserts the DENY with a docstring stating it is a disclosed
cost and not an endorsement, and that flipping the assertion is the wrong repair. A false deny is a
cost, not a hole: it stops legitimate work rather than admitting illegitimate work, and the direction
that matters is unaffected. WHETHER cmd's attached-form support is worth that cost is left to the
owner, with the two narrowings that would remove it named in the docstring.
AND THE PRE-EXISTING SUITE WAS RUN, which no agent had done although Get-ScannableSegments feeds
rules 3, 3b, 3c and 3d and this change adds denies: tests/test_worktree_gate.py, 81 passed. All three
gate suites together: 327 passed.
$GateVersion bumped once, to 2026.08.12.1.
…luster prefix, not the separator b8bc8f5 went through the adversarial verification this lane requires for every gate change, and the verification found a defect in it OF THE CLASS IT EXISTS TO CORRECT. Four lenses; verdict STANDS_WITH_AMENDMENTS. No shipped DENY became an ALLOW -- three independent corpora agree (121 payloads across both builds, a 196-probe sweep of the whole literal flag family crossed with quote character, separator width and anchor position, and a 35-shape path sweep), and a PowerShell-AST comparison found b8bc8f5 changed exactly one non-comment token, the $GateVersion string. So the code was right and the PROSE was wrong, which is why this amends rather than reverts. THE BLOCKER: THE CAUSE WAS MISATTRIBUTED IN FOUR PLACES. b8bc8f5 said the pinned false deny came from the `\s*` separator relaxation -- in the commit subject's reasoning, the gate comment, the test's NAME, and its docstring. Re-derived here with a third instrument (rebuilding $sigil/$psFlag/$shFlag/ $cmdExeFlag/$flagThenSep verbatim in pwsh, independent of the two lenses' real-gate mutants): ls /usr/src/c "git checkout main" cluster prefix + \s* MATCH, captures the payload cluster prefix + \s+ MATCH, captures the payload <-- separator is irrelevant no cluster + \s* no match no cluster + \s+ no match The line carries a literal space before the quote, so `\s*` was never involved. The cause is the `(?:/[^/\s]+)*` CLUSTER PREFIX, added in 5a169ba so cmd's concatenated switch runs (`/Q/C`, `/V:ON/C`) are recognised; it lets an ordinary POSIX path walk in one component at a time: `/usr` `/src` `/c`. The separator was blamed because it was the newest thing nearby -- the same error as the separator claim corrected one commit earlier, made while correcting it. AND THE STATED REMEDY WAS A REGRESSION. The docstring offered "dropping the `\s*` relaxation" as one of two ways to remove the false deny. Measured: it does NOT remove it (the row matches under `\s+` too) and it turns `cmd /c"git checkout main"` from DENY to ALLOW, because the attached form is exactly what `\s*` exists to catch. An owner acting on that advice would have paid a real DENY-to-ALLOW and bought nothing. The non-remedy is now recorded as one, so nobody reaches for it again. THE UNC COST CLAIM WAS FALSE. The deferral of the `//` closure was justified by "a UNC-shaped false-deny surface (`ls //server/share/c "..."`)". Rebuilt with a `//?` sigil and measured: that example, `ls //fileserver/c$/logs "..."` and `ls //nas/backup/c "..."` match under NEITHER the shipped sigil NOR the widened one, because `(?:^|\s)` anchors before the first slash and $sigil feeds $psFlag only, so what follows must be a prefix of `command`. The real new surface is a bare `//c`/`//com`/ `//command`/`//cwa` token before a quoted span -- narrow, not UNC. The deferral now rests on the true reason: closing it is a BEHAVIOUR CHANGE to a security control and wants its own verification. A compensating control resting on a false premise is itself the defect. A TRIPWIRE REPLACES THE DELETED ROW. Removing the `//` must-ALLOW row was right, and it left ZERO coverage of that sigil in EITHER direction -- proven rather than argued: under a minimal closure mutant all three suites stay green, so the suite could see the bypass neither open nor closed, while the pre-commit file reds exactly one row under the same mutant. b8bc8f5 applied "a bound that cannot fail is not a bound" to the false DENY and withheld it from the false ALLOW, which is the direction that admits illegitimate work. test_the_double_slash_sigil_is_a_known_open_residual now asserts the current ALLOW with a failure message stating that redding it is the SUCCESS signal and the ALLOW must not be restored. That documents the residual instead of blessing it. THE `///` CLAIM OVERREACHED. It was measured on pwsh and stated about the SIGIL. Measured through Git Bash with inert payloads: `cmd //c` and `cmd ////c` RUN while `cmd /c` and `cmd ///c` do not -- cmd runs on the EVEN slash counts, because MSYS strips one and cmd.exe tolerates the rest, whereas pwsh runs only on exactly `//` (`pwsh ///Command` and `////Command` were both measured not to run, so the pwsh row itself is correct). Every executing cmd spelling is ALLOW on main AND here, verified by driving both gates. Qualified, and the cmd family added to the residual list as "at least these". THE TABLE IS A FACT ABOUT A CONFIGURATION. Under `MSYS_NO_PATHCONV=1` or `MSYS2_ARG_CONV_EXCL='*'` the child receives `/Command` verbatim and THAT runs while `//Command` does not -- the whole table inverts. Both verified unset on this box. Neither the removal of `//` nor the keeping of `///` changes under either configuration, so the conclusions hold and the SPELLINGS do not. Precondition stated. THE RESIDUAL NOTE NOW LIVES IN THE GATE, not only in a test. This script is what gets installed to %USERPROFILE%\.claude\hooks and it TRAVELS WITHOUT THE TESTS, so the pinned test's instruction to "delete this test and its note in the residual list" pointed at a section that did not exist. THE PINNED ASSERTION IS ATTRIBUTED. It read `denied is not None`, which would hold green if some future unrelated rule denied the same string -- the tripwire silently measuring nothing straight through a $cmdExeFlag narrowing. It now uses assert_denied and checks the reason names the gated verb. Control-row comment corrected: the added row varies the PATH, not the payload, which is what isolates the trigger to the `/c` ending. Verification, scoped to what was actually run: tests/test_worktree_gate.py, tests/test_worktree_gate_interpreter_flags.py and tests/test_worktree_gate_interpreter_sigils.py. 327 -> 331, the four new rows being the double-slash tripwire. ruff check and ruff format clean repo-wide; both changed files remain pure ASCII. Thirteen further gate suites exist that this branch has not run; one review lens ran all thirteen at the parent commit (1 failed, 261 passed), the single failure being test_gate_installed_parity, which reports the installed copy at ~/.claude/hooks is stale against this branch. That is BY DESIGN and is not "fixed" here: reinstalling from a checkout is the owner's call, and installing from an unreviewed checkout downgrades a machine-global control. $GateVersion is NOT bumped again: b8bc8f5 already moved it to 2026.08.12.1 and no executable line changes here.
… git will stand (BACKLOG #1064)
Two independent defects, and closing either alone leaves the other open. Both were measured against
the shipped gate before anything was written, and the rejected `g1064` attempt closed neither.
DEFECT A -- THE TOKENISER COULD NOT READ THE QUOTES IT STRIPPED.
foreach ($tok in @($after -split '\s+' | Where-Object { $_ })) {
if ($tok.StartsWith('-')) { continue }
$victimRaw = $tok.Trim('"', "'")
A quoted path containing a SPACE became two tokens; the first was taken, the quotes were trimmed,
`git -C` failed on the truncated path, and the `continue` fell through to ALLOW. Measured on a rig
whose primary is `<tmp>\Pri mary`: all four spellings -- double-quoted, single-quoted, unquoted, and
the `-C` form -- ALLOWed, while the identical rig with the one space removed DENIED all four. Quoting
the path did not help, which is the tell that the quotes were never parsed. It is latent on this
machine only because the primary checkout happens to have no space in its path, and a path with a
space is ordinary on Windows: "latent because of an accident of this machine's paths" is an
unexercised precondition, not a mitigation.
Replaced with a quote-aware scan. It relies on .NET permitting DUPLICATE named groups, so
`$m.Groups['q']` is whichever branch matched -- unusual enough that it was validated first against a
flag, both quote styles, an embedded space, an unterminated quote and empty input.
DEFECT B -- THE VICTIM WAS RESOLVED AGAINST THE WRONG DIRECTORY, AND THE OBVIOUS RIG CANNOT SEE IT.
`& git -C $victimRaw` resolves a relative token against THIS HOOK PROCESS's cwd. The rejected #1064
attempt changed that to the SESSION cwd, which is also wrong: git resolves it against the EFFECTIVE
working directory, and both a `-C` flag and a prefix `cd` change that.
THE TWO ANSWERS COINCIDE WHEN THE SESSION CWD SITS AT THE SAME DEPTH UNDER THE SAME PARENT as the
`-C` target -- which is the ordinary sibling-worktree shape in this repo. So a rig built from sibling
worktrees consists entirely of BLIND rows and reports a gate that resolves against the wrong
directory as working. My own first probe made exactly that mistake and briefly read the rejected
attempt as having fixed this. The new test therefore parametrises three session positions and keeps
the same-depth one LABELLED BLIND beside a deeper row and an other-parent row, because the blind row
is the one that looks like a fix.
THE FIX IS TO CALL THE RESOLVER THIS FILE ALREADY HAS, not to write a fourth. Rules 3 and 3c resolve
their target with Get-GitTargetCandidatesRaw -- a `-C` read case-sensitively, else exactly one
followable `cd`/`pushd` in the prefix, else the cwd -- and rule 3d simply never called it. The
attempt grew `Get-FullPathRaw $victimRaw $cwdRaw` beside it instead, which is the third resolution
rule in one file. This is the same move that commit's own message says it made for #1061 ("reusing
the same helper rather than growing a second one") and then did not make here.
The `--show-toplevel` lookup one block down had the identical defect and is corrected with it: it was
handed the raw token, so it returned nothing for every relative spelling, leaving $isSelf false and
both branches emitting the generic deny. That is the same shape the note above that line already
records for the $cwd/$cwdRaw case.
RED FIRST, WITH CONTROLS THAT HOLD. Before the fix: 10 failed, 6 passed. After: 16 passed. The 6 that
passed throughout are the four no-space CONTROL spellings and the two pinned indirection residuals --
so the space is the whole cause of defect A and nothing else varied. A control that does not hold
undermines everything measured beside it, and the rejected attempt broke its own.
AND THE FOLD, which this change OWNS because this change created the need for it. `$victimRaw` was
interpolated into the deny reason unfolded, while every other rule in this file routes such values
through Get-SafeForMessage. That was accidentally safe under the old tokeniser, which could not
produce a token containing whitespace. A quoted token can now carry spaces and TABS, and a tab is one
of the three characters that helper exists to neutralise. Three interpolations now fold. A newline
still cannot arrive here because segments are split per line, so this closes the reachable half
rather than a whole class.
RESIDUALS, DISCLOSED RATHER THAN IMPLIED CLOSED:
* COMPOSE vs PREFER. Get-GitTargetCandidatesRaw PREFERS `-C` and discards a `cd` prefix, where a
shell COMPOSES them and resolves a relative `-C` against the post-`cd` directory. So
`cd ../Unrelated && git -C . worktree remove <victim>` still resolves wrongly. That is BACKLOG
#1085; it belongs to the resolver, and fixing it here would be the fourth resolver this change
exists to avoid. It is fixed on the unlanded `w3-l1-gate-3c` (0dbd6ab), verified absent from
both main and the g1064 attempt.
* INDIRECTION IS NOT CLOSED AND CANNOT BE. `p=../Primary-wt; git worktree remove "$p"` ALLOWs, on
main and here alike. The token is `$p` and its value is a runtime fact; a hook that inspects a
tool ARGUMENT before anything runs cannot follow it. That is a limit of the shape, not an
oversight -- the .SYNOPSIS already says this is a guardrail and not a security boundary. It is
PINNED as a known-ALLOW residual rather than left silent, because silence reads as coverage.
$GateVersion bumped once, to 2026.08.12.2.
…it was closing (BACKLOG #1064)
The adversarial verification this lane requires for every gate change found a DENY-to-ALLOW in the
commit that exists to close DENY-to-ALLOWs. Verdict STANDS_WITH_AMENDMENTS, no blockers: the fix is a
real improvement -- three lenses watched real git destroy a governed worktree in shapes that ALLOW on
the parent and DENY at HEAD -- but two things in the scan it rewrote were wrong, and the prose around
them claimed more than was measured.
THE REGRESSION, AND IT IS MINE. The bare alternative was `(?<q>[^\s"'][^\s]*)`, whose TAIL admits
quote characters. The `.Trim('"', "'")` it replaced stripped them. So:
git worktree remove <governed-abs-path>"" main DENY parent DENY 78ed419 ALLOW
git worktree remove <governed-abs-path>'' main DENY parent DENY 78ed419 ALLOW
The trailing quotes rode into the token, `git -C` failed on the malformed path, and the rule fell
through to ALLOW -- the exact fail-open the quote-awareness was added to close, reintroduced by the
fix for it. Measured across THREE gate versions, which is what distinguishes a regression from an
inherited defect, and the whole 556-test gate suite was blind to it. The tail is now `[^\s"']*` and
four rows pin it.
THE QUOTED FLAG, WHICH IS INHERITED -- BUT THE NOTE ABOUT IT WAS NOT. The skip tested `$m.Value`, the
RAW match text. A quoted flag begins with a quote character, so it was never skipped:
git worktree remove "--force" "<governed wt>" ALLOW on main, parent AND 78ed419
`--force` became the victim path, and two lenses confirmed it by letting real git destroy the tree.
That is a pre-existing hole rather than a regression. What was NEW is 78ed419's own claim that the
alternation had been "validated against the flag" -- true of the unquoted spelling only, as its own
control row silently demonstrated. The skip now tests the CAPTURED value, and three rows pin both
quote styles.
THE RESIDUAL LIST WAS A COMPLETENESS CLAIM AND IT WAS SHORT. It named two items and asserted the new
base "equals the directory git will actually stand in". That sentence is false for every shape below,
and an adversarial re-read produced them in an hour. It now reads "AT LEAST THESE" (CLAUDE.md section
11, SDS-3.6) and the heading says ESTIMATE:
* A `-C` VALUE CONTAINING A SPACE -- measured at source: the resolver's `-C\s+"?([^"\s]+)"?`
captures `C:/Pri` from `-C "C:/Pri mary"`. That is the SAME space family this commit's subject
line claims to close, so the rule is quote-aware on one operand and not the other.
* Cumulative `-C`, which git documents as composing and the resolver reads once.
* A `-C` belonging to a LATER command on the line.
* A `cd` whose target leaf is literally `git`: the `$at` git-token regex matches inside that PATH
and truncates the prefix. A `~/git` or `C:\git` directory is ordinary, so this is the most
reachable of the set.
* A failed `cd`, and a subshell prefix.
* COMPOSE vs PREFER (#1085), carried over.
* THE PRE-SPLITTER `-split '(?:&&|\|\||;|\|)'` one line ABOVE the scan is not quote-aware, so a
quoted victim containing `;` or `&&` is truncated before the quote-awareness ever runs.
THE FOLD HAD NO TEST. A mutant setting `$victimMsg = $victimRaw` passed all sixteen new rows AND the
entire thirteen-file gate suite, so the fold added in 78ed419 was guarded by review alone -- the
defect that commit's own message accuses the code it replaced of. Pinned now via a tab-bearing quoted
victim that resolves back to the worktree through `..`.
AND THE ROWS NOW PIN THE CONSEQUENCE, NOT ONLY THE VERDICT. Every rule-3d row ran from a cwd outside
any repository, where `git worktree remove` exits "fatal: not a git repository" -- so the modelled
command was a no-op and the rows would have passed just as happily against a rule that could not
reach the victim at all. The session cwd is now a SECOND REGISTERED WORKTREE in both fixtures, so the
command each row models is one git would really execute against the victim.
Not taken, and recorded rather than silently dropped: the geometry-dependent class the false-deny
lens rated a blocker. With the hook process cwd moved away from the session cwd, the parent ALLOWs
those shapes too -- so main's deny was an accident of sibling-worktree layout, not a control that was
lost. Both lenses supplied that qualifier themselves and it is decisive.
$GateVersion bumped once, to 2026.08.12.3.
…e fixes BROKE, not only what they closed The gate-family lane authored every code commit here. I authored only these two banners, per the coordinator ledger-filing rule: allocation and the commit that files it cannot be split across worktrees, so ledger edits route to the coordinator. Neither item is an allocation -- both headings are already on origin/main, so ledger_check.py's `head - base` never consults ownership. Each banner names the defect the fix INTRODUCED alongside the one it closed, because a banner listing only what a change closed is half a record: #1097 -- ab995cd corrects b8bc8f5's OWN misattribution, and records that the docstring's proposed remedy would have turned cmd /c"..." from DENY to ALLOW. #1064 -- 34fc72d corrects a DENY-to-ALLOW regression that 78ed419 introduced, which a 556-test suite was blind to. Three author-corrected overstatements are named. #1229 is NOT closed by this branch: the double-quote-first blanking at :382-383 is byte-identical here, which I checked by content after a line-range comparison gave a misleading answer across a file that grew ~200 lines.
…m token" advice was false
PROSE ONLY. No executable line changes, no verdict moves. This corrects a sentence I wrote into the
gate two commits ago that was measured false, and it is the third claim of mine about this one matcher
that did not survive being driven.
WHAT I SAID: "The narrowing that works is to require a cmd-like PROGRAM token before the switch run."
The owner ruled on that basis. A design pass then drove it and the premise inverted.
CMD.EXE ITSELF ACCEPTS ARBITRARY `/junk` COMPONENTS IN A SWITCH RUN, and executes the quoted payload.
Driven against the real binary with a payload that COMPUTES its answer (`set /a 111*3` -> 333, so an
echo-back cannot be mistaken for a run), controls in the same batch (`cmd /c` must run, `cmdd /c` must
not, both held):
cmd /usr/src/c "<payload>" RUNS cmd /zzz/c "<payload>" RUNS (z is not a switch)
cmd /mnt/c "<payload>" RUNS cmd /usr/lib/k "<payload>" RUNS
cmd /a:/c "<payload>" RUNS cmd /d /usr/src/c "<p>" RUNS
`/usr` binds as `/U`, `/src` as `/S`, `/zzz` is ignored. So `(?:/[^/\s]+)*/[ck]` is very nearly
EXACTLY the family cmd accepts. It is not the sloppy over-match the earlier note called it, and
dropping it would lose real coverage rather than trim excess.
WHICH MAKES THE FALSE DENY A PROGRAM-IDENTITY PROBLEM, unsolvable at this layer. The only thing
separating `ls /usr/src/c "..."` from `cmd /usr/src/c "..."` is the program token -- and every
program-token spelling tried was defeated by something that EXECUTES:
echo hi;cmd /k "..." RUNS the `;` is not whitespace, and the outer `(?:^|\s)` anchor
(cmd /mnt/c "...") RUNS sits before the whole alternation, so a token inside the cmd
branch inherits that anchor and loses both
Set-Alias xc cmd; xc /usr/src/c RUNS no cmd token near the switch at all
a renamed copy of cmd.exe RUNS
cmd /d /Q/C "..." RUNS program not adjacent to the switch run
Five candidates were built and driven as REAL GATE MUTANTS, not reasoned about. Each traded this one
disclosed false deny for four to fifteen measured DENY-to-ALLOW regressions on shapes that run,
including the recorded trap `C:/Windows/System32/cmd.exe /c"..."` and `cm""d /c"..."`.
SO THE FALSE DENY IS KEPT AND DISCLOSED, and the owner ratified that after seeing the measurement. At
this rule's threat model the two directions are not symmetric: a false DENY stops legitimate work
loudly and has a workaround, while a false ALLOW lets a reset land in the shared primary silently.
"Not deployed" removes urgency here, never the bar.
The pinned tripwire `test_the_cmd_switch_cluster_prefix_costs_a_posix_path_false_deny` stays exactly
as it is -- green, and now green for a documented reason rather than a wrong one.
$GateVersion is NOT bumped: nothing executable changed, and the version exists to flag a behaviour
difference against the installed copy.
|
Coordinator note -- one commit ADDED to this PR after the body above was written.
This is the lane's own final commit ( Verified rather than assumed:
Provenance, stated because it is unusual: this commit was built with plumbing (
|
…OG #1096) (#347) ubuntu step_timeout 25 -> 31, job_timeout 37 -> 43. One executable line; the rest of the diff is this file's own comment block, kept true to the values beside it. BOTH CAPS MOVE OR NEITHER. Raising only step_timeout gives 5:22 + 30:59 + 4:59 = 41:20 against a 37:00 job cap -- NEGATIVE, so the JOB fires before the STEP and a job-level kill reports no step conclusion at all. That destroys the instrument this very item was measured with. The nesting invariant is why the job term is here, not tidiness. Applied this file's own rules rather than picking numbers: step = ceil_minute(1.35 x post-#1027 anchor) 22:50 -> 31:00 job = step + ceil_minute(1.5 x worst overhead) overhead UNCHANGED at 7:44, so the 12:00 room is unchanged and only the step term moves: 31+12 = 43 Every published figure recomputed from its inputs before committing, including a rounding fix: 31:00/20:28 is 1.515x, and I had first written 1.514 by truncating. WHAT TRIGGERED THE RE-DERIVATION, because the ordering is the point: main ALREADY exceeded the old 16:08 anchor at 17:07 BEFORE PR #343 existed. The record rotted on its own. #343 (+274 tests, +3:12) only carried the margin across the 1.30x floor. The floor did exactly what its docstring says it does. 20:28 is #343's own run, deliberately. Its 274 tests land on main when it merges, so that is the next baseline rather than an outlier -- sizing on the pre-merge population would re-break the cap on contact. #1096 STAYS OPEN. The caps are re-derived; the drift underneath them is not fixed, and raising a cap is not fixing it. Pool stated with the number: 58 runs since 2026-08-10, 63 rows, 35 passing, max 20:28, next 17:18, min 14:47.
Closes BACKLOG #1097 and BACKLOG #1064. All code authored by the gate-family lane; I authored only the two banners.
Why this is worth reading rather than skimming
Both fixes introduced a defect, and both were caught by an adversarial pass that the lane mandates and ran against itself. That is recorded in the banners rather than smoothed over, because it is the useful part.
ab995cd6correctsb8bc8f5c's own misattribution: it blamed the\s*separator when the cause is the cluster prefix added for/Q/C, and its docstring offered "drop the\s*" as a remedy that would not fix it and would turncmd /c"git checkout main"from DENY to ALLOW.34fc72d6corrects a DENY-to-ALLOW regression introduced by78ed4195itself. The quote-aware scan's bare alternative had a tail admitting quote characters where the.Trim()it replaced stripped them. Measured across three gate versions, which is what separates a regression from an inherited defect:git worktree remove <governed-abs-path>""reads DENY on main, DENY on the parent, ALLOW on78ed4195. A 556-test suite was blind to it.Verification
test_gate_installed_parity, by design -- it SKIPS on CI where no gate is installed (its own docstring says so). Do not "fix" itResiduals, carried rather than buried
No proof of absence of DENY-to-ALLOW over all inputs -- three finite corpora are not a proof. The
//closure was measured as a mutant, never as a finished change, so cheap is supported and safe is not. MSYS behaviour is established for one box only. The sigil residual list is explicitly "at least these". And one residual stings:-C "C:/Pri mary"capturesC:/Pri-- the same space family #1064's subject line claims to close.Scope notes
#1229 is NOT closed here. The double-quote-first blanking at
:382-383is byte-identical on this branch. I checked that by content after a line-range comparison gave a misleading answer across a file that grew ~200 lines.This branch still carries
b8fd1e82, whose change landed onmainvia #342's squash, so it is not an ancestor. Verified safe rather than assumed:merge-treeis rc=0, and the mergedwiring.pyblob is byte-identical to main's, so merging does not revert #1223.Severity per CLAUDE.md section 0: this is the local developer guardrail, which its own SYNOPSIS says is not a security boundary, and not a product exposure.