Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions docs/BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6155,6 +6155,26 @@ the blanking here without measuring `#1086`'s false-deny rows in the same table.
**Source:** found 2026-08-06 by the must-keep-allowing inventory built for #1066, which enumerated what rule 3c allows today and found this in the gap between the shipped tests.

## 1069. Rule 3c matched the disarm key on the quote-blanked scan string, so a QUOTED key was invisible
> **THE QUOTED-KEY FAIL-OPEN IS FIXED 2026-08-27; the banner stays open for the archive pass, and the
> multi-word spelling below REMAINS OPEN BY DESIGN.** `Remove-QuotedSpans` now UNMASKS a quoted span
> holding a single BARE WORD -- no whitespace, quote, `$`, bracket, brace, semicolon, ampersand, pipe
> or backtick. Prose keeps its spaces and stays masked; a config key has none and becomes visible.
>
> **Measured before and after, with the UNQUOTED spelling as a known-answer control** (it denied in
> both arms, so an ALLOW below is a reading and not a dead probe): all five quoted spellings this row
> lists ALLOWED before and DENY after; three prose commit messages quoting `core.hooksPath` ALLOW in
> both arms, so the false-deny this item warns about was not admitted.
>
> **THE LENGTH-PRESERVING MASK THIS ROW PRESCRIBES WAS DELIBERATELY NOT BUILT, and the reason is a
> measurement rather than a preference.** Its stated rationale is that *"length preservation is what
> lets the same offsets read paths back out of the raw text afterwards"* -- and NO RULE DOES THAT.
> Every path site re-runs `[regex]::Match($seg.Raw, ...)` and computes its offsets inside `Raw` from
> scratch. Length-preserving masking would change what every OTHER rule sees, for zero benefit to the
> defect being closed. **Widening scope beyond the defect is precisely how the earlier attempt at this
> item acquired five new fail-opens**, which is the outcome this row's own DO-NOT-SHIP order records.
>
> The banked patch remains unshipped and untouched.
>

