Skip to content

guard: the commit scan covers every commit form and checkout the guard allows [PROTECTED: owner merges] - #979

Merged
jayden1711 merged 15 commits into
mainfrom
issue-971-commit-scan
Oct 22, 2026
Merged

jayden1711 merged 15 commits into
mainfrom
issue-971-commit-scan

Conversation

@jayden1711

@jayden1711 jayden1711 commented Oct 21, 2026 •

Copy link
Copy Markdown
Owner

Intent: the commit scan now covers every commit form and checkout the guard allows. Before an agent's command commits, the guard models the command line (new src/guard/commit.ts): which checkout the commit lands in and what it will record. It scans that content itself (objects by id and files from disk, with the changed paths from the change reader src/coordinator/change-files.ts), never a diff git renders, across the whole checkout. A command line whose commit it can't model, a directory change it can't resolve exactly, a git command whose words the shell could change, or a place the commit may run that isn't a checkout it can read, is refused with a reason, never scanned narrowly or skipped. Plain commits keep their behaviour: their new staged lines are scanned; people's own sessions are not scanned (they opt in with the git pre-commit hook).

Risk class: protected. It changes the agents' tool guard (src/hook.ts, a new src/guard/ module, src/scan/secrets.ts), the locked guard-tree pin in test/review.test.ts (two modules added: src/guard/commit.ts and src/coordinator/change-files.ts; nothing removed), and moves the SizeFile type from src/pr-rules.ts into src/coordinator/change-files.ts (a type only, re-exported from src/pr-rules.ts) so the guard's tree grows by that one module.

Closes #971

How it was verified

  • Fail-without, round 1 (measured). test/adversarial/commit-scan.test.ts, written as a new file into a worktree at 9f9533a and run there: # pass 2, # fail 8, each failure actual: 'allow'.
  • Fail-without, round 2 (measured). test/adversarial/commit-scan-routes.test.ts, written as a new file (byte-identical, sha256 checked) into a worktree at a40dafc (this PR's previous head) and run there: # pass 1, # fail 6. The passing one keeps plain commits as they were. A per-line run of the same lines at a40dafc let through every route the second review and the final check named (scratch file, not committed).
  • After the fix (measured). On this branch: commit-scan.test 10/10, commit-scan-routes.test 7/7, commit-plan.test 6/6.
  • Related tests after merging origin/main (measured): change-files-renames (the change-reader lint CI flagged: now clean), review (guard-tree pin), pr-rules, install, hook (the recorded guard-answer corpus: unchanged), push-hook-guard, docs-guardrails, changelog, merge-policy: 228/228 in one run. adversarial.test: 96 pass, 0 fail.
  • The reviewers' probes (measured): the second review's harness and the final check's probes, run against this branch's build: every route they list is refused. The final check's ordinary-work probe: everything it lists is allowed except the refusals listed below.
  • Real gitleaks (measured): the hook-level tests use the real scanner (8.24.2 here), as test/install.test.ts already does; they skip where it isn't installed.
  • Checks (measured): npm run typecheck clean; npm run check:denylist 1442 files clean; check-dco.mjs origin/main HEAD all signed off; push gate on origin/main..HEAD clean.
  • Package size (measured, npm pack --dry-run --json, no web build on either side, before the merge of main): tarball 1,484,470 → 1,497,197 (+12,727); unpacked 4,971,214 → 5,019,503 (+48,289). The delta is the new guard module and the content scan (inferred: test files are not in the package). Correction (round 6): these numbers had no web build, and the budget counts it; with the web build the package went over the unpacked budget, see "Package budget" below.
  • Full suite: not run locally (owner rule); CI runs it.

New dependencies

None.

What ordinary work this now refuses

Each is refused with its reason, and a way to do it instead (measured with the final check's ordinary-work probe and the unit tests):

  • A program other than a read, a directory change or staging, run before a commit in the same line (for example a build or a formatter, then the commit). Run it first, then commit in its own call.
  • git commands other than status, rev-parse, diff, log, show, add, rm or mv before a commit in the same line (for example fetch, rebase or stash).
  • A git command whose subcommand isn't a literal word, or whose words hold an unquoted expansion or pattern, in any line (quote the expansion, or use literal words).
  • A directory change before a commit that isn't one literal, existing directory reached directly.
  • A line the model can't read that may commit.
  • In any line: a program named through an expansion, a shell fed its script through a pipe or a here-document or given an expanded script, a program that builds or splits a command (xargs with a replace string, env -S).
  • A commit that takes working-tree files with a filter, encoding or ident attribute (stage them first).
  • A commit where git would run a program the checkout's config names while the line reads changes: a file-system monitor program (git's own daemon is allowed), a filter its files use (the standard git-lfs filter is allowed since round 5b; the refusal says why and what to do), and, only when the line shows diffs or signatures before it commits, an external diff, a text-conversion driver or a signature program. Submodules' own config and other checkouts the line reads are checked the same way (round 5).
  • Interactive staging or an interactive commit (round 7): git add -p/-i in any spelling before a commit in the same line, and git commit -p/--interactive. Way around: stage in a call of its own, then commit.
  • Under the standard large-file filter, a small file (1024 bytes or less) that names git-lfs, hawser or git-media but isn't the exact pointer form, when it is committed from the working tree or staged in the same line (round 7). Way around: stage it in an earlier call (measured allowed by the final check).

What this can never do

  • Let a commit through with nothing scanned: every place it may run must resolve to a checkout the scan reads, or the line is refused; the hook refuses an empty plan too (inferred from the code, measured by the tests from a folder that isn't a checkout).
  • Let the checkout's own settings for showing changes hide content from the scan: the scan reads objects and files, not rendered diffs. Measured for colour, prefixes, path quoting, text conversion, binary and no-diff attributes and external diff, on each scan route; that no other display setting can is inferred from the design (the scan never asks git to render a change).
  • Change the checkout's files or content: the scan reads objects and files and writes its copies only to its own temporary directory, removed afterwards (inferred from the code). Scoped: git itself may refresh the index's stat data during the scan's change listing, and under the standard large-file filter the real git-lfs may store objects in the checkout's large-file store during that listing. Neither changes what is committed.
  • Reopen a route main had closed, as far as measured: at this head, every documented probe line the reviews measured as refused on main is refused or found (measured with the reviewers' probe groups, round 4 below; round 3 claimed this while the config-run program lines were still let through, and was wrong). Each changed file is scanned whole after and before the commit, and a secret counts as new when the file holds it more often after (round 5; this replaced the line placement of rounds 3 and 4). No .gitleaksignore is honoured, not one on disk and not one staged in the commit, and no file of the commit can stand in for one (corrected in rounds 3 and 4). The checkout's .gitleaks.toml and inline allow comments are honoured as on main; they belong to secret scans: ignore suppressions an agent can write #980.
  • What it can't promise: content a program git itself runs during the commit adds (a hook, a filter) doesn't exist when the scan runs, so it isn't scanned (see What remains).

Compromised component

  • Agent user: can no longer get a commit past the scan by choosing a commit form, checkout, directory change, expansion inside git's words or a git display setting the guard allows; a line the model can't read is refused. It can still commit through programs the line runs (a script, a build step) and through programs git runs during a commit (see What remains). It can't widen the model: src/guard/** is locked with the guard, and src/coordinator/** is locked as the coordinator.
  • Coordinator: unchanged behaviour; the type move doesn't change its code paths (inferred; its tests pass, measured).
  • Dashboard, other instances, hub viewer: no new route, input or state; nothing to gain (inferred).
  • Someone with push access: could change src/guard/commit.ts or the change reader, but both are locked, so the change waits for the owner (inferred from src/review.ts).
  • The scanned checkout's own content: the scan reads objects and files only and runs no checkout-configured program itself, except what git runs for the change listing and object reads (inferred).

What remains

  • Commits made by programs the line runs (a script, a build step, a language's subprocess call) are outside what a command-line model can see; the push gate still scans what leaves the machine.
  • Content that programs git itself runs during a commit add (hooks, filters, set up in an earlier call) is not scanned before the commit; the orchestrator is filing a follow-up to scan inside git's own pre-commit step as a second layer.
  • Programs the checkout's config names that git runs while the line reads changes are refused, as far as rounds 4 and 5 name them: a monitor program, a filter its files use, and diff and signature programs when the line shows diffs or signatures; in the checkout, its checked-out submodules and other checkouts the line reads (round 5; round 4 left signature programs, submodules and other checkouts' reads open, and its wording claimed otherwise). Programs git runs during the commit itself (hooks, an editor, a signing program) are not; they are the bullet above.
  • Some shell features still turn text the model sees as data into a command: builtins that evaluate their arguments, builtins that re-point a command name at another program, traps, programs that run a command string they are given, script paths that read the command's input, and process substitution. They are the same as on main and are not closed here; the owner is deciding whether to close them one by one or by design in commit scan: also scan the exact index at commit time (second layer) #983, a commit-time scan git itself runs.
  • The scan runs before the command, so a process outside this command line that changes the checkout in between is out of scope (inferred).
  • Commits made by git commands other than a commit are out of this issue's scope; the push gate still scans what leaves the machine.
  • Under the standard large-file filter (round 5b), git runs whatever git-lfs comes first on its path. The guard reads the hook's PATH and checks that the first git-lfs there is a regular file. It can't verify the program itself, and the agent's shell may have a different PATH; a replaced git-lfs could change what is stored after the scan has read the file.
  • This change decides what is scanned, not how the scanner judges it: since round 5 each file is scanned at its own path, so the checkout's own rules and allowlists match as on main; the scanner's own suppressions are filed separately (secret scans: ignore suppressions an agent can write #980).
  • Whether a large file over 1024 bytes that the large-file program still reads as a pointer can carry an object past the scan is unverified (guess). The push gate still scans what leaves the machine.
  • Follow-ups recommended by the final check, not done in this head:
    • run the named-pipe test in a child process with a timeout, so a build without the check can't block the test run;
    • pin the new refusal reason in the round-6 test of bytes that begin like a pointer.
  • docs/guardrails.md labels the new guarantee "inferred" until CI on main has run its tests.

Round 3

Changes (head after the round-3 commit and a signed-off merge of origin/main):

  • Added lines (D1): placed by a line diff computed in process (a shortest edit script, src/scan/secrets.ts#addedLines), so no repo or user config or attribute can change it. Over a fixed work budget it counts the whole new file as added: more is scanned, never less.
  • No content decides whether a file is read (D2): every file handed to the scanner starts with 64 KiB of fixed text. Measured with a scratch harness: without it, 13 of 16 file-type signatures made the scanner skip the file; with a 16-byte text prefix, signatures placed later in the file still made it skip; with the 64 KiB prefix, all 16, the signatures realigned after it, and signatures placed at 100 KB up to 2 MB were all scanned.
  • Working-tree bytes vs stored bytes (D3): a commit that takes a working-tree file with a filter or working-tree-encoding attribute, from any attributes source, is refused; the scan never runs a filter. A staged change is still read from the index, as stored.
  • The commit's files never configure the scanner (D6): no ignore list is honoured, including one staged in the commit.
  • Programs the model can't name (D4): refused in any line: a command name that isn't literal (after wrappers too); a wrapper that builds a command from its input; a program that splits a string into a command; a shell reading its script from a pipe (with no script operand) or a here-document, or given an expanded script; git run by another program without a literal, non-commit subcommand; and the same, as far as they can be seen, in lines the strict reader can't parse. Correction (round 4): this closes the forms listed, not the whole class; the shell-evaluation forms named in What remains are the same as main and stay open.
  • Exempt programs (D5): the list of programs treated as never running another program is now: echo, printf, which, type, whereis, hash, help, test, [, ls, cat, head, tail, wc, grep, egrep, fgrep, cd, pushd, mkdir, rmdir, rm, cp, mv, touch, stat, file, du, realpath, readlink, basename, dirname. Each prints or tests text, lists, moves or reports on files, or is a shell builtin with no option that runs a program (inferred from their documented options). gh, man, rg, info, whatis and apropos are off it.

Verification:

  • Fail-without (measured): the new test/adversarial/commit-scan-content.test.ts, written as a new file (sha256 checked byte-identical) into a worktree at 9c6761f and run there: # pass 0, # fail 7.
  • After (measured): on this head, the four commit-scan test files pass 30/30; with the related set (change-reader lint, guard-tree pin, pr-rules, install, guard-answer corpus, push-hook guard, docs citations, changelog, merge-policy, adversarial suite): 331 pass, 0 fail, 12 todo.
  • The round-2 final check's probes (measured, on this head): every key-block, signature, encoding and filter, ignore-list, program and wrapper probe is refused or found, except the ones that remain by design: inline allow comments and the checkout's .gitleaks.toml (secret scans: ignore suppressions an agent can write #980), and the config-run programs in What remains.
  • Cost to ordinary work (measured with a scratch list of non-commit lines):
    • Newly refused: a program named through an expansion (an editor variable, a tool under an expanded path, a substituted path); piping a script into a shell (installers); a here-document fed to a shell; a shell given an expanded -c script; xargs with a replace string.
    • Still allowed: literal paths to tools, find -exec with a literal program, xargs without a replace string, loops, pipes into ordinary programs, python here-documents, bash -c with literal text, quoted expansions in arguments.
    • The final check's ordinary-work probe gets the same answers as round 2 (measured).
    • I judge the cost moderate, not high (inferred): each refused form has a literal spelling, and the refusal says so.
  • Checks (measured): typecheck clean; denylist 1457 files clean; DCO all signed off; push gate on origin/main..HEAD clean.
  • Package size (measured, this head, no web build): tarball 1,519,391, unpacked 5,095,934. Main had grown since round 2, so the delta was not re-measured against it.

Round 4

Changes (head 1e210a9; origin/main hadn't moved, so no merge):

  • Every placement of added lines (E1): each run the line diff marks as added is widened across the unchanged lines it could slide over: up while the line before the run equals the run's last line, down while the line after it equals its first. So every placement any diff could choose is scanned (src/scan/secrets.ts#slideWidened). Measured: the three multi-line shapes the reviewer sent (two of them the same on main) are found, staged and from the working tree.

  • No file stands in for the scanner's own (E2): the commit's files are written under a fixed folder below the scan's root; the round-3 rename is gone. Measured: an ignore-list name and a near name staged together, and a file named like that folder, are found.

  • Ident attribute (E3): a working-tree file with an ident attribute refuses the commit, as filter and working-tree-encoding already did. Tracked and untracked forms tested.

  • Config-run programs (second review): before it reads anything, the scan reads the checkout's effective config (git config --list, which runs no program). It refuses the commit when the config names:

    • a file-system monitor (anything but off);
    • an external diff;
    • a diff driver's text conversion or command;
    • a filter that a tracked or untracked file of the checkout uses.

    Fail closed. Measured: the second review's F1–F3s and O3, and the final check's O1 and O2, are refused. A monitor set to off still commits.

    • Cost: a checkout that uses a configured filter (a large-file store, say) refuses every agent commit.
  • Exempt list (E5): hash, test, [ and printf are off it. It is now: echo, which, type, whereis, help, ls, cat, head, tail, wc, grep, egrep, fgrep, cd, pushd, mkdir, rmdir, rm, cp, mv, touch, stat, file, du, realpath, readlink, basename, dirname. Each prints text, lists, moves or reports on files, or is a lookup builtin, with no option that runs a program (inferred from their documented options).

  • Over-refusals fixed (E6): a [ is a pattern only with a ] after it in the same word, and [/[[ as a command name is a name. After a shell keyword, a command's own variable assignments are skipped too. For xargs, only its own options are read: they stop at the command it runs. Tested: test commands, if/while with tests, and xargs runs without a replace string are allowed again; xargs with a replace string is still refused.

  • Read-only programs before a commit (E7, a loosening named under principle 9): round 3 let head, tail, wc, grep and ls run before a commit in the same line, next to true, :, pwd, echo and cat. Why it's safe: they read and print only, with no option that writes a file or runs a program. A redirect or tee that writes a file is still refused (measured with the final check's read-only probe). The check this exempts is "nothing but reads, directory changes and staging before a commit"; it wasn't wrong, but its list was too short for ordinary git log | head && git commit lines.

  • Weakening gate: two existing test lines in this PR changed. Both still require a refusal:

    • In commit-scan-content.test.ts, the staged case with a configured filter now expects a refusal ("could not run") instead of "allow".
    • In commit-scan-routes.test.ts, the text-conversion and external-diff display shapes now expect the config refusal instead of a finding.

Verification:

  • Fail-without (measured): the new test/adversarial/commit-scan-reads.test.ts, written as a new file (sha256 5419da3e…, byte-identical) into a worktree at 1a731dc and run there: # pass 0, # fail 6.
  • After (measured): the five commit-scan test files pass. The related set (change-reader lint, guard-tree pin, pr-rules, install, guard-answer corpus, push-hook guard, docs citations, changelog, merge-policy, adversarial suite) gives 337 pass, 0 fail, 12 todo.
  • Reviewers' probes on this head (measured):
    • Final check: the diff group's SL1–SL4 are found, and SL3 is allowed, as on main. In the paths group, CL1, CL1c and CL2 are found; PA1–PA5 are allowed by the scanner's default path rules. The attrs group is all refused or found. The budget group is found, in 0.6–1.3 s. Cost lines are allowed again except xargs with a replace string. The read-only group refuses every write.
    • The r2 groups: fsm F1–F3s and ordinary O1–O3 are refused.
    • The progs and unread groups still allow the shell-evaluation forms that are the same on main (held, see What remains).
  • Checks (measured): typecheck clean; denylist 1458 files clean; DCO all signed off; push gate clean.
  • Package size (measured, no web build): 1,520,741 / 5,100,694.

Round 5

Changes (head 1d1966f, after a signed-off merge of origin/main 1808043):

  • Whole files, counted (N1): the scan no longer places added lines. Each changed file is scanned whole after the commit and whole before it (the parent's blob, the index's, or nothing for a new file). A secret counts as new when the file holds it more often after than before. Placement can't matter, and a multi-line key always has its full context. The in-process line diff and its widening are deleted. src/scan/secrets.ts#introduced holds the rule.
    • The scanner's report holds the secrets while they are compared. It stays in the scan's own temporary folder, is deleted as soon as it is read, and only rule names and paths leave the function.
  • Own paths (N2): every file is written at its own repo-relative path, so the checkout's path rules and allowlists see what main's scan saw. A root file with a name the scanner reads as its own settings (its ignore list, its config, its baseline) is scanned in a separate root, a folder lower, as content.
  • Option and pattern reading (N3, N4): xargs options that take an attached value only no longer swallow the next word, and xargs named by a path is read too. A [ counts as a pattern when a ] follows it anywhere in the word, across escaped or quoted separators.
  • Config programs where git runs them (N5, and the second review's three open reads):
    • A monitor program is always refused; git's own monitor daemon (a boolean value) is allowed.
    • An external diff, a text-conversion driver or a diff command is refused only when the line shows a diff before it commits: a diff, a show, a log with any option that may print one, or a verbose status.
    • A signature program (gpg.*program) is refused when the line shows commits and asks for signatures, by option, by a signature format, or by the log config.
    • Every checked-out submodule's own config is checked: links in HEAD's tree or staged, with a checkout there.
    • So is every checkout a read before the commit names with -C.
  • Filter refusal (held for the owner): unchanged, now saying why and what to do.
  • E5 exempt list (N7): tested.
  • Weakening gate: three existing test lines in this PR changed.
    • The round-3 and round-4 unit tests of the deleted line diff and widening now test the counting rule that replaced them.
    • The round-2 display-settings test expects a finding again for text conversion and external diff. None of its lines shows a diff, so no diff program runs; the scan finds the secret.
    • Every attack in those files is still refused (measured: they pass).

Verification:

  • Fail-without (measured): the new test/adversarial/commit-scan-findings.test.ts, written as a new file (sha256 72db0afb…, byte-identical) into a worktree at 1e210a9 and run there: # pass 2, # fail 8. The two that pass are:
    • the control that a file already holding a secret still commits;
    • the round-4 exempt-list guard.
  • After (measured): the six commit-scan files plus the related set (change-reader lint, guard-tree pin, pr-rules, install, guard-answer corpus, push-hook guard, docs citations, changelog, merge-policy, adversarial suite), after the merge: 350 pass, 0 fail, 12 todo.
  • Fuzz (measured, in the test): random before and after key files, with each case's added lines taken from git's myers, patience and histogram diffs. Every case where some file holds a key more often after the change is refused, whether or not a diff shows the key whole.
  • Reviewers' probes on this head (measured):
    • r4 slide SL5–SL7, SLH and SLP: found, staged and from the working tree.
    • r4 cfg: C1–C3b, C5, C6 and C9 refused; C4 allowed (not reproduced on either tree).
    • r4 allow: AL1, AL5 and AL6 allowed and AL2–AL4 found, the same as main.
    • r4 xargs XA0–XA5 and glob GL0–GL4: refused.
    • r4 cost: K2–K5 allowed; K1 refused (the filter, held).
    • r2 fsm F1–F3s and ordinary O1–O12 and O17: refused.
    • r2 scan, suppress, diff, paths and attrs: as before, with SL3 now found.
  • Cost (measured): the r4 cost group's monitor daemon, unused text conversion and external diff without diff reads are allowed again. With 30k files and a filter configured, the scan took 1.6 s.
  • Checks (measured): typecheck clean; denylist 1485 files clean; DCO all signed off; push gate clean.
  • Package size (measured, no web build): 1,534,386 / 5,145,993.

Round 5b (owner's decision on large files, 2026-10-21)

A narrow, owner-approved loosening of the filter refusal added in round 4 (principle 9).

  • What it allows: a checkout whose large-file filter is exactly the standard git-lfs one can commit. "Exactly" means filter.lfs.clean = git-lfs clean -- %f, filter.lfs.smudge = git-lfs smudge -- %f, filter.lfs.process = git-lfs filter-process and filter.lfs.required = true. Every value the effective config gives for those keys, from any config source, must equal that string, and the large-file store must be at its default place.
  • What the scan reads: each large file's own bytes, which is what the filter stores. A working-tree file is read from disk. A stored pointer is read as the object it names in the checkout's large-file store, and a pointer whose object is missing refuses. (Corrected in rounds 6 and 7: the stored bytes are read too, not replaced by the object, and only the exact pointer form is read as a pointer; any other small file naming a pointer version refuses.)
  • The check it exempts: "a filter a file uses refuses the commit". That check wasn't wrong. The owner decided the standard large-file filter is an accepted program.
  • Still refused: any other value for those keys, any other filter driver, and a git-lfs first on the path that isn't a regular file. What the guard can't verify about that program is in What remains.

Verification:

  • Fail-without (measured): the new test/adversarial/commit-scan-lfs.test.ts, written as a new file (sha256 e664f9cd…, byte-identical) into a worktree at 1d1966f and run there: # pass 3, # fail 2.
    • The two that fail are the loosening itself: a large file holding a secret is found in its own bytes, and a clean one commits.
    • The three that pass were already refusals there: a near-miss config (an extra argument, a different program, a custom clean, not required), a pointer whose object is missing, and a git-lfs that isn't a regular file.
  • After (measured): the seven commit-scan files and the related set: 355 pass, 0 fail, 12 todo. Typecheck clean; denylist 1486 files clean; DCO all signed off; push gate clean.
  • Not run here (inferred): the tests set large files up by hand, because git-lfs isn't installed on this machine. A working-tree commit of a changed large file runs the real git-lfs in the scan's own change listing; with git-lfs installed, as on CI runners, that is the program the owner allowed.

Round 6

Changes (head 3e63c5e, after a signed-off merge of origin/main 1cee9bd):

  • Stored bytes always (R5-1): under the standard large-file filter, a side counts as a pointer only when the whole blob is exactly one valid pointer. Then the scan reads the pointer's bytes and the object it names, never the object instead. The object must exist, and its size and hash must match the pointer, or the commit is refused. (Corrected in round 7: the large-file program reads other spellings as pointers too, which this round scanned as plain text; since round 7 any small large file that names a pointer version but isn't the exact form refuses.)
  • Pointers on disk too (R5-5): working-tree and untracked files that are exactly a valid pointer are read the same way, since the filter passes a pointer through as it is and the object is what gets pushed.
  • Driver name case: config keys keep their subsection's case, as git does, so only the exactly named lfs driver gets the allowance.
  • Large-file settings (R5-6): any lfs.* key outside a short safe list (remote URLs, lock checking, fetch filters, timeouts, per-remote access) takes the allowance away. That includes extension programs, a moved store and custom transfers.
  • Reads, by design (R5-3):
    • With a diff program configured, these refuse the commit:
      • any diff or show;
      • any log with an option outside a short list of options that never take the next word;
      • any status with an option outside a short list;
      • an interactive staging step, found while re-running the round-5 probes, where the config's diff filter applies. (Corrected in round 7: an interactive step can also open an editor, from config or the environment, so since round 7 it is refused whatever the config.)
    • With a signature program configured, any log or show refuses it.
    • The monitor setting is read the way git reads it (git config --type=bool); a value git doesn't read as a boolean is a program.
  • Nested checkouts (R5-4): a line that stages untracked content also has every untracked nested checkout checked like a submodule (config programs, recursively). Staging could turn it into one.
  • Tests (CI red): every commit-scan test now sets GIT_CONFIG_NOSYSTEM and a scratch GIT_CONFIG_GLOBAL, so the runner's own git config can't change them. The large-file "not required" case now sets required to a non-standard value rather than leaving it out.
  • Costs (R5-7, measured with the round-5 probes):
    • A staged pure rename, from an earlier call, of a file that already holds a secret is refused. The renamed file is new to the scan, and main allowed it.
    • Amending a merge whose secret came from its second parent is refused. The amend is compared against each parent, and main didn't scan amends.
    • Under the large-file allowance, a custom transfer setting is refused (outside the safe list).
  • Weakening gate:
    • The round-5 signature test now expects a plain log before a commit to be refused when a signature program is set. It used to expect "allowed", so this is a tightening.
    • The round-5b "not required" case sets a non-standard value instead of omitting the key. It is still refused.
    • The isolation lines are additions.

Verification:

  • Fail-without (measured): the new test/adversarial/commit-scan-stored.test.ts, written as a new file (sha256 3dd52ad6…, byte-identical) into a worktree at ce7d4ab and run there: # pass 0, # fail 8.
  • After (measured): after the merge, the eight commit-scan files plus the related set (change-reader lint, guard-tree pin, pr-rules, install, guard-answer corpus, push-hook guard, docs citations, changelog, merge-policy, adversarial suite, redaction) give 373 pass, 0 fail, 12 todo. The interactive staging fix came after that run; the commit-scan files and the core checks pass again at 3e63c5e: 181 of 181.
  • Round-5 probes on this head (measured):
    • Found: MV1, MV2 (cost), DUP1, DUP1w, DUP2, NEW2, IDX1–IDX3, AM1 (cost), AM2, AM3, LF1, LF1c, LF2u, LF7, LF8.
    • Refused: LF2, LF3, LF4b, LF4d, LF5, LF6, LF6b, LF6c, LF9, LF10, LP0, LP1, SV0–SV3, SG0–SG4, CD0, CD1, C5b.
    • Allowed: MV3, DUP3, LF4f, LF4l.
    • DF1 was allowed and committed nothing before the interactive staging fix; it is covered by a test now.
  • Checks (measured): typecheck clean; denylist 1504 files clean; DCO all signed off; push gate clean.
  • Package size (measured, no web build): 1,547,313 / 5,186,142. "Within budget" was wrong: the budget counts the web build, and with it the unpacked size was over (CI run 39685972735). See "Package budget".

Round 7

Changes (head 85a5bbd; origin/main hadn't moved):

  • Inexact pointers (R6-1): under the standard large-file filter, a large file of 1024 bytes or less that names any version the large-file program accepts in a pointer, but isn't the exact pointer form, refuses the commit. This covers stored bytes, the working tree and untracked files. The large-file program reads more spellings as pointers than the one form the scan reads with its object, so such a file could stand for an object the scan never sees.
  • Interactive staging (R6-2): interactive staging (add -p, -i, --patch, --interactive, any prefix) and an interactive commit (commit -p, --patch, --interactive) are refused in the line whatever the config, as edited staging (add -e) already was. Either can open an editor, from config or from the environment the guard can't see, after the scan has read.
  • Object files: an object path is checked with lstat before it is read. A path that isn't a regular file (a folder, a link, a named pipe) refuses the commit with that reason, instead of blocking the guard.
  • Tests: the large-file test again has the case with the required key left out, next to the case with it set to false.
  • Weakening gate: four existing test lines changed, across three tests of this PR, all to tightened or still-refused expectations (corrected: an earlier version of this section said three lines).
    • The round-6 test of bytes that begin like a pointer (two lines, one for each of its two cases) accepts the new refusal as well as a finding; both deny.
    • The round-6 interactive-staging test accepts the new outright refusal as well as the config refusal; both deny.
    • The unit test no longer lists commit -p among commits scanned with working-tree content; it is now refused.

Verification:

  • Fail-without (measured): the new test/adversarial/commit-scan-pointers.test.ts, written as a new file (sha256 1d094c07…, byte-identical) into a worktree at dd6b301 and run there: # pass 0, # fail 3.
    • The named-pipe test is in its own file, test/adversarial/commit-scan-fifo.test.ts. On a build without the check its read blocks the whole test process, so it can't be part of a fail-without run; it passes at this head.
  • After (measured): the nine commit-scan test files (plus commit-plan.test.ts) and the related set: 378 pass, 0 fail, 12 todo. Typecheck clean; denylist 1506 files clean; DCO all signed off; push gate clean.
  • Round-6 probes on this head (measured):
    • PV0 found.
    • Refused: PV1–PV8, PVU, ED1–ED3, CP1, CP2, RQ0, MM1, MM2.
    • ED0 (plain staging) allowed.
  • Package (measured, with the web build, npm pack --dry-run --json --ignore-scripts): at 85a5bbd, tarball 2,045,483 and unpacked 6,404,343, within that head's raised budget (2,050,000 / 6,730,000). Superseded by the merge of main: see "Package budget".

Package budget (superseded by the merge of main)

Now (head c935e7e, measured):

History (heads dd6b301 to 85a5bbd):

  • What changed: PACKAGE_BUDGET.unpackedSize in test/package-files.test.ts goes from 6,400,000 to 6,730,000. The tarball budget (2,050,000) is unchanged; that dimension isn't over.
  • Measured (head dd6b301, after npm run build and npm run build:web, npm pack --dry-run --json --ignore-scripts): tarball 2,045,128 (budget 2,050,000); unpacked 6,402,689, over the 6,400,000 budget by 2,689. CI shard 1/4 of run 39685972735 at 3e63c5e measured the same 6,402,689.
  • The new value: measured plus 5% is 6,722,823, rounded up to the next 10,000: 6,730,000. That follows the owner's package-budget practice.
    • The comment above the budget in that test says "about 2%" for a raise between releases. I followed the practice the orchestrator named (5%), and flag the mismatch here for the owner.
  • The check it loosens: "the package stays within its size budget". Its finding wasn't wrong. This PR grows the package with the new guard module and the content scan in src/scan/secrets.ts, on top of what main has added since the budget was set.
  • Verified (measured): the budget test alone passes at dd6b301 with the web build present.

Adversarial pass

Every attack below was run against this branch with scratch checkouts and the test token only (measured), grouped generically under the #707 rule; the exact command lines are with the orchestrator.

  • Commit aimed at or run in another checkout, from a folder inside the checkout, or from a project folder that isn't one: scanned in the right checkout, or refused.
  • Directory changes the shell may resolve differently from the model: refused (one test per variant).
  • Commit forms that take working-tree content, in all spellings, and amends: scanned on that content.
  • Shell evaluation inside a git command's words, including its subcommand, and git run by another program: refused.
  • Staging, writing or running other programs earlier in the same line, including inside the commit's own arguments and redirects: staging is scanned; the rest is refused.
  • The checkout's settings for showing changes, on staged, working-tree, untracked and amended content: scanned all the same.
  • Message text that looks like a commit (here-document bodies in a commit message or a PR body, quoted or not): not mistaken for one.
  • Round 7 (measured with the round-6 probes on 85a5bbd):
    • small large files in pointer spellings other than the exact form, staged or untracked: refused;
    • interactive staging and interactive commits, with and without a configured editor or diff filter: refused;
    • plain staging before a commit: allowed;
    • the required large-file key left out or set otherwise: refused;
    • a large-file object with other bytes or a size off by one: refused;
    • a large-file object that isn't a regular file: refused with that reason.
  • Fixed during the passes: redirect targets' substitutions are checked; candidate checkouts are never skipped; alias lookups that fail refuse.

…d allows

The commit secret scan now covers every commit form and checkout the guard
allows (#971). Before an agent's command commits, the guard models the
command line (src/guard/commit.ts): which checkout the commit lands in and
what it will record. It scans that, across the whole checkout. A command
line whose commit it can't model is refused with the reason, never scanned
narrowly. Plain commits keep their behaviour: their staged changes are
scanned, and people's own sessions are not.

The model is a strict reading of the shell line: only syntax whose meaning
is fully known is accepted, and it must agree with the guard's general
matcher on every commit in the line. The git queries it needs are run by
src/hook.ts, so the new guard module imports only node built-ins and
src/guardrails (#785); test/review.test.ts pins it in the guard's tree.

Test (fails without): test/adversarial/commit-scan.test.ts, 8 of its 10
tests fail at 9f9533a (the other 2 keep today's behaviour);
test/commit-plan.test.ts covers the model itself.

Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
@jayden1711

Copy link
Copy Markdown
Owner Author

Second adversarial review: CHANGES NEEDED (head a40dafc)

I tried to get a secret committed through an agent's command without a refusal, using scratch checkouts, the test token, a scratch git config with no system config, and the hook entry (runHook) with real gitleaks 8.24.2. I compared every route against a build of main at 9f9533a. Under the #707 rule, routes are described in general terms only. The orchestrator has the exact lines.

Findings

  1. Regression (measured): a commit the model can't place in any checkout is let through with no scan at all. If none of the folders the model works out for a commit resolves to a checkout, the plan has zero scans, and the hook allows the command. From a project folder that isn't itself a checkout, a line whose directory change the shell resolves differently from the model committed the token into another checkout. Main refused the same line, because its scan of the non-checkout folder failed and so failed closed. This PR allows it. That reopens a route main had closed (decision guide 3), and it breaks "fail closed when unsure" (1). The code is src/guard/commit.ts:736-741 (a candidate whose top-level query fails is skipped silently) and src/hook.ts:168 (an empty scan list counts as clean).
  2. The model's candidate checkouts miss where the shell actually goes (measured, also open on main). I found three ways a directory change in the line is resolved differently by the model and by the shell. In each, the commit landed in a second checkout holding a secret, while the guard scanned only the hook's checkout and allowed the command. The code is changeDir, src/guard/commit.ts:527-548, plus the silent skip at :739. One of the three depends on what the agent's shell inherits; that part is inferred. This is the class commit scan: cover every commit form and checkout the guard allows #971 is meant to close (other-checkout coverage), so "scanned in the right checkout" is not yet true.
  3. The model sees a git command it can't read and lets it through (measured, also open on main). If the strict parser recognises git but can't read its subcommand, it returns "no commit" unless the word "commit" appears literally (src/guard/commit.ts:691-694). A line like that committed the token. A second, wrapper-based form also got past both matchers. The PR body says commits hidden behind "expanded names" and "wrappers" are refused; for these forms that is false. This is close to the builder's residual "general matcher blind spots", but here the strict model has the information and fails open, and the fix is cheap.
  4. The scan reads git's diff output, which the checkout's own settings can change (measured, also open on main; a new class). Several checkout-level git settings, which an agent can set in an earlier call (measured: the guard allows it), make the staged token absent from the diff gitleaks reads, while the commit still records it. This is a different class from the scanner-suppression issue filed separately: that one is gitleaks' own allowlists, while this is git changing what gitleaks is given. It makes this claim false: "every path either scans a superset of the commit's content".

Checked and holding (measured)

Option abbreviations and negations, combined short options, path-limited commits, and a physical-path move through a plain link all scanned in the right checkout or were refused. Lines that aren't commits were unaffected.

Known residual I confirmed but don't count

Something git runs during the commit, set up in an earlier call, can add content after the scan (measured). That falls under the PR's "anything git itself would run there". I'd still name it in "What this can never do", because the "superset" bullet is false while it exists.

Required changes

  • R1: when a commit is modelled, refuse if any candidate folder fails its top-level query or the set of top levels is empty. Add a test from a project folder that isn't a checkout.
  • R2: refuse every directory change the model can't resolve exactly as the shell would, and don't skip a candidate silently. Add a test per variant (the orchestrator has the three).
  • R3: refuse a git invocation whose subcommand isn't literal, and a wrapper that runs git with no literal subcommand. Add a test for each.
  • R4: close the diff-presentation class (pin how the scan's git presents the diff, or scan the content itself), or correct the "What this can never do" bullet and file the class next to the suppression issue. Name the in-commit programs in the same bullet.
  • None of the PR's tests fail on R1-R3 today (measured: those routes pass at this head).

Verdict: CHANGES NEEDED (one regression against main, three working route classes, two false claims).

@jayden1711

Copy link
Copy Markdown
Owner Author

Evaluator verdict: CHANGES NEEDED (round 1/2)

Head checked: a40dafc7. Base: main 9f9533aa; origin/main hasn't moved since.

I checked that the commit scan covers every commit form and checkout the guard allows. It covers the forms the PR models, and those are tested well. It doesn't yet cover all of them. I measured further commit lines that the guard still lets through at this head without scanning what they record. Under the #707 rule the details have gone privately to the orchestrator, not here. CI is also red, and this PR is the cause.

Acceptance criteria (#971)

Criterion Status Evidence
An attack case for each form in test/adversarial, each failing without the fix partially met Measured: for the forms the PR covers, the tests fail without the fix and pass with it (see Fail-without). Further forms the guard allows still get through unscanned at the head (measured, sent privately), so "every form" isn't met yet.
The guarantee is counted in docs/guardrails.md only once this closes met Measured: the new entry in docs/guardrails.md is labelled "inferred until CI on main runs these tests".

Design checks

  1. commitPlan parses strictly, refuses what it doesn't understand, and agrees with splitCommands on the commit count: partially.
    • Measured: when the parse succeeds, the count must agree (src/guard/commit.ts:696-699).
    • Measured: some commit lines are still let through unscanned (private). → Required change 1.
  2. The scan runs at the right top level for every checkout a commit can land in: partially.
    • Measured: each candidate directory goes through git rev-parse --show-toplevel, and the scan runs from that top level over .. Attack tests 1 and 2 pass: another checkout, a folder inside the checkout, -C .., and cd <other>.
    • Measured and inferred: there are gaps in the checkout coverage (private). → Required change 2.
  3. It records what the commit will contain, and the real index is never touched: met for the modelled forms.
    • What each form scans:
      • staged changes: always;
      • -a, -o, -i, -p, a pathspec or --pathspec-from-file: the unstaged changes too;
      • staging earlier in the line: the unstaged changes and untracked files, through a copy of the index with intent-to-add entries;
      • --amend: the HEAD commit too.
    • Measured: .git/index had the same sha256 before and after each of 34 ordinary lines through the hook at the head, the staging ones included.

Fail-without (method b)

  • Measured. I wrote test/adversarial/commit-scan.test.ts as a new file into a fresh worktree at 9f9533aa and checked it is byte-identical to the head's copy (sha256 1815dd5f…). I ran node --import ./dist/test/isolate.js --test dist/test/adversarial/commit-scan.test.js. The result was # pass 2, # fail 8, and each of the 8 failed with actual: 'allow'. The builder's numbers match.
  • Measured. At the head, the new test and test/commit-plan.test.ts pass 16 of 16.
  • Measured. I ran the 29 attack lines from the adversarial file one by one through runHook, using real gitleaks 8.24.2 and scratch checkouts with temporary HOME and state. At 9f9533aa all 29 were allow. At the head all 29 were deny: 20 with "gitleaks found a secret", 9 with "can't tell what this command line commits".

No regression (measured: ordinary lines through the hook in a clean scratch checkout, base vs head)

These are still allowed at the head:

  • git commit -m wip and git commit -s -m "fix: thing"
  • git commit -s -m "$(cat <<'EOF' … EOF)"
  • git add -A && git commit -s -m "$(cat <<'EOF' … EOF)"
  • git add a b && git commit, and git add -A && git commit -m x && git push -u origin HEAD
  • git -C <worktree> commit and git -C <worktree> add -A && git -C <worktree> commit from the root
  • cd <worktree> && git add f && git commit
  • git commit -am x, git commit --amend --no-edit, and git commit -m x 2>&1 | tail -3
  • git status && git add -A && git commit, and git add -A; git commit -m x; git log --oneline -1
  • gh pr create and gh pr comment with a quoted-delimiter heredoc body that mentions git commit -a or --amend
  • git log --grep=commit, git merge, git cherry-pick, and cat <<'EOF' > notes.md with commit text in the body

New refusals (allowed at base, refused at head) and whether ordinary work hits them:

Line Ordinary? Judgement
git diff --stat && git commit -am x, git diff --cached --stat && git commit -m x, git log --oneline -3 && git commit -m x yes, occasionally A cost: read-only git before the commit is refused. Only status, rev-parse and ls-files count as read-only. Recommended: add diff, log and show without --output or --ext-diff. Not required.
npm run build && git add -A && git commit, npx prettier --write src && git commit -am x yes Correct by design: the scan runs before the program writes anything.
git fetch && git rebase origin/main && git commit, git stash && git commit rare Correct by design.
cd <worktree> && git lfs pull, and any git subcommand that isn't built in, in a line with cd yes, in LFS repos Over-refusal: the line has no commit at all, and the reason tells the agent to "stage … then run a plain git commit", which doesn't fit. Recommended: look the alias up in each candidate directory, or apply this check only when the line has a commit.
gh pr create --body "$(cat <<EOF … commit … EOF)" (unquoted delimiter) rare (agents quote 'EOF') Over-refusal of a line with no commit. Recommended, not required.
cd ~/x && git commit -m x rare A cost.

Measured, no loosening: over a 229-line corpus taken from the five related test files, main's gitCommit scanned one line that the head's commitPlan returns null for. That line is a quoted heredoc fed to cat, which doesn't commit. Nothing that commits went from scanned to unscanned. People's sessions are still not scanned (measured by the adversarial test's last case).

Weakening gate and locks

  • test/review.test.ts:
    • Measured (word diff): the only change is one added token, 'src/guard/commit.ts', in GUARD_TREE. Nothing is removed. It is the only changed existing test line in the PR.
    • Measured: test/hook.test.ts is untouched.
    • Measured (grep): the removed gitCommit export from src/hook.ts had no test or other caller.
  • Guard lock tests:
    • Measured: review.test tests matching "guard" pass 8 of 8, including the pinned tree, the guard process's loaded modules, and "the guard's own list (src/hook-imports.ts) is exactly the tree's modules no other glob locks".
    • Measured (src/review.ts:365): src/guard/commit.ts is locked through src/guard/**, so it is correctly left out of HOOK_IMPORTS.
  • Measured: the related tests pass at the head, 78 of 78: install, review, hook, push-hook-guard, docs-guardrails and changelog.

Protection

  • Measured: the PR is marked protected: the title has [PROTECTED: owner merges], **Risk class:** protected is set, it has the needs-owner label, and it has "What this can never do" and "Compromised component" sections.
  • Measured: the first "can never do" claim is false today: some commits get through on a scan of less than they record (see check 1).
  • Measured: two Adversarial-pass claims are false for some inputs (private). → Required change 4.
  • Measured: the commit is signed off, and neither the commit nor the PR body has an attribution line.
  • Inferred: tests and scripts use temporary HOME and state through test/isolate.ts and exampleProject, and nothing reaches real state.

Checks

  • Measured: npm run typecheck is clean, and npm run check:denylist reports 1418 files clean.

  • Measured (npm pack --dry-run --json --ignore-scripts, base then head):

    • tarball 1,484,470 → 1,493,763 (+9,293);
    • unpacked 4,971,214 → 5,006,733 (+35,519).

    This matches the builder's figures and stays within the budget.

  • CI at the head is red, caused by this PR (measured). Shard 4/4 fails on Node 22 and 24 in test/change-files-renames.test.ts › "one helper is the only way src/ lists changed names or counts; scripts/ only on this keyed list". It reports two files as not on the list: src/guard/commit.ts (raw 3, ls-files 2, whatchanged 1, porcelain 1) and src/scan/secrets.ts (worktree changes 1, ls-files 2). I reproduced it locally: it fails at the head (# fail 1) and passes at 9f9533aa (# pass 1). I didn't rerun CI.

Merge-tree (measured, git merge-tree --write-tree)

Required changes

  1. Close the further commit forms the guard still lets through unscanned. The orchestrator has the list privately under the Public adversarial suite in CI: the merge gate refuses every hostile PR case (prompt and delimiter injection, rename and copy tricks, poisoned .claude settings and hooks, credential smuggling), with the existing guard tests folded in #707 rule. Each fix needs an attack test in test/adversarial/commit-scan.test.ts that fails at 9f9533aa. Fix the class rather than each instance, so the model refuses, by design, any input it can't fully model.
  2. Fix the checkout coverage gaps sent privately. Under principle 1, refuse whenever a plan input can't be determined. Each fix needs a test.
  3. Make CI green. Prefer changing the code so it doesn't trip change-files-renames where the words aren't git reads: the GIT_BUILTINS string, the COMMIT_LONG keys and the raw names. If any OUTSIDE_THE_HELPER entry is added, for example for the untracked-file listing in src/scan/secrets.ts, that is a loosening (principle 9). The PR body must then name the check, say why its finding is wrong for that file, and send it for an independent review.
  4. Correct the PR body. Fix the two Adversarial-pass claims that measured false, and the first "can never do" claim, until required changes 1 and 2 land. Also add to "What remains", in general terms, that a commit made by a program the line runs is outside the model, as on main.

Recommended (not required): the two over-refusals in the regression table, cd <dir> && git <non-built-in subcommand> and the unquoted-heredoc mention. Also consider allowing read-only git diff, git log and git show before a commit.

Review round 2 for #971 (#979). The commit scan now covers every commit
form and checkout the guard allows in these further ways, each failing
closed:

- Every place a commit may run must resolve to a checkout the scan reads;
  a lookup that fails or finds none refuses the line, and the hook never
  treats an empty plan as clean.
- A directory change is followed only where the shell's answer and the
  model's can't differ; any other is refused.
- No shell evaluation inside a git command's words: a subcommand that
  isn't a literal word, a word the shell could turn into others, or git
  run by another program without a literal subcommand, is refused.
- The scan reads what the commit records (objects by id, files from
  disk), with the changed paths from the change reader
  (src/coordinator/change-files.ts), never a diff git renders. SizeFile
  moves next to that reader so the guard's tree grows by that one module.

Ordinary work keeps going: read-only git before a commit, other git
commands in a line that changes directory, and here-document bodies that
only hold text.

Test (fails without): test/adversarial/commit-scan-routes.test.ts, 6 of
its 7 tests fail at a40dafc (the 7th keeps plain commits as they were).

Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
@jayden1711

Copy link
Copy Markdown
Owner Author

Evaluator verdict: CHANGES NEEDED (round 2/2, delta final check)

Head checked: 9c6761f8 (round 2, with origin/main 40ef312a merged in). Previous reviewed head: a40dafc7.

Every finding from both reviews of a40dafc7 is fixed. But the new content scan reads less than main does in three ways. In each one, main refuses a commit that this head lets through, and the commit then records a secret. That reopens routes main had closed (decision guide 3).

Under the #707 rule, this comment gives verdicts by finding ID only. The orchestrator has the details and the probe harness.

I ran each probe through the hook entry with real gitleaks 8.24.2. I used scratch checkouts, a test token, a temporary HOME, state and TMPDIR, and a scratch git config with no system config. Each line ran against this head and against a build of main at 40ef312a. Where the hook allowed a line, I ran it in bash and checked whether the secret reached HEAD.

Findings from the previous round

ID Verdict
R1 (second review) Fixed, measured. Refused at the head; refused on main.
R2: A3, A4, A5 Fixed, measured. Refused at the head; allowed on main.
R3: A1, A2 Fixed for the named forms, measured. But the class stays open: see D4.
R4: A7, A11 Fixed, measured. The scan finds the secret under each setting. The new reading added D1–D3.
A6 Not fixed, the same as main (measured). It is a disclosed residual in "What remains".
A10, A12 Measured: refused, or the secret is found.
R1a, R1b Fixed, measured. Every line is refused at the head; on main each one is allowed and commits the secret.
R2a Fixed, measured. Refused, or scanned in the right checkout.
R2b Fixed, measured (commitPlan with failing lookups). Each of these refuses: an empty answer, a timeout, a relative answer, an alias lookup that fails other than "no such key", and an alias loop.
Final check: CI Fixed, measured. CI is green. No list entry was added to the change-reader lint.
Final check: recommendations Taken, measured. Read-only diff, log and show before a commit are now allowed, and so are other git commands after a cd and unquoted here-document bodies.

The builder's claims against the code

  • Lookups fail closed: true (measured, see R2b). Also, src/hook.ts refuses an empty plan.
  • What changeDir refuses: true (measured). It refuses options, .., links, missing targets, more than one argument and CDPATH. It allows only one literal, existing directory. Inferred, not required: it reads CDPATH from the hook's environment, not from the shell's.
  • Expansions or globs in git's words, and a non-literal subcommand, are refused: true for the words after git.
    • Measured: it is false for other spellings that reach git (D4).
    • Measured: the exempt list of programs is not closed (D5).
  • The scan reads content by object id or from disk: true (measured). It reads less than main in D1–D3.
  • GUARD_TREE gained one module (src/coordinator/change-files.ts): true, and it only tightens (measured).
    • The round-2 diff of test/review.test.ts adds one token.
    • The guard-tree tests pass 8 of 8, among them "every module in the tree is self-modification, L3".
  • SizeFile moved: true (measured). It is a type only, re-exported from src/pr-rules.ts. pr-rules.test passes 8 of 8.

New findings

ID What Against main
D1 Regression. The scan keeps only the lines that appear nowhere in the file's earlier version, so lines a secret needs as context can be dropped. A multi-line secret was committed in two ordinary agent calls, both allowed by the hook. Main refused (measured)
D2 Regression. Whether the scanner reads a file at all depends on content the agent controls. A secret was committed in one call, both from staged changes and from working-tree changes. Main refused (measured)
D3 Regression. The working-tree route reads something other than what git stores. With no program involved, a secret was committed. Main refused (measured)
D4 The class closed by R1b, A1 and A2 is still open in other spellings. Each one commits the secret. Same as main (measured)
D5 The list of programs that "never run their arguments" includes programs that do. One of them committed the secret. Same as main (measured)
D6 The PR body says a .gitleaksignore is not read. A .gitleaksignore that is part of the commit still suppresses the scan. One kept only on disk no longer does. Better than main for the on-disk file; the same for the committed one (measured)
D7 Programs that the checkout's config runs during a read git now allows before a commit stage content after the scan. Main allowed two of these forms. It refused the third only because git's warning broke the scanner; with a well-formed response the scanner runs clean on main. Not a regression (measured, with the end-to-end commit on main inferred)

Scanner suppressions (#980), measured. The head is the same as main for inline allow comments and for the checkout's .gitleaks.toml. That covers a .gitleaks.toml committed earlier, one staged in the same commit and one only on disk: the head reads it from disk at the checkout's top level, as main did. The head is slightly better for .gitleaksignore (D6). Not required here: gitleaks 8.24.2 has a flag that turns off inline allow comments, for #980.

The exempt program list (D5). Inferred from the tools' documented options: the other listed programs don't run their arguments as commands. The list is not closed while D5's entries stay on it.

Fail-without (method b)

  • Measured. I wrote test/adversarial/commit-scan-routes.test.ts as a new file into a worktree at a40dafc7 and checked it is byte-identical (sha256 bb37e0d0…). There: # pass 1, # fail 6. Each failure is an actual: 'allow', apart from the ordinary-work test, which got actual: 'deny' at a40dafc7. The passing test is "keeps plain commits as they were".
  • Measured. At the head: commit-scan-routes, commit-scan and commit-plan pass 23 of 23.

Weakening gate

These are the changed existing test lines in round 2. None loosens what is refused (inferred):

  • test/adversarial/commit-scan.test.ts: the refusal pattern now also accepts the new "can't read this command line" refusal. Both are refusals.
  • test/commit-plan.test.ts: the checkout assertions moved from made-up paths to real temporary folders. Every earlier refusal is kept, and more are added.
  • test/review.test.ts: one module added to GUARD_TREE.

Checks

Required changes

  1. D1: read a file's added lines the way a diff's hunks place them, not as lines unseen anywhere in the earlier version. Add a test, failing at 9c6761f8, for each shape sent privately.

  2. D2: make sure the content can't decide whether a file is scanned. Add a test for each shape sent privately.

  3. D3: for working-tree content, read what git would store, or refuse when git would store something else (principle 1). Add a test.

  4. D4: close the class, not the instances, for each form sent privately, and add a test for each. If you judge the cost to ordinary work too high, put that cost to the owner rather than leaving the class open.

  5. D5: take the programs that can run commands off the exempt list, or narrow the claim. Add a test.

  6. PR body: correct three things.

    • "Reopen a route main had closed": it is false until 1–3 land.
    • "A line already in the earlier version is no new content": D1 shows otherwise.
    • "A .gitleaksignore is not read": D6 shows otherwise. Fix it, or state it.

    Also widen "What remains" (D7): programs the checkout's config runs during the reads allowed before a commit, and during the scan's own change listing.

Review round 3 for #971 (#979). The commit scan covers every commit form
and checkout the guard allows; this round makes what it reads match what
the commit records, and closes the class of lines that run a program the
model can't name:

- Added lines come from a line diff computed in process (a shortest edit
  script), so no config or attribute of the checkout can change it, and a
  line counts where it is added even if the file had it elsewhere.
- Nothing in the content decides whether the scanner reads a file.
- A working-tree file that git would store as other bytes refuses the
  commit; the scan never runs a filter.
- The commit's own files never configure the scanner.
- Every word in command position must be literal, after wrappers too; no
  wrapper may build a command from its input; no shell may read its
  script from a pipe, a here-document or an expanded string.
- The list of programs treated as never running another program keeps
  only programs with no option that runs one.

Test (fails without): test/adversarial/commit-scan-content.test.ts, all 7
of its tests fail at 9c6761f.

Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
@jayden1711

Copy link
Copy Markdown
Owner Author

Second review (verification-scoped, after a classifier stop): CHANGES NEEDED on 1a731dc

Scope. Under the owner's standing rule, this rerun did three things only:

  • re-ran the documented probes, unchanged;
  • re-ran the PR's fail-without tests;
  • checked the claims against the code.

It built no new payloads.

Setup. Main is b3176d3. Every run used scratch checkouts and the test token only, with a temp HOME and state, a scratch git config and real gitleaks 8.24.2. Probes are named by group and finding ID only (#707).

Documented probes (measured, head and main)

Probe group Lines Head refused or found Main refused or found Head allows, main refused
Second review harness (A-series, R1) 26 24 7 none
Final check, holes, default 14 14 1 none
Final check, holes, force 3 3 0 none
Final check, holes, alias 3 1 0 none
Final check, cdp 6 6 0 none
Delta: scan (S-series) 23 21 18 none
Delta: suppress (X-series) 7 3 1 none
Delta: progs (P-series) 12 11 4 none
Delta: magic (M-series) 3 3 2 none
Delta: extra (W-series) 9 9 1 none
Delta: fsm (F1-F3s) 6 0 6 F1, F1s, F2, F2s, F3, F3s
Delta: ordinary (O-series) 17 10 1 O3
Delta: commit model unit (R2b, directory changes) 29 20 refuse, 9 scan n/a (module not on main) n/a
Delta: harness copy (h2) 24 23 7 none

Notes on the table:

  • Allowed on both trees.
    • Second review harness: A6, a hook in the checkout's own hook folder (disclosed), and A7b's config step, which is not a commit.
    • Holes, alias: two lines, which git itself rejects, so nothing is committed.
    • Scan: S1c-1 (the clean control) and S5.
    • Suppress: X1-X4 (secret scans: ignore suppressions an agent can write #980, same as main).
    • Progs: P0, which is a setup step, not a commit.
    • Ordinary: O1 and O2 (disclosed), and O13-O16, which are ordinary work.
  • The two flagged rows. F1-F3s and O3 are let through at the head, and running each one commits the test token. Main refuses all seven because the scanner could not run.
  • Scanner under the file-system monitor (f). This probe doesn't load either tree.
    • When the monitor program's response is malformed, the scanner exits 1 on git's warning.
    • When the response is well-formed (v1 or v2), the scanner exits 0.

Fail-without (measured)

  • test/adversarial/commit-scan-routes.test.ts was written as a new file at a40dafc (cmp: byte-identical) and run there: pass 1, fail 6. This matches the body; the passing test is the keep-plain-commits control.
  • test/adversarial/commit-scan-content.test.ts was written as a new file at 9c6761f and run there: pass 0, fail 7. This matches the body.
  • At the head, the PR's five test files pass 52/52, with 0 skipped. npm run typecheck exits 0.

Claims against the code

What this can never do

  1. Nothing scanned: true.
    • Measured: routes test 1, and commit-model unit lines L1-L3.
    • Code: src/hook.ts refuses a refused plan and an empty plan. A thrown error fails closed with exit 2.
  2. Display settings hide nothing: true.
    • Measured: A7, A11 and routes test 6.
    • Code: blobs come from the batch object reader and files from disk. The raw listing is used only for paths.
  3. Doesn't change the index or files: true for the scan's own writes (code: one temp dir, removed in finally).
    • Unscoped: the change listing doesn't turn off git's optional locks, so git may refresh the index's stat cache. That is inferred, involves no content change, and is not required.
  4. Doesn't reopen a route main had closed: false as worded (measured).
    • F1, F1s, F2, F2s, F3, F3s and O3 are documented lines that the reviews measured as refused on main. At this head each one is allowed and commits the token.
    • Sub-claims:
      • The line-diff placement is true (content test 1; addedLines).
      • "No .gitleaksignore is honoured" is true (X5 and X6 are found; the code passes an empty ignore path and renames a staged root ignore file).
      • ".gitleaks.toml and inline allows are honoured as on main" is true (X1-X4 are allowed on both).
  5. Hooks and filters are not promised: true. A6 is allowed and commits on both trees. Working-tree filters are now refused (S10).

Compromised component

  • Agent user: true for the forms the probes cover (measured). The config-run programs are disclosed under What remains.
  • Locks: true (code). src/review.ts locks src/guard/** with the tool guard and src/coordinator/** as the coordinator; both entries are already on main.
  • Coordinator unchanged: true (code). The SizeFile type moves into src/coordinator/change-files.ts, and src/pr-rules.ts re-exports it.
  • Dashboard, other instances, hub viewer: unscoped. The diff adds no route to them.
  • Push access: true (code; the same lock entries).
  • Scanned checkout's content: true (code).
    • The raw listing renders no patch, the object reads apply no filter, and the attribute check runs nothing.
    • The exception the claim names, git's change listing, is where the monitor program runs. That is measured by the fsm group.

What remains

  1. Programs the line runs: true. This is a scope statement.
  2. Hooks and filters set up earlier: true. A6 was measured on both trees.
  3. Config-named programs, "measured on this head and on main for the forms the reviewers tried":
    • The head part is true: O1, O2, O3 and F1-F3s were measured.
    • The main part is false for the file-system-monitor forms: main refuses O3 and F1-F3s (measured).
    • That main could be passed with a well-formed monitor response is inferred. The f probe measured the scanner's exit status, not a commit.
  4. Out-of-band changes: unscoped. It is labelled inferred, as stated.
  5. Commits made by other git commands: unscoped. This is a scope statement.
  6. Scanner rules and suppressions: true. X1-X4 behave as on main (secret scans: ignore suppressions an agent can write #980).
  7. docs/guardrails.md label: true (docs diff).

Required changes

  1. "What this can never do", the bullet on reopening routes. Name the exception:
    • The file-system-monitor forms (fsm group, O3) are refused on main only because git's warning breaks main's scanner, and this head lets them through.
    • Label main's exposure to a well-formed response as inferred.
  2. "What remains", bullet 3. Say it is measured on main for the diff-program forms (O1, O2). For the file-system-monitor forms, main refuses them, and main's exposure is inferred.

Settling by the decision guide. Both changes are body corrections (principle 5).

If the orchestrator treats main's refusal as a closure under principle 3, the fix is code, not words. The commit scan would refuse to proceed in a checkout whose config names a file-system-monitor program, failing closed as main effectively does. That follows principles 1 and 3, so it needs no owner decision.

The round-2 final check called this "not a regression" (D7). That call rests on an end-to-end commit on main that was inferred and never run. A verification-scoped rerun may not build that payload, so this review leaves the call to the orchestrator.

Everything else checks out.

  • Every other documented probe is refused or found at the head wherever main refused it.
  • Both fail-withouts reproduce.
  • The head's tests and typecheck pass.

@jayden1711

Copy link
Copy Markdown
Owner Author

Evaluator verdict: CHANGES NEEDED (round 3, delta final check)

Head checked: 1a731dc0 (round 3, with origin/main b3176d3e merged in). Previous reviewed head: 9c6761f8.

Round 3 closes every route the round-2 check named. D2 holds. D7 is fixed. But three new ways appeared in which main refuses a commit and this head lets it through with the secret recorded (decision guide 3). The D4 and D5 claims are also still false.

Under the #707 rule, this comment gives verdicts by finding ID only. The orchestrator has the details and the probe harness.

How I tested:

  • I ran each probe through the hook entry with real gitleaks 8.24.2 and git 2.43.0.
  • I used scratch checkouts, a test token and a fake key, a temporary HOME, state and TMPDIR, and a scratch git config with no system config.
  • I ran each probe against this head and against a build of main at b3176d3e.
  • Where the hook allowed a probe, I ran it in bash and checked whether the secret reached HEAD.

Round-2 findings

ID Verdict
R1–R4, A1–A12, R1a, R1b, R2a, R2b Still fixed, measured. I re-ran the earlier reviews' probes and the round-2 probes. The results match round 2. A6 is the disclosed residual.
D1 Partly fixed. Measured: every round-2 shape is refused here; main refuses them too. Inferred from the code: the in-process diff ignores config. But measured: it places some runs differently from git and scans less than main. See E1.
D2 Fixed, measured. Every file-type shape is refused. File-type signatures placed after the prefix, at offsets up to about 4 MB, are all scanned, and so are null bytes; no finding is lost. Files skipped by path rules are skipped the same on main (measured), so that belongs to #980.
D3 Fixed for filter and encoding attributes, measured. That holds from every source I tried: subfolder .gitattributes, info/attributes, core.attributesFile, macros, and the index. Attributes or config given on the command line, in the environment or from another tree are refused. Main let each of those commit the secret. Not fixed for ident: see E3. Inferred from the code, and measured for a CRLF secret: text/eol conversions hide nothing.
D4 Partly fixed, measured. Every round-2 shape is refused, along with several new spellings. Other members of the class still commit the secret, the same as on main: see E4. The builder chose to close the class, so the claim that it is closed is false.
D5 Partly fixed, measured. With the round-2 entries removed, their shapes are refused. Three entries left on the list can still run a program: see E5.
D6 Fixed for the round-2 shapes, measured. An ignore list staged with the secret, or kept only on disk, no longer suppresses the scan. But the rename that does this adds a regression: see E2.
D7 Fixed (wording). "What remains" now names the programs that config runs during the reads allowed before a commit and during the scan's own change listing.
Body claims (round 2) Fixed: "a line already in the earlier version", and ".gitleaksignore not read". Still false: "reopen a route main had closed", because of E1–E3.

New findings

ID What Against main
E1 Regression (D1). The in-process diff places a run of added lines in a different spot from git's default diff, so lines a secret needs are left out. The shapes are sent privately. A secret was committed this way from staged changes and from working-tree changes. Two related shapes fail the same way on head and main. Any scan that reads only the hunks misses them. Main refused (measured)
E2 Regression (D6). Renaming the commit's .gitleaksignore can collide with another file in the same commit. The secret file is then replaced before the scan, and the secret is committed. Main refused (measured)
E3 Regression (D3). Under the ident attribute, git stores different bytes from the working-tree file. The disk form scans clean; the stored form matches a default rule. A secret was committed from a tracked file. With an untracked file it is the same as main. Main refused (measured)
E4 D4 class still open. Each of these commits the secret, measured on head and main: a literal command name re-pointed by a shell builtin; command substitution evaluated inside quoted text by a builtin; a string a builtin runs later; programs that run a literal string as a shell command; and a shell reading its script from a pipe through a file operand or from a process substitution. The last one contradicts the body's "no shell may read its script from a pipe". Same as main (measured)
E5 D5 list. Three exempt programs can run a program. One makes a later literal name run git. Two evaluate a command substitution through an option. Each committed the secret. Same as main (measured)
E6 Unreported cost. Two common kinds of ordinary line are now refused, and neither is in the cost list. Both are matching bugs, not part of the class: (a) [/[[ test commands in any line, read as an expanded command name; (b) a program run by xargs whose own option contains i, I or J, read as an xargs option. 9c6761f8 allowed both. Over-refusal (measured)
E7 Undisclosed loosening (principle 9). The set of programs allowed before a commit in the same line gained five read-only programs. Writes through them are still refused (measured), and I judge the change harmless (inferred). The body must name it and say why. The new test's last case fails at 9c6761f8 because of this change, not a bug. Needs disclosure

Cost to ordinary work (D4's report). The intended refusals are: an expanded program name (an editor variable, a tool under an expanded path, a substituted path), piping a script into a shell, a here-document fed to a shell, an expanded -c script, and xargs with a replace string. Each has a literal spelling, and the refusal says so. I judge this cost acceptable (inferred). Expanded tool paths are probably the most common case (guess). E6 is not intended cost: it is a bug to fix, not a cost to take to the owner.

D1's work budget (measured).

  • Past the budget, every middle line of the file counts as added, so more is scanned, never less.
  • Through the hook, large changes took 0.6–1.3 s, against 0.5–0.7 s on main.
  • The slowest shape I could build is about 100 MB of long lines that differ only at the end. Its diff alone took 5.8 s.
  • Past 10 s, the hook's deadline refuses (src/hook-deadline.ts), so a stall fails closed.

Fail-without (method b)

  • Measured. I wrote test/adversarial/commit-scan-content.test.ts as a new file into a worktree at 9c6761f8 and checked it is byte-identical (sha256 673a8f0b…). There: # pass 0, # fail 7.
    • Tests 1–6 fail for the reasons the findings describe: the diff function is missing, or the result is actual: 'allow'.
    • Test 7 (ordinary work) fails with actual: 'deny'. That is E7, not a bug.
  • Measured. At the head, the four commit-scan test files pass 30 of 30.

Weakening gate

Measured: round 3 changes no existing test line. The only test change is the new file.

Checks

Required changes

  1. E1: when you pick the added lines, cover every placement a diff could choose. One way is to widen each run of added lines over the unchanged lines it could slide across. That also closes the two shapes that fail the same way on main. Add a test for each shape sent privately, failing at 1a731dc0.
  2. E2: hand the scanner the commit's files so no path can collide. For example, put every file under a fixed subfolder of the scan root. Measured: the scanner reads an ignore list only at the root. Add a test.
  3. E3: refuse working-tree files with the ident attribute too, or read what git would store. Add tests for a tracked and an untracked file.
  4. E4: close each member sent privately and add a test for each. Or, if closing one costs too much ordinary work, list it in "What remains" with that cost so the owner can decide. Either way, correct the D4 claims in the commit message and the body.
  5. E5: take the three programs off the exempt list, or narrow the claim. Add a test. For two of them, taking them off the list doesn't close the matching E4 member.
  6. E6: fix both over-refusals. Read [ and [[ as literal names. Check only xargs's own options. Add tests that these lines are allowed. Then update the cost list.
  7. E7: name the widened read-only set in the body as a loosening and say why it is safe.
  8. PR body: the "reopen a route main had closed" claim stays false until 1–3 land.

Review round 4 for #971 (#979). The commit scan covers every commit form
and checkout the guard allows; this round:

- Scans every placement a line diff could give each run of added lines:
  a run is widened across the unchanged lines it could slide over.
- Writes the commit's files under a fixed folder below the scan's root,
  so no name of theirs can be a file the scanner reads at its root.
- Refuses a working-tree file with an ident attribute, as it already did
  for filter and encoding attributes.
- Refuses a commit in a checkout whose effective config names a program
  git runs while reading changes, in the reads allowed before a commit or
  in the scan's own listing (fail closed).
- Keeps on the exempt list only programs that can't run another one.
- Reads the test command and xargs's own options as what they are, so
  ordinary test commands and xargs runs are not refused.

Test (fails without): test/adversarial/commit-scan-reads.test.ts, all 6
of its tests fail at 1a731dc.

Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
@jayden1711

Copy link
Copy Markdown
Owner Author

Second review (verification-scoped, after a classifier stop)

Verdict: CHANGES NEEDED on 1e210a9.

Scope, per the owner's standing rule: the documented attacks and the PR's fail-without tests were re-run, and the claims were checked against the code by reading only. No new payloads and no new attack forms. Refusals: none.

1. Documented attacks: head vs origin/main cec056a (measured)

Scratch checkouts, a test token, a fake key, temp HOME and temp git config. The groups were run directly and again through the adapted wrapper, with identical results.

Group Head Main
r3 diff SL1, SL1w, SL2 found; SL4, SL4w found; SL3 allowed (not executed) SL1, SL1w, SL2 allowed and committed; SL4, SL4w found; SL3 allowed
r3 budget BG1–BG3 found, 0.5–1.1 s found, 0.4–0.5 s
r3 paths PA1–PA5 allowed (the scanner's default path rules); CL1, CL1c, CL2 found same
r3 attrs AT1–AT8 refused; AT4s, AT9, AT10 found AT4u, AT5–AT8 allowed and committed; AT9 could not run; AT10 allowed
r3 progs DS2, HS, SO2, EV, EV2, CO, BI, XS, TS refused. H1, H2, T1–T5, TR, SC, FL, DS, PS, SO, FX allowed and committed; AL allowed, no commit every head-allowed ID also allowed and committed; DS2, HS, SO2, EV, EV2, CO allowed too
r3 keywords K1–K7 refused; K8–K14 allowed, no commit K1, K3, K6 allowed and committed
r3 unread U1–U8, U10, U12 refused; U9, U11 allowed and committed U9, U11 the same; most of the others allowed and committed
r2 scan all found or refused, except S1c-1 and S5, which add no secret S2, S7 allowed; S11p allowed and committed
r2 suppress X1–X4 allowed (the checkout's own suppressions, #980); X5–X7 found X1–X6 allowed
r2 progs P1–P11 refused P1, P3–P6 allowed and committed
r2 magic M1–M3 found M3 allowed and committed
r2 extra W1–W8 refused; W9 found W1–W7 allowed and committed
r2 fsm F1–F3s refused refused
r2 ordinary O1–O12 and O17 refused; O13–O16 allowed (not executed) O1, O2 allowed and committed; O4–O17 allowed
t (diff unit) placement widened; slowest case 4.8 s (2k lines × 50k chars); the interleaved case trips the budget and marks everything as added n/a (no in-process diff)
cost, cost2, ro expanded program names refused in any line (disclosed), xargs with a replace string refused, the rest allowed; writes before a commit refused n/a (no line model)
g, g2 (the scanner alone) default path rules skip some names and extensions; deep file signatures don't stop reading; ident disk forms aren't matched same binary

Lines the head allows that main refused: none (measured). Still open at both: H1, H2, T1–T5, TR, SC, FL, DS, PS, SO, FX, U9, U11. These are the shell-evaluation class the PR holds for #983.

2. Fail-without (measured)

  • commit-scan-reads.test.ts, written as a new file (sha256 5419da3e…, byte-identical) at 1a731dc: pass 0, fail 6. All six are assertion failures.
  • commit-scan-content.test.ts, written as a new file (sha256 e45f80d4…, byte-identical) at 9c6761f: pass 0, fail 7. All seven are assertion failures.
  • At the head, the four commit-scan adversarial files plus commit-plan.test.ts give 36 pass, 0 fail. Typecheck is clean.

3. Claims vs code (all inferred, by reading src/scan/secrets.ts, src/guard/commit.ts and the change reader)

(a) Scan folder vs the checkout's own .gitleaks.toml paths: false, a regression.

  • Main runs the scanner's git mode in the checkout, so rule path and allowlist paths patterns see repo-relative paths.
  • The head writes every file under the fixed commit/ folder and scans that folder's parent. So every path the scanner matches starts with commit/. The findings mapping strips that prefix (secrets.ts:90, :99, :118).
  • Result for a ^-anchored pattern:
    • An allowlist paths entry no longer matches. That is an over-refusal (fail closed).
    • A path-scoped detection rule (path = '^…', with or without a regex) no longer fires. That is a miss where main found the file.
  • So "the checkout's .gitleaks.toml … honoured as on main" is false, and an existing install with such a rule loses a refusal main gave (principle 3).

(b) Signature-verification programs through log/show: unscoped.

  • git log and git show are allowed before a commit (commit.ts:526). Their risky-option check covers only output, ext-diff and textconv (:548–552). So a signature-display option passes.
  • noReadPrograms (secrets.ts:350–378) checks only fsmonitor, diff.external, diff drivers and filters. It does not check log.showSignature or gpg.*program.
  • A checkout-configured verification program therefore runs during an allowed read, after the scan.
  • The round-4 bullet ("programs the checkout's config names that git runs while reading changes are refused") doesn't cover this. The "signing program" it lists as open is the one that runs during the commit, not during reads.

(c) A submodule's own config: unscoped.

  • noReadPrograms reads the top checkout's config --list only.
  • An allowed read such as status or diff recurses into submodules under that submodule's own config. So does the scan's own working-tree listing, which forces --ignore-submodules=none (change-files.ts:45–55).
  • That config's fsmonitor or filters are never checked. The Compromised-component exception for "the change listing" technically covers the scan's side. The round-4 "refused" bullet doesn't.

(d) Reads with -C in another checkout: unscoped.

  • beforeRefusal accepts a read's own -C directories through gitWords (commit.ts:439–442, :936).
  • Only the commit's directories become scan tops (:963–969), so noReadPrograms never reads the other checkout's config.
  • Its configured programs run during the allowed read, after the scan.

Other claims:

Claim Label
Never: nothing scanned (every place resolves to a top; the hook denies an empty plan, hook.ts:168) true
Never: display settings hide content (reads objects and files; change list with --no-textconv/--no-ext-diff, attributes file off) true
Never: changes the index or files true for content. git diff may rewrite stat info in the index; harmless
Never: reopens a closed route, as far as measured true for the documented probes (above); see (a)
Never: can't promise program-added content true
Compromised: agent user can't pass by "a git display setting the guard allows" unscoped: (b), (d), and (c) through reads
Compromised: agent can't widen the model (src/guard/** and src/coordinator/** locked, review.ts:378, :432) true
Compromised: coordinator, dashboard, push access true
Compromised: scanned checkout runs no configured program except for the change listing and object reads true as worded; the listing reaches submodule config (c)
Remains: programs the line runs; programs git runs during a commit; race; other git commands; docs label "inferred" (guardrails.md:187) true
Remains: config-run read programs refused since round 4 unscoped: (b), (c), (d)
Remains: shell features that turn data into a command, same as main true for T1–T5, TR, SC, FL, DS, PS, SO, FX, U11 (measured same as main). H1/H2 (a builtin that remaps a command name) fit none of the listed kinds: unscoped, add it to the list
Remains: "decides what is scanned, not how the scanner judges it" unscoped: see (a); the folder changes how the checkout's own path rules judge

Required changes

  1. (a) Make the checkout's own path rules see repo-relative paths again, or fail closed when its .gitleaks.toml has any rule path or allowlist paths. Over-refusal is acceptable; a miss is not. Keep the CL1 and scan-folder protections. Add a test with an anchored path-scoped rule that is found on main and fails without the fix.

  2. (b)–(d) Either refuse them (preferred, principle 1):

    • a signature-display option or config on an allowed read;
    • -C on a read before a commit, or check those directories' config too;
    • submodule configs, read or refused when a submodule is checked out.

    Or correct the round-4 "What remains" bullet and the Agent-user line so they name these as open. Each one closed gets a fail-without test.

  3. Add the command-remapping builtin (H1/H2) to the "What remains" list of shell features.

The fixes follow the decision guide (fail closed, no regression, correct the claim), so no owner decision is needed unless a fix adds a new over-refusal cost beyond these.

Review round 5 for #971 (#979). The commit scan covers every commit form
and checkout the guard allows; this round:

- Scans each changed file whole, after and before the commit, and counts
  a secret as new when the file holds it more often after. No placement
  a diff could choose changes what is found; the in-process line diff and
  its widening are gone.
- Writes each file at its own path, so the checkout's own path rules see
  the paths main's scan saw; a file named like the scanner's own settings
  is scanned apart, as content.
- Reads xargs's options and bracket patterns as the shell does in the
  remaining spellings.
- Refuses config programs exactly where git runs them: a monitor program
  always, diff programs only when the line shows diffs, a signature
  program when it shows signatures; git's own monitor daemon is allowed.
  Submodules' own config and other checkouts the line reads are checked.
- Says why, and what to do, when a filter its files use refuses a commit.

Test (fails without): test/adversarial/commit-scan-findings.test.ts, 8 of
its 10 tests fail at 1e210a9 (the others keep a fixture-holding file
committable and guard round 4's exempt list).

Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Round 5b for #971 (#979), by the owner's decision (2026-10-21): a
checkout whose large-file filter is exactly the standard git-lfs one, as
its installer writes it, may commit. The scan reads each large file's own
bytes: from the working tree, or from the checkout's large-file store for
a stored pointer (a pointer whose object is missing refuses). Any other
value for those keys, any other filter, or a git-lfs on the path that is
not a regular file still refuses.

This is a narrow, owner-approved loosening of the filter refusal added in
round 4.

Test (fails without): test/adversarial/commit-scan-lfs.test.ts, the two
tests of the loosening fail at 1d1966f; the three refusal tests pass
there and guard it.

Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
@jayden1711

Copy link
Copy Markdown
Owner Author

Second review (verification-scoped, after a classifier stop)

Verdict: CHANGES NEEDED on ce7d4ab.

Scope: this run repeated the documented attacks and the PR's fail-without tests, and checked the claims by reading the code. It built no new payloads. Where it says "measured", that comes from a command run in this review; "inferred" means read from the code, not run. Probe lines are referred to by group and ID only (#707).

1. Documented attacks, run at the head and at origin/main 1808043 (measured)

Group Head Main Head allows a line main refused?
r4 slide (SL5–SL7, SLH, SLP, staged + working tree) all found all found no
r4 cfg C1–C3b, C5, C6, C9 refused; C4 allowed, no secret in HEAD C1–C3b refused; C4–C6 and C9 allowed (C5 and C9 leak) no
r4 glob GL0–GL4 all refused all allowed (GL1–GL4 leak) no
r4 env EN1–EN6 all refused all allowed, no leak no
r4 gitrun (marker only) same as main same as head n/a
r4 cost K1–K5, K2t all allowed (K1 is now allowed under 5b) all allowed no
r4 allow AL1–AL6 AL1, AL5, AL6 allowed; AL2–AL4 found same no
r4 xargs XA0–XA5 all refused all allowed and leak no
r3 diff SL1–SL4w all found SL1, SL1w, SL2 leak; SL3 allowed; SL4 and SL4w found no
r3 budget BG1–BG3 found (0.8–0.9 s) found no
r3 paths PA1–PA5 allowed; CL1, CL1c, CL2 found same no
r3 attrs AT1–AT10 all refused or found AT4u and AT5–AT8 leak; AT10 allowed no
r3 progs DS2, HS, SO2, EV, EV2, BI, CO, XS, TS refused; H1, H2, T1–T5, TR, SC, FL, DS, PS, SO, FX allowed and leak; AL allowed, no leak everything allowed except BI, XS, TS no (the leaking lines are the shell-evaluation forms listed in What remains, and they leak on main too)
r3 keywords K1–K14 K1–K7 refused; K8–K14 allowed, no leak K2, K4, K5, K7 refused; K1, K3, K6 leak no
r3 unread U1–U12 all refused except U9 and U11 (which leak) U5 and U6 refused; the rest leak no
r3 cost, cost2, ro results as in round 4 (cost refuses only programs named through an expansion, scripts fed to a shell, and xargs with {}; ro refuses every write) n/a: the module doesn't exist on main n/a
r3 t can't run: addedLines was deleted in round 5 n/a n/a
r3 g, g2 (the scanner alone) unchanged same n/a
r2 scan S1–S12 everything refused or found except S1c-1 (no leak) and S5 S2, S7 and S11p allowed (S11p leaks) no
r2 suppress X1–X7 X1–X4 allowed; X5–X7 found X1–X6 allowed no
r2 progs P1–P11 all refused P1 and P3–P6 leak no
r2 magic, extra, fsm, ordinary M1–M3 found; W1–W9, F1–F3s and O1–O12, O17 refused; O13–O16 allowed M3, W1–W7, O1 and O2 leak; O4–O17 allowed no

Result (measured): across all of r2, r3 and r4, the head allows no line that main refused. Every line the head allows and that leaks (r3 progs and unread) is allowed and leaks on main too.

2. Fail-without (measured)

  • commit-scan-findings.test.ts, written as a new file into a worktree at 1e210a9 (sha256 72db0afb…, byte-identical to the head's copy): # pass 2, # fail 8. This matches the PR body.
  • commit-scan-lfs.test.ts, written as a new file into a worktree at 1d1966f (sha256 e664f9cd…, byte-identical): # pass 3, # fail 2. This matches the PR body.
  • At the head, the seven commit-scan files (the six adversarial ones plus commit-plan) give 51 pass, 0 fail, 0 skipped. Real gitleaks was present.
  • npm run typecheck at the head is clean.

3. Required changes

  1. CI is red at this head (measured). In run 39661132215, shard 3/4 fails on both Node 22 and Node 24. The failing test is commit-scan-lfs.test.ts › "a large-file filter that is not exactly the standard one still refuses". The failing case is "not required": 'allow' !== 'deny'. The same file passes locally.
    • Likely cause (inferred): the runner's system git config already carries the standard filter.lfs.* keys, required = true among them. The effective config is then exactly standard, and the code allows it, as the 5b text says it should ("from any config source").
    • So the test's assumption is what breaks, not the code. Make the test independent of the host's git config, and make "not required" a case that is really non-standard in the effective config (for example, an explicit non-true value).
  2. The large-file allowance also covers a filter whose name matches the standard one only when letter case is ignored (inferred by reading; not run, per this review's scope).
    • Git treats a config subsection name (the filter's name) as case-sensitive. But checkReadPrograms (src/scan/secrets.ts:337) and lfsStandard (:384) lowercase the whole key, subsection included, and the refusal check compares a lowercased attribute value (:359). Meanwhile storedAsOnDisk (:309) and lfsContent (:429) compare the attribute value to the standard name exactly.
    • Result: under a filter whose name differs from the standard one only in case, with the standard values, the filter refusal is lifted. Content staged in an earlier call is then scanned as pointer text, not as the stored object. The base 1d1966f refused this.
    • Main scanned pointer text too, so this is not a regression against main. But it makes two 5b claims false for that config: "any other filter driver … still refused" and "the scan reads each large file's own bytes". It also makes the loosening wider than the one named (principle 9).
    • Fix: keep the subsection's case when reading filter keys in both functions (only the section name and the key name are case-insensitive). Add a test that fails without the fix.
  3. One claim in "What this can never do" is stale (measured and read). It says "Added lines are scanned at every placement a line diff could give them, computed in process". Round 5 deleted that mechanism: r3 t fails with addedLines is not a function, and secrets.ts:116–144 now compares whole files by count. Replace the sentence with the round-5 rule (principle 5).

4. Claims checked by reading (src/scan/secrets.ts, src/guard/commit.ts, src/hook.ts)

(a) The unredacted report: true, with two edges that are out of scope.

  • The report is written inside the scan's own temporary folder (${root}.json under tmp, :98). It is removed right after it is read (:110–112), and the whole folder is removed in finally (:148–150). That covers a throw, a gitleaks exit other than 3, and gitleaks' own 120 s timeout (r.error → throw).
  • Only rule names and paths leave the function: findings are built from key.split('\0')[0] (:142). secrets.jsonl logs only status (hook.ts:165, :174, :181). The refusal text for leaks is a fixed string (hook.ts:182). No event is emitted.
  • Edge 1 (inferred): if the hook process itself is killed from outside, finally never runs. The 0700 temp folder, holding the report and the scanned copies, then stays in the OS temp directory.
  • Edge 2 (inferred): the unavailable refusal carries the error's first line. A JSON.parse error on a malformed report would quote part of it, but that needs gitleaks to exit 3 with a broken report. Optional hardening: give that parse a fixed error message.

(b) The "before" side of the count: true.

  • Staged: HEAD's blob (or the empty tree) against the index (:269).
  • Working tree: the index blob against the disk (:275–279).
  • Untracked: nothing (:287).
  • Amend: each parent against HEAD (:461–466), on top of the staged comparison.
  • Each pair of steps covers the whole span: if a file holds a secret more often after the commit than in HEAD, at least one step must show an increase. So splitting HEAD→index→disk (and parent→HEAD→index) can only refuse more, never less.
  • Scope comes from commitPlan:1018 (unstaged = worktree || staging, untracked = staging, head = amend).

(c) Exact standard config: true for the values, the store location and the program check; false for the filter's name (finding 2). All four keys must be present, and every value must equal the standard string (:388). lfs.storage refuses (:387). A missing object refuses (:439). A git-lfs that isn't a regular file refuses (:392–405).

What this can never do

  • Nothing scanned: true. The empty plan is refused (hook.ts:168), and every candidate folder must be a checkout (commit.ts:1022–1028).
  • Display settings: true. The scan reads objects with cat-file --batch and the disk; listings use --raw --no-renames.
  • Index and files: true in substance (inferred). Git's own refresh of index stat data, and git-lfs storing objects during the scan's own change listing under 5b, are writes outside the temp folder.
  • Reopen: true as measured (section 1). The stale sentence is finding 3.

Compromised component

  • Agent user: true as far as measured.
  • Locks: true. src/guard/** is locked (src/review.ts:409) and src/coordinator/** is locked (:477).
  • Coordinator, dashboard and scanned content: true as stated.

What remains

  • All bullets are consistent with the code.
  • The shell-evaluation forms were measured to leak on main and at the head alike.
  • Hooks that git runs while reading changes, as opposed to while committing, aren't named. They belong to the same class as the hooks bullet: config set up in an earlier call.

Round 5 and 5b sections

  • N1–N5 match the code. N3/N4 and the probe results were measured above.
  • Round 5's "K1 refused" was true at 1d1966f and is superseded by 5b (measured: K1 is allowed now).
  • 5b: see (c) and finding 2. The fail-without counts were reproduced.

Refusals during this run: none.

@jayden1711

Copy link
Copy Markdown
Owner Author

Evaluator verdict: CHANGES NEEDED (round 5 and 5b, delta final check)

Head checked: ce7d4abf (rounds 5 and 5b). Previous reviewed head: 1e210a92. Main: 18080438.

Round 5 fixes N1–N7 and the second review's (a)–(d) as their probes measure them, and count-based findings hold up. But round 5b's large-file reading lets through a secret that main refuses, and CI is red on this PR's own new test. The narrowed config-program check also misses some spellings. Main allows all of those too, so they are not regressions, but they make round-5 claims false.

Under the #707 rule, this comment gives verdicts by finding ID only. The orchestrator has the shapes and the probe harness.

How I tested:

  • Each probe went through the hook entry with real gitleaks 8.24.2 and git 2.43.0.
  • I used scratch checkouts, a test token and fake keys, a temporary HOME, state and TMPDIR, and a scratch git config with no system config.
  • Each probe ran against this head and against main 18080438.
  • Where the hook allowed a probe, I ran it in bash and checked whether the secret reached HEAD.
  • git-lfs isn't installed here. Where a result depends on what git-lfs itself does, I say so and label it inferred.

Round-4 findings and the second review

ID Verdict
r2 groups (scan, suppress, progs, magic, extra, fsm, ordinary) Measured: nothing main refuses is allowed here.
r3 groups (diff, budget, paths, attrs, progs, keywords, unread) Measured: nothing main refuses is allowed here. SL3 is now found. The E4 class (held for #983) is unchanged, the same as main.
N1 Fixed, measured. SL5–SL7, SLH and SLP are found, both staged and from the working tree.
N2 Fixed, measured. AL1–AL6 behave exactly as on main.
N3, N4 Fixed, measured. XA0–XA5 and GL0–GL4 are refused.
N5 Fixed, measured. K2–K5 are allowed. K1 is allowed under 5b. Also measured: git runs the external diff only for git diff, and text conversion for git diff and git log -p. Neither runs for status, commit or a raw listing (the same on both trees). Text conversion also runs for a verbose status. But see R5-3.
N6 / (b)–(d) Partly fixed. C1–C3b, C5, C6 and C9 are refused, and C6's marker isn't written (measured). A -C read in another checkout is refused, and so is a directory change into one (measured). But other spellings of the same reads get through: see R5-3 and R5-4. "What remains" now names the re-pointed-name member of the E4 class.
N7 Fixed (test added; measured passing).
(a) Fixed, measured. A rule scoped to an anchored path is found, and AL1–AL6 match main.

Count-based findings (measured)

  • A secret moved to another file: found.
  • The same secret added again: found, whether on a new line, on the same line, or from the working tree.
  • A file that already holds a secret, with that secret neither added nor removed: allowed, the same as main.
  • A second, different secret next to an old one: found.
  • The index as the working tree's "before": found in each shape I tried, including a staging step earlier in the same line.
    • Inferred, why this is sound: the staged comparison always runs.
    • Suppose the commit holds a secret more often than its parent does. Then either the index holds it more often than the parent, or the working tree holds it more often than the index. Either way it is found.
  • Amends: found against each parent.
  • A secret whose rule id changes: inferred to be found. Rule and secret together are one key, so a changed key counts as new (fail closed).
  • The unredacted report:
    • It sits in the scan's 0700 temporary folder.
    • It is deleted after it is read. On a scanner error, a timeout or a throw, the finally removes the whole folder (inferred from the code).
    • Logs carry the status only, and a refusal for a found secret is fixed text. Neither carries the secret (inferred from the code).
    • The scan's copies of the files were always written to the same folder, so the report adds no new exposure.

New findings

ID Finding vs main
R5-1 Regression (5b). Under the standard large-file filter, the scan reads a stored blob as large-file content when the blob merely starts like a pointer. It then scans only the object that pointer names, never the blob's own bytes, and git commits those bytes. Measured: two shapes (sent privately) commit a token into HEAD here, and main finds it in both. The pointer test is anchored only at the start. The only check on the object is that it exists. Main refused (measured)
R5-2 CI is red at this head because of this PR's new test. In run 39661132215, shard 3/4 failed on node 22 and node 24. The failing case is commit-scan-lfs, "not required": it expected deny and got allow. Inferred cause: the runner's system git config already holds the standard large-file filter, so the checkout's missing key comes from there. The guard is right to allow it, since the effective config is standard. The test isn't isolated from system and global git config. n/a
R5-3 The narrowed read-program check misses spellings. Each of these commits the secret here and on main (measured):
• LP1: a log with a value-less option before a patch option. Measured with real git: one option the check reads as taking the next word doesn't take it.
• SV1–SV3: a verbose status, bundled or abbreviated.
• SG1–SG3: signatures shown through format config, a pretty alias, or a boolean git reads as true but the check doesn't.
The body says these programs are refused "when the line shows diffs or signatures". That claim is false.
Same as main (measured)
R5-4 A nested checkout staged in the same line. It becomes a submodule after the scan. Its own config is never checked, because the scan reads submodules from HEAD's tree and the index as they stand before the line runs. C5b commits the secret here and on main (measured). The body's claim "every checked-out submodule's own config is checked" is false for this case. Same as main (measured)
R5-5 A pointer in the working tree is read as text. The scan reads a working-tree side from disk and never resolves it, and untracked files skip the large-file reading entirely. Measured: LF2u is allowed on both trees. Inferred from git-lfs's design (not installed here): its clean step passes a valid pointer through unchanged, so git stores the pointer, and the object it names is what large-file storage keeps and pushes. That object is never scanned. The body says the scan reads "each large file's own bytes". That claim is false here. Same as main (measured)
R5-6 git-lfs's own program config isn't checked. The exact-standard check covers filter.lfs.* and lfs.storage only. With large-file extension programs configured, the commit is allowed (LF9, measured). Inferred, not run because git-lfs isn't installed here: git-lfs runs those programs inside the clean step, so they can change the stored bytes after the scan. n/a (main has no LFS allowance)
R5-7 Undisclosed costs (measured).
• A staged pure rename of a file that already holds a secret is refused. Main allows it, and so does this head when the rename is in the same line.
• Amending a merge whose secret came from its other parent is refused. Main allows it, because it doesn't scan amends.
Both fail closed, but "What ordinary work this now refuses" doesn't list them.
Over-refusal
R5-8 Stale claims.
• "What this can never do" still says added lines are scanned "at every placement a line diff could give them, computed in process". Round 5 deleted that line diff.
• The git-lfs regular-file check reads the hook's own PATH. The agent's shell may use a different one, so word it that way (inferred).
n/a

The LFS checks that hold (measured):

  • Refused:
    • a missing object (LF3);
    • lfs.storage set (LF5);
    • a second value for a key from another config source (LF6);
    • a value with a trailing space (LF6b);
    • a missing key (LF6c);
    • a git-lfs first on PATH that is a directory or a broken link (LF4d, LF4b).
  • Found: a staged pointer whose object holds a secret, both in the checkout (LF8) and from a linked worktree, whose store is the common dir's (LF7).
  • Allowed: a regular file or a link to one (LF4f, LF4l). This is the cost "What remains" discloses.

A git-lfs that isn't found is allowed. My judgement (inferred), backed by one measurement:

  • The check exists to stop a replaced git-lfs. When none exists, git can't run the filter. Measured: here the scan's own listing failed and refused.
  • With required = true, git fails any step that needs the filter, rather than storing other bytes.
  • A line can't install one before its commit, because writes before the commit are refused.
  • That leaves the race, which "What remains" already names. Acceptable.

The monitor's boolean handling.

  • Measured: true is allowed.
  • Inferred:
    • A number such as 2 is refused, although git reads it as the daemon. That is an over-refusal.
    • The check trims and lowercases the value, but git keeps quoted whitespace. So a quoted " true" counts as a boolean here, while git runs it as a command. That matters only through a program with that name on PATH. It's minor, but parsing booleans as git does (see R5-3) closes it too.

Test lines changed (weakening gate)

Three existing lines changed, all in test files this PR adds, none on main. I judge all three acceptable (inferred):

  • Two unit tests tested the line diff and the widening, which round 5 deleted. They now test the counting rule that replaced them. The attack tests in both files are unchanged and pass (measured).
  • In commit-scan-routes, text conversion and external diff go from "could not run" to "found". Both are still deny. The new form is stronger: it shows the scan reads the content under those settings again.

Fail-without (method b)

  • Measured: commit-scan-findings.test.ts, written as a new file into a worktree at 1e210a92. It is byte-identical (sha256 72db0afb…). Result: # pass 2, # fail 8, all assertion failures. The two that pass are the no-regression control and the exempt-list test.
  • Measured: commit-scan-lfs.test.ts, written as a new file into a worktree at 1d1966f4. It is byte-identical (sha256 e664f9cd…). Result: # pass 3, # fail 2. The two that fail are the allowance itself.
  • Measured: at the head, the seven commit-scan files plus commit-plan.test.ts pass 51 of 51 here.

Checks

Required changes

  1. R5-1: treat a side as a large-file pointer only when the whole blob is a valid pointer, anchored at both ends. Also scan the stored bytes themselves, so the object is scanned in addition to the blob, never instead of it.
    • Add a test with the shapes sent privately. It must fail at ce7d4abf.
  2. R5-2: isolate commit-scan-lfs from the machine's system and global git config, so that each case's config is the only one. Then CI must be green at the new head.
  3. R5-3: close these by design, not by reading more spellings (principle 1).
    • When a diff program is configured, refuse:
      • any diff or show;
      • any log with an option outside a short list of options that never take a separate word;
      • any status with an option outside a short list.
    • When a signature program is configured, refuse any log or show at all.
    • Read booleans as git does, or fail closed.
    • Add tests for LP1, SV1–SV3 and SG1–SG3.
    • Or keep the narrowing, and correct the claim to name each of these as open.
  4. R5-4: when the line stages anything, check the config of every nested checkout under the top, or refuse staging a path that is a nested checkout. Add a test. Or name it in "What remains".
  5. R5-5: resolve a working-tree side that is a valid pointer the same way a stored one is resolved, and refuse when its object is missing. Run untracked files through the same reading. Add a test. Or correct the claim and name it.
  6. R5-6: under the large-file allowance, refuse when any lfs.extension.* key is set. It is better still to refuse any lfs.* key outside a short list known not to name a program or change stored bytes. Add a test.
  7. R5-7: list both costs in "What ordinary work this now refuses".
  8. R5-8: correct the stale line-diff claim. Word the git-lfs check as the hook's PATH.
  9. Rebase or merge as needed so CI runs the merged head. The git: an instance's git runs nothing its checkout's config names, and .git/ is the coordinator's (#926) [PROTECTED: owner merges] #944, git: one runner for every engine git call, with replace refs and grafts off (#807) [PROTECTED: owner merges] #927 and Review levels: a path that looks like a locked or protected one (UTS #39 skeleton) always waits (#763) [PROTECTED: owner merges] #910 conflicts are for whichever merges second.

R5-1 is a regression against main, so it can't be held for the owner. R5-3 to R5-6 follow from principle 1 or need a claim corrected, so no owner decision is needed. No new trade-off goes to the owner.

Everything else checks out:

  • N1–N7 and (a)–(d) are fixed as their probes measure them.
  • The count-based rule is sound in every shape tried.
  • The fail-without tests reproduce, and the head's tests pass locally.
  • Typecheck, denylist and size are clean.

…fig as git does

Review round 6 for #971 (#979). The commit scan covers every commit form
and checkout the guard allows; this round:

- Under the standard large-file filter, the bytes git stores are always
  scanned, and a side that is exactly one valid pointer is scanned with
  the object it names (checked against the pointer); working-tree and
  untracked pointers too.
- Config keys keep their subsection's case, so only the exactly named
  large-file driver gets its allowance; any large-file setting outside a
  short safe list takes the allowance away.
- With a diff program configured, any diff or show, and any log or status
  with an option outside a short list, refuses the commit; with a
  signature program configured, any log or show does; the file-system
  monitor setting is read as a boolean the way git reads it.
- A nested checkout the line stages is checked like a submodule.
- The commit-scan tests no longer read the host's system or user git
  config.

Test (fails without): test/adversarial/commit-scan-stored.test.ts, all 7
of its tests fail at ce7d4ab.

Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Round 6 for #971 (#979), found while re-running the round-5 probes: an
interactive staging step shows each change through any diff filter the
config names, so with one configured, a line that stages interactively
before it commits is refused, as other diff-showing reads are.

Test (fails without): test/adversarial/commit-scan-stored.test.ts, its
interactive staging case fails at ce7d4ab.

Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
@jayden1711

Copy link
Copy Markdown
Owner Author

Second review (verification-scoped)

Verdict: PASS on 3e63c5e for the guardrail. I found no route that main had refused and this head lets through, and no false safety claim.

Not mergeable as it stands: CI is red on this head, and that needs a builder fix (see Required changes).

This review is verification-scoped, per the owner's decision of 2026-10-21. It re-ran the documented probe groups and the PR's fail-without tests, and checked the claims against the code. It built no new payloads. The comment names groups and IDs only (#707).

1. Documented probe groups: head 3e63c5e vs origin/main 1cee9bd (measured)

All of these ran unchanged except for paths, on scratch checkouts with a temp HOME.

Group Head Main Head allows a line main refused?
r5 count Found: MV1, MV2, DUP1, DUP1w, DUP2, NEW2, IDX1-3, AM1-3. Allowed: MV3, DUP3. MV2 and AM1-3 allowed No
r5 lfs Found: LF1, LF1c, LF2u, LF7, LF8. Refused: LF2, LF3, LF4b, LF4d, LF5, LF6, LF6b, LF6c, LF9, LF10. Allowed: LF4f, LF4l. Every line allowed except LF1, LF1c, LF2 No
r5 reads LP0, LP1, SV0-3, SG0-4, CD0, CD1, C5b and DF1 refused All allowed; LP1, SV1-3, SG1-3, CD1 and C5b committed No
r4 (slide, cfg, glob, env, gitrun, cost, allow, xargs) SL* found. C1-C3b, C5, C6 and C9 refused. GL0-4, EN1-6 and XA0-5 refused. C4 allowed, nothing committed. C6b: marker not written. C5, C9, GL*, EN* and XA* allowed. C6 and C6b wrote the marker. No
r3 (diff, budget, paths, attrs, progs, keywords, unread) SL1-4, BG1-3, CL*, AT1-10, DS2, HS, SO2, EV, EV2, BI, CO, XS, TS, K1-7, U1-8, U10 and U12 refused or found Main refuses a subset of these No
r3 cost, cost2, ro Same false-refusal profile as round 5. ro: writes before a commit are refused. No commit model on main (module absent) n/a
r3 g, g2 (scanner only) The scanner's default allowlist skips some file names; signatures past the 64 KiB prefix are still read (40/40) Same scanner n/a
r2 (scan, suppress, progs, magic, extra, fsm, ordinary) All main-refused lines refused or found. Also S2, S7, S11p, X5, X6, P1-6, M3, W1-8, O1, O2, O4-12 and O17 refused or found. Those allowed No

Still committed at head, the same as on main (measured):

  • r3 progs: H1, H2, T1-T5, TR, SC, FL, DS, PS, SO, FX
  • r3 unread: U9, U11

These are the shell-feature residuals named in "What remains" bullet 4 (#983), so they are disclosed, not new.

2. Fail-without (measured)

Each file was written byte-identical to the head copy (sha256 matched) as a new file into a fresh worktree, then built and run there.

  • test/adversarial/commit-scan-stored.test.ts (sha256 3dd52ad6…) at ce7d4ab: # pass 0, # fail 8. Every failure is an assertion ('allow' !== 'deny').
  • test/adversarial/commit-scan-lfs.test.ts (sha256 3ca920ce…) at 1d1966f: # pass 3, # fail 2.
    • The two failures are tests 1 and 2: the standard filter is refused rather than scanned.
    • The three passes are "still refuses" checks. The base refused every filter, so it already met them.
  • At head:
    • the seven test/adversarial/commit-scan*.test.ts files gave 53 pass, 0 fail, 0 skipped (gitleaks present);
    • test/commit-plan.test.ts gave 6 pass.
    • Typecheck is clean.

3. Claims vs src/scan/secrets.ts and src/guard/commit.ts

What this can never do

  • Nothing scanned: true. commit.ts:1026-1035 refuses any place that isn't a checkout; hook.ts:168 refuses an empty plan.
  • Display settings can't hide content: true.
    • Objects are read through cat-file --batch (secrets.ts:189-203) and files from disk (248-258).
    • The listing is diff --raw, and gitrun measured that it runs no diff program.
  • Never changes the index or files: true for index content and working files (inferred), but unscoped for the large-file store.
    • Under 5b, the scan's own listing makes git run git-lfs. LF2's measured error shows this, and git-lfs clean writes to .git/lfs (inferred).
    • "Compromised component" bullet 5 already scopes this; this bullet doesn't. Wording only.
  • No reopened route, as far as measured: true (section 1).
    • The count rule is at secrets.ts:158-164.
    • No ignore file is read, at secrets.ts:93-95, 100, 127-131 and 174. The .gitleaks.toml handling is as on main (90-92).
  • Hook and filter content added during the commit isn't scanned: true (a disclosure).

Compromised component

  • Agent user: true as worded. The shell-feature residuals above are covered by "What remains" bullet 4. The locks are at review.ts:409 (src/guard/**) and 477 (src/coordinator/**).
  • Coordinator unchanged: unscoped. It lies outside these two files and I didn't run its tests.
  • Dashboard, other instances and hub viewer: unscoped (inferred in the PR).
  • Push access: true. review.ts:409 and 477 lock these, and src/**/*secret* at review.ts:394 covers secrets.ts.
  • The scanned checkout's own content: true (inferred). The scan spawns only git and gitleaks, and gitleaks gets a config, not a program.

What remains

  • Bullets 1, 2, 5 and 6 (out of scope): true as disclosures.
  • Bullet 3, config programs refused as named: true.
    • secrets.ts:350-387
    • hook.ts:170-177 checks other checkouts' reads
    • commit.ts:996-1009
  • Bullet 4, shell features: true (measured, same on main).
  • Bullet 7, git-lfs on PATH: true.
    • secrets.ts:418-431 reads the hook's PATH.
    • LF4f and LF4l are allowed; LF4d and LF4b are refused (measured).
  • Bullet 8, what is scanned rather than how it's judged: true (secrets.ts:131). The scanner's file-name allowlist (g) is the same on main (secret scans: ignore suppressions an agent can write #980).
  • Bullet 9, docs label: true (docs/guardrails.md:184-192).

Round 6

  • Stored bytes always (R5-1): true.
    • The pointer regex is anchored and capped at 1 KiB, at secrets.ts:442 and 461-464.
    • The pointer and the object are both scanned (470).
    • A missing or mismatched object refuses the commit (467, 469).
  • Pointers on disk (R5-5): true, at 244 and 290.
  • Driver name case: true, at 326-331 and 411.
  • Large-file settings (R5-6): true, at 402 and 412.
    • Minor: the body's summary of the safe list omits repositoryformatversion, concurrenttransfers, keepalive, skipdownloaderrors and fetchrecentalways.
  • Reads (R5-3): true.
    • commit.ts:561-568 and 997-999; secrets.ts:317, 356, 358 and 367.
    • The interactive check covers add only. That covers every staging step, because only add, rm and mv may precede a commit (commit.ts:556, 571, 590).
  • Monitor read as git reads it: true, at 363-366.
  • Nested checkouts (R5-4): true when the line stages, at 386 and 479-490.
  • Test isolation: true. All seven commit-scan files set both variables; commit-plan.test.ts runs no git.
  • Costs: true (measured). MV2 and AM1 are found at head and allowed on main; LF10 is refused at head and allowed on main.
  • Weakening gate: true. The signature test went from expecting allow to expecting refuse, and "not required" is still refused.
  • Verification numbers: the fail-without 0/8 and the round-5 probe lists match my runs exactly (measured). I didn't re-run the 181/181 figure.
  • Package size "within budget": false (measured). See Required changes.

Required changes (builder; not guardrail)

  1. CI is red on 3e63c5e (run 39685972735, shard 1/4 on Node 22 and 24).
    • The failure is test/package-files.test.ts: the unpacked size is 6,402,689 bytes against a budget of 6,400,000.
    • main's CI at 1cee9bd passes, so this PR's growth crosses the line (measured).
    • Fix: raise PACKAGE_BUDGET as the package-budget practice says, giving the new npm pack --dry-run --json numbers (with the UI built, as CI builds it) and the reason in the body.
    • Also correct the "Package size … Within budget" line. Its numbers look like a build without the UI (inferred).
  2. Optional wording: scope the "never changes the index or files" bullet to say that, under the large-file allowance, git runs git-lfs during the scan's listing.

Digest

  • Guardrail: the agents' commit-time secret scan (commit scan: cover every commit form and checkout the guard allows #971) now models every commit form and checkout, and reads stored content. It also refuses configured programs that git would run during the line's reads.
  • Allows/blocks: it blocks every documented bypass that main also blocked, plus many main let through. It allows the exact standard large-file filter, with pointer and object both scanned.
  • Why: main scanned a rendered diff of one checkout, and display settings, other checkouts and command forms could hide content from it.
  • Verdict: PASS on 3e63c5e for the guardrail; merge waits on the package-budget CI fix.
  • Risk if wrong: a secret reaches a local commit unscanned. The push gate still scans what leaves the machine.

Refusals: none.

#971 (#979) grows the package: with the dashboard UI built, npm pack
--dry-run --json --ignore-scripts measures 6,402,689 bytes unpacked,
over the 6,400,000 budget (the tarball, 2,045,128, is still under its
2,050,000). The unpacked budget becomes the measured size plus 5%,
rounded up to the next 10,000: 6,730,000. The tarball budget is
unchanged. This is a loosening of a check, named in the PR body.

Test: test/package-files.test.ts's size budget test passes with the
measured package; it failed in CI at 3e63c5e.

Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
@jayden1711

Copy link
Copy Markdown
Owner Author

Second review (verification-scoped)

Verdict: PASS on dd6b301. I found no route that main refuses and this head lets through, and no false safety claim. CI's ci run 39696007403 passed on this head, all four shards on Node 22 and 24. The release workflow's build job was still running when I checked.

This review is verification-scoped, per the owner's decision of 2026-10-21. It re-ran the documented probe groups and the fail-without test, checked the claims against the code, and built no new payloads. The comment names groups and IDs only (#707).

1. What changed since 3e63c5e (measured)

  • Commits: one, dd6b301, whose parent is 3e63c5e.
  • Code: diff --stat 3e63c5ed dd6b3014 shows only test/package-files.test.ts, one line changed: PACKAGE_BUDGET.unpackedSize goes from 6_400_000 to 6_730_000. The tarball budget is unchanged. Nothing changed under src/, docs/ or scripts/.
  • PR body: I compared the edit made after the last second review (23:51Z) with the one it reviewed (22:22Z). The changes are:
    • the per-round "Package size" lines now say "no web build", and the false "Within budget" wording is corrected;
    • the "never changes the index or files" bullet is now scoped. git may refresh the index's stat data during the listing, and under the large-file filter git-lfs may write to the large-file store. This was the optional wording item from the last round;
    • a new "Package budget (a loosening, principle 9)" section.
  • Merge with main: merge-tree with origin/main (1cee9bd) is clean.

2. Documented probe groups: head dd6b301 vs origin/main 1cee9bd (measured)

All groups ran unchanged on scratch checkouts with a temp HOME and TMPDIR.

Group Head Main Head allows a line main refused?
r5 count Found: MV1, MV2, DUP1, DUP1w, DUP2, NEW2, IDX1-3, AM1-3. Allowed: MV3, DUP3. MV2 and AM1-3 allowed No
r5 lfs Found: LF1, LF1c, LF2u, LF7, LF8. Refused: LF2, LF3, LF4b, LF4d, LF5, LF6, LF6b, LF6c, LF9, LF10. Allowed: LF4f, LF4l. Every line allowed except LF1, LF1c, LF2 No
r5 reads LP0, LP1, SV0-3, SG0-4, CD0, CD1, C5b and DF1 refused All allowed; LP1, SV1-3, SG1-3, CD1 and C5b committed No
r4 (all groups) SL* found. C1-C3b, C5, C6 and C9 refused. GL0-4, EN1-6 and XA0-5 refused. C4 allowed, nothing committed. C6b: marker not written. C5, C6, C9, GL*, EN* and XA* allowed. C6b wrote the marker. No
r3 (all groups) Nothing main refused is allowed. Allows a superset of what head allows. No
r3 cost, cost2, ro Ran to completion, same false-refusal profile as before. ro: writes before a commit are refused. Module absent on main n/a
r2 (all groups) Allowed only S1c-1, S5, X1-4 and O13-16, which main allows too. Also allows S2, S7, S11p, X5, X6, P1-6, P11, M3, W1-8, O1, O2, O4-12 and O17. No

Across all four groups, the comparison found no ID that the head allows and main refuses.

Still committed at head, the same as on main:

  • r3 progs: H1, H2, T1-T5, TR, SC, FL, DS, PS, SO, FX
  • r3 unread: U9, U11

These are exactly the residuals of the last round. They are the shell-feature cases disclosed in "What remains" bullet 4 (#983).

3. Fail-without and tests (measured)

  • Fail-without: I wrote test/adversarial/commit-scan-stored.test.ts as a new file into a fresh worktree at ce7d4ab. It matches the head copy (sha256 3dd52ad6…). Built and run there, it gave # pass 0, # fail 8, and every failure is an assertion ('allow' !== 'deny').
  • At head:
    • the seven test/adversarial/commit-scan*.test.ts files plus test/commit-plan.test.ts gave 59 pass, 0 fail, 0 skipped (53 + 6, as last round; gitleaks present);
    • npm run typecheck is clean.

4. Package budget (measured)

  • I ran npm run build and npm run build:web at dd6b301, then npm pack --dry-run --json --ignore-scripts. It gave a tarball of 2,045,128 and an unpacked size of 6,402,689 (536 files). That matches the body and CI's earlier failure at 3e63c5e.
  • The arithmetic holds: 6,402,689 × 1.05 = 6,722,823.45, which rounds up to the next 10,000 as 6,730,000. That follows the owner's package-budget practice (new size + 5%, rounded to 10,000), and the tarball budget is untouched.
  • The budget test alone (--test-name-pattern='the package stays within its size budget') passes at the head with dist/web built.
  • The body labels this a loosening (principle 9) and names the check. It also flags that the comment above the budget says "about 2%", which is a fair point for the owner. It isn't a blocker, since the practice the owner set is 5%.

5. Claims vs code

The guard code is byte-identical to 3e63c5e, so last round's claim checks and line references still hold (measured: no src/ diff). I spot-checked the pointer handling at secrets.ts:441-470: the anchored regex, the 1 KiB cap, a missing or mismatched object refusing the commit, and the pointer and object both scanned. All are where they were.

  • New scoped bullet: true (inferred). The scan's listing is git diff --raw -z --no-renames (change-files.ts:225, used by secrets.ts:270 and 277). That is the porcelain diff, which may write refreshed stat data to the index. Under the standard filter, git runs git-lfs filter-process for files whose stat data changed, and that writes to .git/lfs/objects. Neither changes a staged entry's content, so "neither changes what is committed" holds.
  • Package lines: now true. They are labelled "no web build", and the "Within budget" claim is corrected (section 4).

Required changes

None.

Digest

  • Guardrail: the agents' commit-time secret scan (commit scan: cover every commit form and checkout the guard allows #971) now models every commit form and checkout, and reads stored content. It also refuses configured programs that git would run during the line's reads. This push only raises the unpacked package budget to 6,730,000.
  • Allows/blocks: it blocks every documented bypass main blocks, plus many main lets through. It allows the exact standard large-file filter, with the pointer and object both scanned. The probe results are unchanged from 3e63c5e.
  • Why: main scanned a rendered diff of one checkout, and display settings, other checkouts and command forms could hide content from it. The budget raise covers the measured growth (6,402,689 + 5%).
  • Verdict: PASS on dd6b301; CI ci is green.
  • Risk if wrong: a secret reaches a local commit unscanned (the push gate still scans what leaves the machine), or the package grows without notice up to 6.73 MB.

Refusals: none.

@jayden1711

Copy link
Copy Markdown
Owner Author

Delta final check (round 6 + package budget): CHANGES NEEDED on dd6b301

Head checked: dd6b3014b25e858785838fbdf88f110bd52f4446, which is round 6 (3e63c5e) plus the budget commit. Previous fully reviewed head: ce7d4ab. Exact probe lines and shapes are with the orchestrator (#707). This comment uses finding IDs only.

Summary. Nothing that main refuses is let through at this head (measured). Every round-5 finding is fixed as written. The budget raise is correct and named as a loosening. CI is green. Two gaps of the same class as R5-1/R5-5 and DF1 remain, and both are the same as on main: R6-1 and R6-2 below.

1. Round-5 findings and the second review's points

ID Status at dd6b301
R5-1 Fixed (measured). The stored bytes are always scanned, with the object added, never in its place. Both shapes from the earlier review are found. The pointer pattern is anchored at both ends. But see R6-1.
R5-2 / CI Fixed (measured). Every commit-scan test file sets GIT_CONFIG_NOSYSTEM and a scratch GIT_CONFIG_GLOBAL in process.env. The scan's git calls inherit process.env (gitOut, read in src/scan/secrets.ts), so the isolation reaches the guard too (inferred from the code). CI is green, see section 5.
R5-3 Fixed (measured). With a diff program configured, any diff/show, any log option outside the list and any status option outside the list are refused. With a signature program configured, any log/show is refused. The monitor setting is read with git config --type=bool. The earlier review's read probes are all refused, and main commits the token for all of them.
R5-4 Fixed (measured). A nested checkout staged in the line is checked like a submodule and refused. Untracked nested checkouts at any depth come from ls-files --others, which lists them with a trailing slash (inferred).
R5-5 Fixed for the standard spelling (measured). Untracked and working-tree sides go through the large-file reading. A pointer to an object that holds a token is found. But see R6-1.
R5-6 Fixed (measured). Extension programs, a moved store, custom transfers and a second value are all refused. A safe key (lfs.url) keeps the allowance (test).
R5-7 Disclosed (measured). The staged pure rename and the merge amend are refused at head and allowed on main, and both are listed under Costs. The custom transfer cost is listed too and measured as refused.
R5-8 Fixed (read). The line-diff sentence is gone, and the git-lfs check is worded as the hook's PATH.
R5-9 Done (measured). origin/main 1cee9bd is merged, and merge-tree with it is clean.
Filter name case Fixed (measured). Subsection case is kept (configKey). A filter named like lfs but in other case is refused (test).
Object match Fixed (measured). Other bytes under the oid are refused, and so is a pointer whose size doesn't match (two scratch probes). A missing object is refused too.

2. New findings

R6-1: pointer spellings git-lfs accepts are not read as pointers.

  • Measured: under the standard filter, several pointer spellings name an object that holds a token, and the commit is allowed. Each is a spelling that git-lfs's own parser accepts but the guard's pattern (canonical form only) doesn't. They were tried staged and untracked. Each one is scanned only as its stored text, never as the object. The canonical spelling of the same pointer is found (control).
  • Inferred from git-lfs's lfs/pointer.go (read from upstream; git-lfs isn't installed here): git-lfs treats each of these as a pointer, so checkout writes the object and push uploads it. The token then leaves through the large-file store without being scanned.
  • This is the same on main (measured: main allows every probe, and it reads no objects at all), so it is not a regression. But it is a hole in the R5-1/R5-5 fix. It also makes two claims false for those spellings: round 5b's "a stored pointer is read as the object it names" and round 6's "working-tree and untracked files that are exactly a valid pointer are read the same way". It also sits inside the 5b loosening: before 5b, these commits were refused.

R6-2: an interactive staging step runs the editor after the scan.

  • Measured: in a line that stages interactively and then commits, an editor that git runs during the staging step wrote a token into a tracked file, and the commit took it. The editor here came from the checkout's config. The same happens on main, so this is not a regression.
  • The head already refuses the edit staging option (measured; main allows it and commits the token). The interactive form reaches the same editor.
  • Round 6's interactive-staging fix counts that step only as "shows a diff", so it is refused only when a diff program is configured. Inferred: the editor can also come from the agent's environment, which the guard can't see, so checking config keys can't close it. This is the DF1 class, and the fix covers one instance of it.

3. Weakening gate (changed existing test lines, ce7d4ab to dd6b301)

  • The six existing commit-scan files gain the isolation lines. These are additions.
  • commit-scan-findings: a plain log before a commit with a signature program set went from "allowed" to "refused", plus a new "allowed" for a bare commit. This is a tightening.
  • commit-scan-lfs, "not required": the omitted key became an explicit false value. That is what the verification review suggested, and it is still refused. Measured: the case as first written (key omitted) is also refused at head under isolated config. It is no longer tested anywhere, though. Restoring it next to the new one is recommended, not required.
  • test/package-files.test.ts: unpackedSize 6,400,000 → 6,730,000. This is the loosening named in the body, see section 4.

4. Package budget (principle 9 loosening, independent review)

  • Measured at dd6b301, after npm run build and npm run build:web, with npm pack --dry-run --json --ignore-scripts: tarball 2,045,128 and unpacked 6,402,689 (536 files, dist/web/index.html present). This matches the body and CI.
  • Measured on main 1cee9bd with the same build: 2,023,978 and 6,328,480. This PR adds +21,150 to the tarball and +74,209 unpacked.
  • Measured: 6,402,689 × 1.05 = 6,722,824, which rounds up to 6,730,000, as the body says. The tarball budget (2,050,000) is unchanged. The budget test alone passes at head with the UI built.
  • Named as a loosening: yes (read). The body names the check, says its finding wasn't wrong, and says what grew.
  • For the owner (read): the comment above PACKAGE_BUDGET says a raise between releases is "measured + about 2%" (that would be 6,540,000). The 5% follows the owner's practice. The builder flagged the mismatch, and so do I. This is not a required change here. The comment and the practice should be reconciled in a separate change.
  • Inferred: tarball headroom is now 4,872 bytes (0.24%), so the next PR that grows the package will likely trip the size budget.

5. Other checks

Required changes

  1. R6-1: under the large-file allowance, fail closed on any side that git-lfs could read as a pointer but that isn't the canonical form: up to 1024 bytes and containing git-lfs's version marker. Or parse git-lfs's full pointer grammar and read the object it names. Add a test per spelling (the orchestrator has them). Or correct the 5b and round-6 claims and name the gap in "What remains" (principles 1 and 5).
  2. R6-2: refuse an interactive staging step before a commit in the same line outright, as the edit staging option already is, whatever the config. The editor it may run can come from config or from the environment. Add a test. Or name it in "What remains" (principle 1; fix the class, not the instance).

Neither is a regression against main, and both follow from principles 1 and 5, so neither needs an owner decision. The only owner item is the 2%-vs-5% budget comment in section 4, which doesn't block this PR.

Review round 7 for #971 (#979). The commit scan covers every commit form
and checkout the guard allows; this round:

- Under the standard large-file filter, a small large file that names a
  pointer version in any spelling but is not the exact pointer form
  refuses the commit, since the large-file program may still read it as a
  pointer to an object the scan can't see.
- A large-file object that is not a regular file refuses the commit with
  its reason, before anything reads it.
- Interactive staging, and an interactive commit, in the line are refused
  whatever the config, as edited staging already was: either can change
  what is committed after the scan.
- The large-file test keeps its case with the required key left out, next
  to the case with it set to another value.

Test (fails without): test/adversarial/commit-scan-pointers.test.ts, all
3 of its tests fail at dd6b301; test/adversarial/commit-scan-fifo.test.ts
is head-only (on a build without the check its read blocks the process).

Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
@jayden1711

Copy link
Copy Markdown
Owner Author

Second review (verification-scoped): PASS on 85a5bbd

Scope (owner, 2026-10-21): this review re-ran the documented probe groups and the round-7 fail-without test, and checked the claims against src/scan/secrets.ts and src/guard/commit.ts by reading. It built no new payloads and tried no new attack forms. Head 85a5bbd; main 1cee9bd (origin/main); fail-without base dd6b301.

1. Documented probe groups, head vs main (all measured)

Each group was run unchanged except for the worktree path, in scratch checkouts with a temporary HOME and isolated git config.

Group Head refuses or finds Head allows Main Head allows something main refuses?
r6 pv / req / mm / edit PV0 found; PV1–PV8, PVU, RQ0, MM1, MM2, ED1–ED3, CP1, CP2 refused ED0 (plain staging, no secret committed) allows all of them; ED1–ED3 commit the token no
r6 fifo (FF named pipe, FZ link to an endless device) both refused, "not a regular file", in 360 ms and 449 ms none allows both no
r5 count / lfs / reads MV1, MV2, DUP1, DUP1w, DUP2, NEW2, IDX1–3, AM1–3, LF1, LF1c, LF2u, LF7, LF8 found; LF2, LF3, LF4b, LF4d, LF5, LF6, LF6b, LF6c, LF9, LF10, LP0, LP1, SV0–3, SG0–4, CD0, CD1, C5b, DF1 refused MV3, DUP3, LF4f, LF4l allows these four too (plus MV2, AM1–3, LF2u, LF3–10 and others); LP1, SV1–3, SG1–3, CD1 and C5b commit the token no
r4 slide / cfg / glob / env / gitrun / cost / allow / xargs SL5–SLPw found; C1–C3b, C5, C6, C9, GL0–4, EN1–6, XA0–5 refused; C6's marker not written; AL2–4 found C4 (nothing committed), K1–K5, AL1, AL5, AL6 allows the same; C5, C9, GL1–4 and XA0–5 commit the token no
r3 diff / budget / paths / attrs / progs / keywords / unread SL1–SL4w, BG1–3, CL1, CL1c, CL2, AT4s, AT9, AT10 found; AT1–AT8 (except AT4s), DS2, HS, SO2, EV, EV2, BI, CO, XS, TS, K1–K7, U1–U8, U10, U12 refused PA1–5, K8–14 (nothing committed), AL (exit 127); H1, H2, T1–T5, TR, SC, FL, DS, PS, SO, FX, U9, U11 commit the token allows all of these too, and commits the token in each no
r3 cost / cost2 / ro (classifier only) expanded command names, text fed to a shell, xargs with {}, writes before the commit: refused ordinary lines n/a (the classifier is new in this PR) n/a
r2 scan / suppress / progs / magic / extra / fsm / ordinary S1–S12 (except S1c-1, S5), X5–X7, P1–P11, M1–M3, W1–W9, F1–F3s, O1–O12, O17 refused or found S1c-1 (nothing committed), S5, X1–X4, P0, O13–O16 allows the same, and commits the token in S11p, P1, P3–P6, M3, W1–W7, O1, O2 no

The routes still open at head are open on main too (measured). They fall in the classes the PR body discloses under "What remains", bullet 4: builtins that evaluate their arguments, a builtin that re-points a command name, traps, programs that run a command string, a script path that reads the command's input, and process substitution (r3 H/T/TR/SC/FL/DS/PS/SO/FX/U9/U11).

2. Fail-without (measured)

  • test/adversarial/commit-scan-pointers.test.ts was written as a new file into a worktree at dd6b301. It is byte-identical to the head copy (sha256 1d094c07…, compared with cmp). Result there: # pass 0, # fail 3.
    • Test 1 fails on the hawser spelling: expected deny, got allow.
    • Test 2 fails on the reason only. The base already denied, with an EISDIR read error, where the test expects "not a regular file".
    • Test 3 fails on add -p: expected deny, got allow.
  • The named-pipe test was not run at the base, as briefed.
  • At the head, the ten commit-scan files (nine in test/adversarial/commit-scan*.test.ts plus test/commit-plan.test.ts) gave # tests 63, # pass 63, # fail 0, # skipped 0. That includes commit-scan-fifo.test.ts.
  • npm run typecheck at the head: clean.
  • CI on 85a5bbd: every check that ran passed; the real-systemd, bootstrap and publish jobs were skipped.

3. Claims against the code

Round 7:

  • Inexact pointers: true. Measured by PV1–PV8, PVU and LF1/LF1c.
    • In lfsContent, a side of 1024 bytes or less that fails LFS_POINTER but matches LFS_MARKER (git-lfs, hawser or git-media, any case) throws. The scan then reports "could not run" and the hook denies.
    • lfsContent runs on staged entries, on working-tree entries (entryContent via fromDisk) and on untracked files (untrackedContent). So stored bytes, the working tree and untracked files are all covered.
  • Interactive staging and commits: true. Measured by ED1–ED3, CP1, CP2 and DF1.
    • For staging, beforeRefusal refuses add with a short bundle holding p or i, or with an INTERACTIVE_LONG match.
    • Only add, rm, mv and read-only git subcommands may come before a commit, so no other interactive stager is reachable.
    • For commits, commitEffects throws on short p and on the long patch / interactive options, through longOption. That covers prefixes, and an ambiguous prefix also refuses.
    • That INTERACTIVE_LONG covers every prefix git accepts is inferred from git's option names. The prefixes it misses (--p, --pa, --i, --in…) are ambiguous in git add.
  • Object files: true. Measured by FF, FZ and pointers test 2.
    • lstatSync comes before readFileSync. Anything other than a regular file (a folder, a link or a pipe) throws "not a regular file".
    • A swap between the lstat and the read by another process falls under "What remains", bullet 5 (inferred).
  • Tests bullet: true. The case with the required key left out is back in commit-scan-lfs.test.ts.
  • Weakening gate: true in substance. Every changed expectation still asserts deny. One small wording correction: it is four changed lines across three tests (two lines in the stored-bytes test), not "three lines". This is not blocking.
  • Fail-without: true (measured above). The "After" figures are partly checked: the ten files are measured above. The 378 count for the related set, the denylist, DCO, push gate and package figures were not re-run here (unscoped).
  • Round-6 probes on this head: true. My run matches the body line for line.

What this can never do:

  1. Nothing scanned: true (inferred from the code). commitPlan refuses with no checkout (commit.ts "no checkout to scan"), and hook.ts denies an empty plan.
  2. Display settings: true (inferred from the code). The content comes from raw change listings and object or file reads. Gitleaks runs dir over copies written by the scan.
  3. Doesn't change the checkout: true as scoped. Inferred: writes go only to mkdtemp, which is removed in finally, and round 7 adds only lstat and reads.
  4. No reopened route: true as far as measured (section 1). The other parts also hold by reading:
    • counting is by occurrences (introduced);
    • --gitleaks-ignore-path points at an empty file;
    • the checkout's .gitleaks.toml is honoured.
  5. Content added by a program git runs during the commit: true as a stated limitation.

Compromised component:

  • Agent user: true, with the scope stated. The routes that are still open are the ones disclosed in What remains (section 1).
  • Locked files: true (inferred). src/review.ts locks src/guard/**, src/coordinator/** and src/**/*secret*; that last glob covers src/scan/secrets.ts.
  • Coordinator, dashboard: true (inferred). Round 7 changes only secrets.ts and commit.ts.
  • Checkout content: true (inferred).

What remains:

  • Bullets 1–2 and 5–6: true (scope statements).
  • Bullet 3 (config-named programs during reads): true. Measured by C1–C9, LP/SV/SG, CD0/CD1 and C5b.
  • Bullet 4 (shell features): true. Measured: each is open on main as well (section 1).
  • Bullet 7 (git-lfs on PATH): true. lfsProgramIsFile follows links with statSync. Measured: LF4f and LF4l are allowed, LF4b and LF4d are refused.
  • Bullet 8: true. Each file is written at its own path under the scan's root.
  • Bullet 9: true. docs/guardrails.md labels the guarantee "inferred until CI on main runs these tests".

Verdict

PASS on 85a5bbd.

  • No probe line that main refuses is allowed at head.
  • The new test fails at dd6b301 and every commit-scan file passes at head.
  • No safety claim checked was false.
  • Non-blocking: fix the weakening-gate count in the body (four lines, three tests).

@jayden1711

Copy link
Copy Markdown
Owner Author

Delta final check (round 7): CHANGES NEEDED (PR body only) on 85a5bbd

Head checked: 85a5bbde31bb1f0945a6e5797418ccfb913c002d (round 7). The previous reviewed head was dd6b301. The exact probe lines are with the orchestrator (#707), so this comment uses finding IDs only.

Summary.

  • The code at this head passes every check I ran:
    • R6-1 and R6-2 are fixed, and so are the two round-6 notes (the omitted-key case and the object file check).
    • Nothing that main refuses is let through (measured).
    • The fail-without holds, typecheck, denylist and size are clean, and CI is green.
  • One required change is to the PR body only: the cost section doesn't list the two new refusals from this round. No code change and no new CI run are needed for it.

1. Round-6 findings

ID Status at 85a5bbd
R6-1 Fixed (measured). Under the standard large-file filter, every non-exact pointer spelling (PV1–PV8) is refused, staged and untracked (PVU). The reason is "looks like a large-file pointer but is not in its exact form". The exact form is still read with its object and found (PV0). Inferred from upstream lfs/pointer.go (read today): git-lfs reads a blob as a pointer only if it matches git-media|hawser|git-lfs, which is the same test as LFS_MARKER. The guard's copy ignores case, so it refuses a little more than git-lfs would. git-lfs's pre-push only considers blobs under 1024 bytes (gitscanner_catfilebatchcheck.go), and the guard's bound is ≤1024 bytes, so it covers that. On main, all ten probes are allowed.
R6-2 Fixed (measured). ED1, ED2, ED3, CP1 and CP2 are refused whatever the config, and the plain-staging control ED0 is allowed. On main, ED1–ED3 commit the token. 20 more spellings were measured, and all are refused: long and short forms, prefixes, short-flag clusters, the option after the path, quoted forms, --no-patch, -C ., and ; or || before the commit. Interactive staging alone in a line, with no commit in it, is still allowed (measured), which is correct because the scan runs at the later commit.
RQ0 (note) Fixed (measured). The omitted-key case is refused again, and the test has it back next to the explicit false.
FF (note) Fixed (measured). An object path that is a named pipe, a link to an endless device or a folder is refused with the reason "not a regular file". The pipe case took 271 ms, where it hung at dd6b301. MM1 and MM2 are still refused.

2. The pointer-marker refusal: what it costs (measured, scratch checkouts, a git-lfs stand-in that cleans to the exact pointer)

  • Not affected:
    • A README that mentions git-lfs, in a checkout using the large-file filter for other files. Allowed, whether untracked and staged in the line or tracked and committed with -a.
    • Files under the filter that are over 1024 bytes.
    • A small random binary under the filter.
    • A file under the filter that was staged in an earlier call, which git-lfs stores as the exact pointer. Allowed, with the object read.
  • Newly refused: a file of 1024 bytes or less that is itself under the large-file filter and names git-lfs, hawser or git-media anywhere (for example a short note mentioning git-lfs, or prose using the word "hawser"), when it is committed from the working tree or staged in the same line. On main and at dd6b301 these are allowed.
  • The way around it (measured): stage the file in a call of its own, then commit.
  • My judgement (inferred): the cost is acceptable. It needs a text file under the large-file filter, which is rare, that is small and names one of three words. It fails closed (principle 1), and there is a simple way around it.
  • But it isn't disclosed: see required change 1. The refusal reason ("could not run (… not in its exact form …); not committing unscanned") doesn't say what to do instead.

3. Weakening gate: changed existing test lines, dd6b301 to 85a5bbd

  • commit-scan-stored, "begin like a pointer" (2 lines):
    • What changed: the expected reason went from "found" to "found, or could not run". The decision is still asserted as deny.
    • Why: the bytes in this case are small and contain the marker, so they are now refused before the scan.
    • Direction: the guard is not loosened, and the assertion is a little wider (inferred).
    • I'd pin the new reason ("looks like a large-file pointer"), but that isn't required.
  • commit-scan-stored, interactive staging with a diff filter (1 line):
    • What changed: the expected reason now accepts the outright refusal. The decision is still deny.
    • Direction: this follows a tightening, and the refusal now applies whatever the config.
  • commit-plan: git commit -p -m x was removed from the list of commits scanned with working-tree content.
    • Why: that form now throws, so it is refused.
    • Coverage: the refusal is tested in the new commit-scan-pointers file (-p, --interactive) and measured above.
    • Direction: this is a tightening.
  • commit-scan-lfs: one line added (the omitted-key case).
  • Overall: none of these lines loosens what the guard allows (inferred from the code, measured with the probes).

4. The named-pipe test and CI

  • At this head the test can't hang (inferred from the code, measured):
    • lstatSync runs before anything opens the object, and it doesn't open the path.
    • It passes in 0.23 s locally (measured) and in 245 ms in CI (shard 4/4, Node 24, measured from the log).
    • The link-to-device and folder variants are refused the same way (measured).
  • What a regression would do (inferred): if a later change dropped the check, readFileSync would block the test process's event loop. An in-process test timeout couldn't fire then. The CI shard jobs set no timeout-minutes, so the shard would wait for GitHub's 6-hour default.
  • Recommended, not required: run the hook in a child process with a spawn timeout in that file, so a regression fails in seconds.

5. Other checks

Required changes

  1. PR body, "What ordinary work this now refuses" (principle 5): add round 7's two new refusals, each with a way around it.
    • Interactive staging (add -p/-i, any spelling) before a commit in the same line, and an interactive commit (commit -p/--interactive). The way around it: stage in a call of its own, then commit (the refusal already says so).
    • A file of 1024 bytes or less, under the standard large-file filter, that names git-lfs, hawser or git-media but isn't the exact pointer, committed from the working tree or staged in the line. The way around it: stage it in a call of its own first (measured above).
    • Optionally, the marker refusal's reason could say the same.

This is a body edit. It doesn't need a new head, a fail-without or a CI run. It follows from principle 5, so it isn't an owner decision.

Recommended, not required:

  • (a) Bound the named-pipe test with a child-process timeout (section 4).
  • (b) Pin the new reason in the "begin like a pointer" test (section 3).
  • (c) Add round 7's attacks to the body's Adversarial pass. That section has no round-7 entries yet.

@jayden1711

Copy link
Copy Markdown
Owner Author

Delta final check (round 7), body re-check: PASS on 85a5bbd

The head is unchanged: 85a5bbde31bb1f0945a6e5797418ccfb913c002d (measured, gh pr view --json headRefOid). I read the body through the raw API. The code findings in my earlier comment (issuecomment-6332069416) still stand.

  • Required change 1 is done (read). "What ordinary work this now refuses" now lists both round-7 refusals, each with its way around:

    • interactive staging and interactive commits: stage in a call of its own, then commit;
    • small files under the large-file filter that name a pointer version but aren't the exact form: stage them in an earlier call.

    Both match what I measured.

  • Weakening gate (read, checked against the diff dd6b301..85a5bbd): four changed lines across three tests is correct.

    • Two lines in the "begin like a pointer" test.
    • One line in the interactive-staging test.
    • One line in the commit-plan unit test.
    • The large-file test's new line is an addition.
  • Round-7 Adversarial pass entry (read): each item matches a result I measured at this head. Pointer spellings, staged and untracked, are refused. Interactive forms, with and without config, are refused. Plain staging is allowed. The missing-key and mismatch cases are refused, and so is an object that isn't a regular file.

  • What remains (read):

  • No attribution lines in the body (measured, grep).

Nothing remains from my earlier comment.

The package budget line conflicted: main's values (2,170,000 / 6,750,000) are kept, as the merged package measures
2,085,053 / 6,541,354 with the web build, within them.

Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
@jayden1711

Copy link
Copy Markdown
Owner Author

Delta check (merge-only head c935e7e): CHANGES NEEDED (PR body only)

Head c935e7e = merge of origin/main 8df2304 (#918) into 85a5bbd. The code is fine; one body section is no longer true.

Checks (all measured):

REQUIRED (c), overclaim in the body:

  1. The "Package budget (a loosening, principle 9)" section says the PR changes PACKAGE_BUDGET.unpackedSize from 6,400,000 to 6,730,000. At c935e7e the PR no longer changes the budget at all: main's values from Receipts: signed, chained merge receipts anyone can verify offline (#717 slice 1) [PROTECTED: owner merges] #918 apply. The PR body still names a loosening this head doesn't contain. Mark the section as superseded by the merge, and state the measured size at c935e7e (2,085,053 / 6,541,354, within main's 2,170,000 / 6,750,000). The line "within the raised budget (2,050,000 / 6,730,000)" also needs to be scoped to 85a5bbd. This is a body edit only; no code change is needed.

Follow-ups (not required): none.

Note (inferred): the body's adversarial pass already lists a round 7, and DELTA's round cap says no PR goes past round 6 without the owner's rethink. The orchestrator should decide whether this body-only fix needs the owner.

@jayden1711

Copy link
Copy Markdown
Owner Author

Delta check, body re-check (head c935e7e): PASS

The head is unchanged at c935e7e; only the body was edited. I read the body through the raw API.

The required change from issuecomment-6332886090 is fixed (measured):

  • The section is now titled "Package budget (superseded by the merge of main)".
  • Its "Now (head c935e7e)" part gives main's Receipts: signed, chained merge receipts anyone can verify offline (#717 slice 1) [PROTECTED: owner merges] #918 budget (2,170,000 / 6,750,000). It says the PR no longer changes test/package-files.test.ts and that the loosening is not in the head. It gives the measured size 2,085,053 / 6,541,354, which matches my measurement at this head.
  • The old raise is kept under "History (heads dd6b301 to 85a5bbd)".
  • The Round 7 package line is scoped to 85a5bbd and points to the superseded section.
  • The file count reads "nine commit-scan test files (plus commit-plan.test.ts)", which is correct.

The code checks and CI from the previous comment still stand: run 39722718259 succeeded at this head, and that run includes the package-size test.

@jayden1711

Copy link
Copy Markdown
Owner Author

Second review (verification-scoped): PASS on c935e7e

Head checked: c935e7ec7c53bdd5aa0405bee59aaf24f44f0884, which merges origin/main 8df2304 into 85a5bbd. Main compared against: 8df2304. Fail-without base: dd6b301.

Scope (owner decision): this review re-ran the documented probe groups (rounds 2 to 7) and the round-7 fail-without, and checked the body against the merged code. It built no new payloads. Probe lines stay with the orchestrator (#707), so this comment uses IDs only.

1. What the merge changed (measured)

  • git diff --stat 85a5bbde c935e7ec lists only Receipts: signed, chained merge receipts anyone can verify offline (#717 slice 1) [PROTECTED: owner merges] #918's files (signed receipts: src/signed-receipts*, src/cli.ts, src/os/index.ts, docs, changelog.d/918-*, its tests) and test/package-files.test.ts.
  • The same diff limited to src/scan, src/guard, src/hook.ts, src/review.ts, src/pr-rules.ts, src/coordinator, docs/guardrails.md, this PR's changelog fragment, test/adversarial and test/commit-plan.test.ts is empty. The guard and its tests are byte-identical to the head that passed both reviews.
  • test/package-files.test.ts resolved to main's line (2,170,000 / 6,750,000). git diff --stat 8df23045 c935e7ec no longer lists that file, so the PR no longer changes the budget.

2. Documented probe groups, head vs main 8df2304 (measured)

Each group ran unchanged on both builds, with a temporary HOME, TMPDIR and state dirs, isolated git config, and recording stand-ins (exit 1) first on PATH for gh, sudo, systemctl, launchctl, loginctl, docker, claude, tailscale, useradd, usermod, setfacl and id.

Group Probes Head allows a line main refuses or finds? Head commits the token where main doesn't?
r7 interactive spellings and cost 36 no no
r6 pv / req / fifo (FF, FZ) / mm / edit 21 no no
r5 count / lfs / reads 46 no no
r4 (all groups) 46 no no
r3 (all groups) plus cost, cost2, ro 164 (85 classifier-only, not on main) no no
r2 (all groups) 76 no no

Details:

  • r7: IP1–IP20 are refused at head (main allows them). In the cost probes, K3, K7 and I8 are refused and the rest allowed, the same profile the round-7 delta check recorded.
  • r6: PV0 is found. PV1–PV8, PVU, RQ0, FF, FZ, MM1, MM2, ED1–ED3, CP1 and CP2 are refused, and ED0 is allowed. FF and FZ are refused as "not a regular file", in 425 ms and 679 ms. On main, ED1–ED3 commit the token.
  • r5:
    • Found at head: MV1, MV2, DUP1, DUP1w, DUP2, NEW2, IDX1–3, AM1–3, LF2u, LF7 and LF8.
    • Allowed at head: MV3, DUP3, LF4f and LF4l, which main allows too.
    • LF1 and LF1c are refused at head under round 7's inexact-pointer rule, where main finds them. They stay closed either way. (The previous verification table listed them as "found" at head, but the head refuses them.)
    • On main, LP1, SV1–3, SG1–3, CD1 and C5b commit the token.
  • r4:
    • Found on both: SL5–SLPw and AL2–4.
    • Allowed at head: C4, K1–K5, AL1, AL5 and AL6.
    • C6's marker is not written at head, and is written on main.
    • On main, C5, C9, GL1–4 and XA0–5 commit the token.
  • r3:
  • r2:
    • Allowed at head: S1c-1, S5, X1–X4 and O13–O16. Main allows these too, and head commits none of them.
    • On main, S11p, P3–P6, M3, W1–W7, O1 and O2 commit the token.
    • One difference from earlier runs, caused by the test setup: P1 didn't commit on main this time, because the recording gh stand-in exits 1. The decisions match earlier runs (main allows P1 and P2; head refuses both).

3. Fail-without and tests (measured)

  • I wrote test/adversarial/commit-scan-pointers.test.ts as a new file into a worktree at dd6b301. cmp shows it matches the head copy byte for byte (sha256 1d094c07…). Built and run there, it gave # pass 0, # fail 3:
    • test 1: expected deny, got allow;
    • test 2: the reason doesn't match (EISDIR read error instead of "not a regular file");
    • test 3: expected deny, got allow.
  • At head, the nine test/adversarial/commit-scan*.test.ts files plus test/commit-plan.test.ts gave # tests 63, # pass 63, # fail 0, # skipped 0. That includes the fifo file.
  • npm run typecheck at head: clean.
  • CI on c935e7e (gh pr checks): every check that ran passed, including all eight test shards on Node 22 and 24, dco, denylist, push-gate, scan, scope, new-deps and agentshield. The real-systemd, bootstrap and sign-in probe jobs were skipped.

4. Body claims after the merge

  • "Package budget (superseded by the merge of main)": true (measured).
    • After npm run build and npm run build:web at c935e7e, npm pack --dry-run --json --ignore-scripts gives a tarball of 2,085,053 and an unpacked size of 6,541,354 (540 files, web index present). These match the body exactly and are within main's 2,170,000 / 6,750,000.
    • The package-size test alone (--test-name-pattern='the package stays within its size budget') passes at head.
    • The PR no longer carries its own budget raise (section 1). The section labels the loosening as history, which is accurate.
  • Round 7, What this can never do, Compromised component and What remains: still true as the last verification found them. The guard, scan, hook, lock list and docs are unchanged by the merge (section 1, measured), and the probe results match (section 2). The lock globs in src/review.ts still cover src/hook*, src/guard/**, src/coordinator/** and src/**/*secret* (read at head).
  • "No reopened route, as far as measured": true at this head (section 2, measured).

Verdict

PASS on c935e7e.

  • No probe line that main refuses or finds is allowed at the head, and the head commits the token nowhere main doesn't.
  • The fail-without reproduces at dd6b301, and every commit-scan file passes at the head.
  • The body's claims, including the updated package-budget section, hold after the merge.

Required changes: none.

@jayden1711
jayden1711 merged commit 7b13c8b into main Oct 22, 2026
27 checks passed
@jayden1711
jayden1711 deleted the issue-971-commit-scan branch October 22, 2026 05:20
jayden1711 added a commit that referenced this pull request Oct 22, 2026
Main 7b13c8b (#746, #979). The one conflict was test/review.test.ts's locked GUARD_TREE list, resolved as the union
of both sides: this branch's src/confusables.ts and src/confusables-data.ts, and main's
src/coordinator/change-files.ts and src/guard/commit.ts.

Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
jayden1711 added a commit that referenced this pull request Oct 22, 2026
Conflicts, each resolved to keep both sides:
- .worklane/review.yaml: the engine-guards list keeps test/git-runner.test.ts and #903's vacuity tests.
- src/coordinator/change-files.ts: the runner's import; main's local SizeFile replaces the pr-rules import.
- src/coordinator/merge-flow.ts: the runner's imports with main's randomBytes; isAncestor keeps main's arguments
  through gitSpawnSync.
- src/scan/secrets.ts: main's new content-based scanCommit (#979) whole; the runner's imports for scanRange.
- src/worktrees.ts: main's removed() split, with the runner's calls.

Every raw git start the merge brought now goes through the runner, with the same arguments and options:
merge-flow.ts (the tip fetch, sync and async; merge-base; rev-list --merges; vacuityFor's git; branch -D; the
changed-lines spawn), scan/secrets.ts (gitOut, headId, the core.fsmonitor read), worktrees.ts
(removeWorktreeAsync), hook.ts (gitQuery) and vacuity.ts (baseTextReader). No entry is added to the scan's
exception list. Its src/scan/secrets.ts entry shrinks from 2 starts to 1, since #979 took the commit scan's
gitleaks `git` call out; the pin on that call is replaced by one on scanRange's `--log-opts` snapshot range.

Signed-off-by: Jayden Vasquez <jaydenvasquez1711@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

commit scan: cover every commit form and checkout the guard allows

1 participant