> 🔢 **Re-scored 2026-08-20 -> P2.** Value **7/10** · Difficulty **4/10** · _quick win_. Rule 3c still decides on $seg.Scan at worktree_gate.ps1:976-978 while Remove-QuotedSpans at :347-388 blanks every closed quoted span, so a quoted danger key is erased before the disarm regex runs; grep finds no length-preserving mask, no bare-word unmask, and the pinned ALLOW test the item names is absent from tests/. Value 7 because the fail-open needs no unusual spelling and disarms the ledger, claim and leak commit gates for every worktree at once with no compensating detection, but its blast radius is the developer harness rather than a deployment; difficulty 4 because the one written fix was rejected on verification after acquiring five new fail-opens, so the remainder is a scanner rewrite plus an adversarial test round. _(was 8/10 · 3/10.)_
>
Expand Down
26 changes: 25 additions & 1 deletion scripts/hooks/worktree_gate.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -554,7 +554,31 @@ function Remove-QuotedSpans([string]$s, [bool]$PosixEscapes = $false) {
# main denies, so it was reverted. Do NOT re-add the lowercase emit without that
# discriminator, and do not add the discriminator without re-measuring those two.
$span = $s.Substring($openAt + 1, $i - $openAt - 1)
if ($span -cmatch '[\\/](git(?:\.exe)?)$') {
# BACKLOG #1069: A QUOTED SPAN HOLDING ONE BARE WORD IS UNMASKED, because quoting an argument
# is ORDINARY and blanking it erased the disarm key before rule 3c ever ran. Measured on the
# shipped gate, all ALLOW where the unquoted spelling DENIES:
# git -c "core.hooksPath=/dev/null" commit -m x
# git config 'core.hooksPath' '/dev/null'
# git config --add "core.hooksPath" /dev/null
#
# WHY NOT MATCH THE RAW TEXT INSTEAD: a commit message quoting the rule's own name would then
# refuse, and this workstream writes such messages constantly. The discriminator is WHITESPACE
# -- prose has it and stays masked; a config key does not and becomes visible.
#
# DELIBERATELY NOT LENGTH-PRESERVING, against this item's own prose. That rationale is "the same
# offsets read paths back out of the raw text afterwards", and NO RULE DOES THAT: every path
# site re-runs [regex]::Match($seg.Raw, ...) and computes offsets inside Raw from scratch.
# Length-preserving masking would change what every other rule sees for zero benefit, and
# widening scope is exactly how the earlier attempt at this item acquired five new fail-opens.
#
# ONE SPELLING STAYS OPEN BY DESIGN: a quoted MULTI-WORD value such as
# -c 'alias.ci=commit --no-verify'. Its value contains a space, so quoting is its only writable
# spelling and this carve-out cannot reach it without re-admitting the prose false-deny. Pinned
# as an ALLOW test so a later change cannot close it silently or claim it was never there.
if ($span.Length -gt 0 -and $span -cnotmatch '[\s''"$(){};&|`]') {
[void]$out.Append($span)
}
elseif ($span -cmatch '[\\/](git(?:\.exe)?)$') {
[void]$out.Append($Matches[1])
}
else {
Expand Down
92 changes: 92 additions & 0 deletions tests/test_worktree_gate_quoted_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
# Copyright (C) 2026 MessageFoundry Organization and contributors
"""BACKLOG #1069: a QUOTED disarm key was blanked before rule 3c could see it.

``Get-ScannableSegments`` builds each segment's ``Scan`` through ``Remove-QuotedSpans``, which blanks
every closed quoted span. Rule 3c matched the danger key against ``Scan``. So quoting the key erased it
before the disarm regex ran, and **quoting an argument is ordinary** -- this needed no unusual spelling
and disarmed the ledger, claim and leak commit gates for every worktree at once.

THE FIX IS A BARE-WORD UNMASK, NOT A RAW-TEXT MATCH. Matching the raw text instead would refuse a commit
message that quotes the rule's own name, and this workstream writes such messages constantly. The
discriminator is WHITESPACE: prose has it and stays masked, a config key does not and becomes visible.

EVERY DENY ROW HERE IS PAIRED WITH THE UNQUOTED CONTROL BELOW. Without it an ALLOW cannot be told from a
probe that never reached a governed repo -- the first version of this file reported the unquoted control
as ALLOW, which would have read as a far larger finding than the real one. It was a broken probe.
"""

from __future__ import annotations

from types import SimpleNamespace

import pytest

from tests.test_worktree_gate import run_gate
from tests.test_worktree_gate_control_plane import repo, shell # noqa: F401

#: The unquoted spelling. Denied before this fix and after it, so it is a KNOWN-ANSWER control on the
#: probe itself rather than a test of the fix.
UNQUOTED_CONTROL = "git config core.hooksPath /dev/null"

#: Every spelling BACKLOG #1069 measured as ALLOW on the shipped gate. All five deny now.
QUOTED_DISARMS = [
'git -c "core.hooksPath=/dev/null" commit -m x',
"git -c 'core.hooksPath=/dev/null' commit -m x",
'git config "core.hooksPath" /dev/null',
"git config 'core.hooksPath' '/dev/null'",
'git config --add "core.hooksPath" /dev/null',
]

#: Prose that NAMES the danger key inside a quoted commit message. These are the false denies a
#: raw-text match would have introduced, and they are the reason the carve-out is bare-word-only.
PROSE_MUST_STILL_ALLOW = [
'git commit -m "do not set core.hooksPath in a worktree"',
"git commit -m 'BACKLOG #1069: core.hooksPath was invisible when quoted'",
'git commit -m "see rule 3c and its core.hooksPath disarm list"',
]


def test_the_unquoted_key_is_denied(repo: SimpleNamespace) -> None: # noqa: F811
"""THE CONTROL EVERY OTHER ROW DEPENDS ON. If this allows, the probe never reached a governed repo
and every ALLOW in this file is meaningless rather than reassuring."""
assert run_gate(shell(UNQUOTED_CONTROL, cwd=repo.primary), repo.repos) is not None, (
"the UNQUOTED disarm key was allowed -- the probe is broken, so nothing else here is a reading"
)


@pytest.mark.parametrize("command", QUOTED_DISARMS)
def test_a_quoted_disarm_key_is_seen(repo: SimpleNamespace, command: str) -> None: # noqa: F811
assert run_gate(shell(command, cwd=repo.primary), repo.repos) is not None, (
f"FAIL-OPEN: quoting the key hid it from rule 3c -- {command}"
)


@pytest.mark.parametrize("command", PROSE_MUST_STILL_ALLOW)
def test_prose_naming_the_key_is_not_denied(repo: SimpleNamespace, command: str) -> None: # noqa: F811
assert run_gate(shell(command, cwd=repo.primary), repo.repos) is None, (
f"FALSE DENY: a commit message that merely NAMES the key was refused -- {command}"
)


def test_a_QUOTED_MULTI_WORD_alias_value_is_still_invisible(repo: SimpleNamespace) -> None: # noqa: F811
"""PINNED AS AN ALLOW ON PURPOSE. THIS IS AN OPEN HOLE, NOT A PASSING PROPERTY.

``-c 'alias.ci=commit --no-verify'`` disarms the hooks and is still not seen, because its value
contains a SPACE -- so the bare-word carve-out cannot reach it without re-admitting the prose false
deny the tests above pin. Quoting is that value's only writable spelling, so the whole class is open.

IT IS PINNED SO A LATER CHANGE CANNOT CLOSE IT SILENTLY OR CLAIM IT WAS NEVER THERE. If someone
builds a real argument tokeniser, this test SHOULD fail, and the correct response is to invert it
together with BACKLOG #1069's banner -- not to delete it. The one pass that attempted that tokeniser
acquired five new fail-opens, which is why the narrow carve-out shipped instead.
"""
assert (
run_gate(
shell("git -c 'alias.ci=commit --no-verify' ci -m x", cwd=repo.primary), repo.repos
)
is None
), (
"the multi-word alias spelling now DENIES. That is progress, not a regression -- invert this "
"test and BACKLOG #1069's banner together rather than deleting either."
)
1 change: 1 addition & 0 deletions tests/tooling_manifest.txt
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ tests/test_worktree_gate_hijack.py
tests/test_worktree_gate_interpreter_flags.py
tests/test_worktree_gate_interpreter_sigils.py
tests/test_worktree_gate_quote_straddle.py
tests/test_worktree_gate_quoted_key.py
tests/test_worktree_gate_receipts.py
tests/test_worktree_gate_remedy_families.py
tests/test_worktree_gate_rule_agnostic_coverage.py
Expand Down
Loading