Skip to content

fix: close guard hook git gaps and dispatch field-report defects - #355

Merged
jeremymcs merged 37 commits into
mainfrom
fix/issue-353-field-report
Oct 7, 2026
Merged

jeremymcs merged 37 commits into
mainfrom
fix/issue-353-field-report

Conversation

@jeremymcs

@jeremymcs jeremymcs commented Oct 4, 2026 •

Copy link
Copy Markdown
Member

Intent

Close the two findings from the GPT-6-SOL (codex CLI) second opinion of PR #355's branch: (1) a literal git -c core.fsmonitor= passed the guard while git executes the configured program during the read-only status subcommand, demonstrated as an archive write — fixed by denying -c/--config keys whose value git executes (fsmonitor, hooksPath, editors, pagers, sshCommand, askPass, filter/diff/merge drivers) with literal values as with parameters, ordinary settings unaffected; (2) worktree placement receipt release when idle was flagged as mid-milestone re-rooting — accepted as the documented design requested by issue #353 (a supported way to choose a shorter root), gated on zero linked worktrees so no stale path resolution exists; no change intended for item 2 beyond existing documentation.

What Changed

  • Guard hook (scripts/guard_hook.py, HOOKS.md): the hook now denies git -c/--config and git config keys whose value git executes or loads code through (for example core.fsmonitor, core.hooksPath, editors, pager.<cmd>, core.sshCommand, askpass, gpg programs, credential helpers, filter/diff/merge drivers, include.path). The denial applies to literal values and to parameters; ordinary settings still pass. The hook also parses git config modes, matches denied git options by abbreviated prefix, accepts a parameter in a read-only git argument only inside plain double quotes, denies zsh expansion flags and non-literal git words, and unwraps command runners so the git checks reach the command they run.
  • Worktree and sidecar lifecycle (isolation.py, worktree_paths.py, integration.py, dispatch_driver.py): sidecar removal now handles initialized submodules, read-only files and tree-deletion failures, with a direct-delete fallback and Windows long-path support. Finish recreates a missing verify sidecar from the recorded base. Landing tolerates uncommitted bookkeeping paths, and serial activation is refused while the task has a live parallel worktree. The placement pin is released when no linked worktree remains, so the next allocation reads GSD_PATH_WORKTREE_ROOT again (documented in DOCS.md).
  • Other fixes, with tests and synced skill copies: check_handoffs.py allows same-wave file overlap when one task transitively depends on the other (planner docs and templates updated); members.py validate and repair no longer reject the refs that Path creates; the daemon probe resolves status through the project launcher .gsd-path/status_runtime.py before the per-project runtime; install.py adds --runtime-provenance-source|patch|note for --runtime-upgrade, stored as metadata on the runtime declaration and outside the pinned digest.

Risk Assessment

⚠️ Medium: The head commit is a one-line, verified addition that satisfies intent item 1 for pager keys with no fail-open found, but the branch as a whole adds a large guard parser that needed many fix rounds, so it is safe to merge with follow-ups and not a low-risk change.

Testing

I first reproduced the reported archive write on real git. I then installed the base and HEAD guard hook in scratch projects and sent real hook events on stdin: HEAD denied all 37 executed-config and adversarial forms (base allowed 35 of them) and allowed all 17 ordinary and disabling forms. I also drove the placement pin and release on real git worktrees, and ran the 11 targeted tests; all passed. No baseline command ran before this step. The change has no UI, so the evidence is CLI transcripts and not screenshots.

  • Live validation: ✅ go - 6 of 6 scenarios driven live against the product
Scenario Result Live Evidence
An agent runs git -c core.fsmonitor=&lt;path&gt; status with a literal value (from the project root, with an archive operand, and after cd into the archive); the guard denies it and names the key ✅ pass live guard_transcript.txt, group S1: base ALLOWED, HEAD DENIED; message git -c core.fsmonitor configures a program git executes. exploit_repro.txt shows that real git runs the program and writes into the…
An agent passes each other executed key from the intent with a literal value (hooksPath, editors, pager, sshCommand, askPass, filter/diff/merge drivers, --config-env); the guard denies each one ✅ pass live guard_transcript.txt, group S1: 16 of 16 DENIED on HEAD; base ALLOWED 15 of them
Adversarial: an agent hides the executed key (upper case, attached -ckey=, quotes, -C . first, /usr/bin/git, command/env/nice/sudo/timeout runners, sh -c, brace expansion, persisted… ✅ pass live guard_transcript.txt, group S2: 21 of 21 DENIED on HEAD; base ALLOWED 20 of them
An agent uses ordinary settings and disabling forms (-c user.name=, -c color.ui=, core.fsmonitor=false, empty or bare key, pager.log=false, git config &lt;key&gt; read, plain runners, `git diff --… ✅ pass live guard_transcript.txt, group S3: 17 of 17 ALLOWED on HEAD and on base
A user sets a new GSD_PATH_WORKTREE_ROOT while one linked worktree exists; the next sidecar stays in the pinned location (no re-root during a milestone) ✅ pass live placement_transcript.txt: with 1 linked worktree, T003 resolves next to T002 in the pinned root; tests.test_worktree_placement also passes
A user sets a new GSD_PATH_WORKTREE_ROOT after the last linked worktree retires; the next worktree is created below the new root ✅ pass live placement_transcript.txt: with 0 linked worktrees, T003 is created below &lt;scratch&gt;/moved; tests.test_worktree_placement.test_release_workspace_lets_a_new_root_apply_after_retirement also passes
Evidence: Exploit reproduction on real git (why the fix is necessary)

$ git --version git version 2.54.0 (Apple Git-157) $ ls .project/archive/001-mvp # before plan $ git -c core.fsmonitor=/tmp/scratch/h.sh status # the command base allowed $ ls .project/archive/001-mvp # after: git ran the program during read-only status PWNED.md plan

$ git --version
git version 2.54.0 (Apple Git-157)
$ cat h.sh
#!/bin/sh
touch "/tmp/scratch/proj/.project/archive/001-mvp/PWNED.md"
$ ls .project/archive/001-mvp   # before
plan
$ git -c core.fsmonitor=/tmp/scratch/h.sh status   # the command base allowed
$ ls .project/archive/001-mvp   # after: git ran the program during read-only status
PWNED.md
plan
Evidence: Guard verdicts, base versus HEAD (54 commands)

base HEAD command ALLOWED DENIED git -c core.fsmonitor=/abs/h.sh status ALLOWED DENIED cd .project/archive/001-mvp && git -c core.fsmonitor=/abs/h.sh status ALLOWED DENIED git -c core.hooksPath=/tmp/hooks status ALLOWED DENIED git -c core.editor=/abs/h.sh log -1 ALLOWED DENIED git -c core.pager=/abs/h.sh log ALLOWED DENIED git -c core.sshCommand=/abs/h.sh ls-remote . ALLOWED DENIED git -c core.askPass=/abs/h.sh ls-remote . ALLOWED DENIED git -c filter.x.clean=/abs/h.sh status ALLOWED DENIED git -c diff.external=/abs/h.sh diff ALLOWED DENIED git -c merge.x.driver=/abs/h.sh status ALLOWED DENIED git -c CORE.FSMONITOR=/abs/h.sh status ALLOWED DENIED nice git -c core.fsmonitor=/abs/h.sh status ALLOWED DENIED sh -c 'git -c core.fsmonitor=/abs/h.sh status' ALLOWED DENIED git config core.fsmonitor /abs/h.sh ALLOWED DENIED git -c pager.log=/abs/h.sh log ALLOWED ALLOWED git status ALLOWED ALLOWED git -c user.name=Test log -1 ALLOWED ALLOWED git -c color.ui=never status ALLOWED ALLOWED git -c core.fsmonitor=false status ALLOWED ALLOWED git -c pager.log=false log -1 unexpected HEAD verdicts: 0 HEAD denial message for the reported command: gsd-path guard: GSD Path guard could not validate the tool request: git -c core.fsmonitor configures a program git executes; the guard cannot allow it


## S1 literal executed-config values (expect DENIED on HEAD)
base     HEAD     command
ALLOWED  DENIED   git -c core.fsmonitor=/abs/h.sh status
ALLOWED  DENIED   git -c core.fsmonitor=/abs/h.sh status .project/archive
ALLOWED  DENIED   cd .project/archive/001-mvp && git -c core.fsmonitor=/abs/h.sh status
ALLOWED  DENIED   git -c core.hooksPath=/tmp/hooks status
ALLOWED  DENIED   git -c core.editor=/abs/h.sh log -1
ALLOWED  DENIED   git -c sequence.editor=/abs/h.sh log -1
ALLOWED  DENIED   git -c core.pager=/abs/h.sh log
ALLOWED  DENIED   git -c core.sshCommand=/abs/h.sh ls-remote .
ALLOWED  DENIED   git -c core.askPass=/abs/h.sh ls-remote .
ALLOWED  DENIED   git -c filter.x.clean=/abs/h.sh status
ALLOWED  DENIED   git -c filter.x.smudge=/abs/h.sh status
ALLOWED  DENIED   git -c filter.x.process=/abs/h.sh status
ALLOWED  DENIED   git -c diff.external=/abs/h.sh diff
ALLOWED  DENIED   git -c diff.x.textconv=/abs/h.sh diff
ALLOWED  DENIED   git -c merge.x.driver=/abs/h.sh status
DENIED   DENIED   git --config-env=core.fsmonitor=FSM status

## S2 adversarial spellings (expect DENIED on HEAD)
base     HEAD     command
ALLOWED  DENIED   git -c CORE.FSMONITOR=/abs/h.sh status
ALLOWED  DENIED   git -ccore.fsmonitor=/abs/h.sh status
ALLOWED  DENIED   git -c 'core.fsmonitor=/abs/h.sh' status
ALLOWED  DENIED   git -c "core.fsmonitor=/abs/h.sh" status
ALLOWED  DENIED   git -C . -c core.fsmonitor=/abs/h.sh status
ALLOWED  DENIED   git --no-pager -c core.fsmonitor=/abs/h.sh status
ALLOWED  DENIED   /usr/bin/git -c core.fsmonitor=/abs/h.sh status
ALLOWED  DENIED   command git -c core.fsmonitor=/abs/h.sh status
ALLOWED  DENIED   env git -c core.fsmonitor=/abs/h.sh status
ALLOWED  DENIED   nice git -c core.fsmonitor=/abs/h.sh status
ALLOWED  DENIED   sudo -uroot git -c core.fsmonitor=/abs/h.sh status
ALLOWED  DENIED   timeout 5 git -c core.fsmonitor=/abs/h.sh status
ALLOWED  DENIED   sh -c 'git -c core.fsmonitor=/abs/h.sh status'
ALLOWED  DENIED   git -c {core.fsmonitor=/abs/h.sh,status}
DENIED   DENIED   read x < v.txt; git -c "core.fsmonitor=$x" status
ALLOWED  DENIED   git config core.fsmonitor /abs/h.sh
ALLOWED  DENIED   git config core.fsmonitor /abs/h.sh && git status
ALLOWED  DENIED   git -c "pager.status=touch .project/archive/001-mvp/N" status
ALLOWED  DENIED   git -c pager.log=/abs/h.sh log
ALLOWED  DENIED   git -c PAGER.LOG=/abs/h.sh log
ALLOWED  DENIED   git config pager.log /abs/h.sh

## S3 ordinary settings and disabling forms (expect ALLOWED on HEAD)
base     HEAD     command
ALLOWED  ALLOWED  git status
ALLOWED  ALLOWED  git -c user.name=Test log -1
ALLOWED  ALLOWED  git -c color.ui=never status
ALLOWED  ALLOWED  git -c core.quotepath=false status
ALLOWED  ALLOWED  git -c core.fsmonitor=false status
ALLOWED  ALLOWED  git -c core.fsmonitor= status
ALLOWED  ALLOWED  git -c core.fsmonitor status
ALLOWED  ALLOWED  git -c pager.status=false status
ALLOWED  ALLOWED  git -c pager.log=false log -1
ALLOWED  ALLOWED  git config pager.log
ALLOWED  ALLOWED  git config core.fsmonitor
ALLOWED  ALLOWED  git --no-pager log -1
ALLOWED  ALLOWED  nice make
ALLOWED  ALLOWED  timeout 5 npm test
ALLOWED  ALLOWED  sudo -upostgres psql
ALLOWED  ALLOWED  cd .project/archive/001-mvp && git diff --text
ALLOWED  ALLOWED  git log --grep='fix$'

unexpected HEAD verdicts: 0

HEAD denial message for the reported command:
gsd-path guard: GSD Path guard could not validate the tool request: git -c core.fsmonitor configures a program git executes; the guard cannot allow it
Evidence: Guard driver script
"""Drive scripts/guard_hook.py as a host does: one JSON hook event on stdin.
Each version is installed at <project>/scripts/, the project holds .project/archive/001-mvp."""
import json, subprocess, sys
base, head, hook = sys.argv[1:4]
def verdict(root, command):
    script = root + "/scripts/guard_hook.py"
    p = subprocess.run([sys.executable, script], input=json.dumps(
        {"tool_name": "Bash", "tool_input": {"command": command}, "cwd": root}),
        capture_output=True, text=True, cwd=root)
    return ("DENIED" if p.returncode == 2 else "ALLOWED" if p.returncode == 0 else f"rc={p.returncode}"), p.stderr.strip()
H = hook
A = ".project/archive/001-mvp"
groups = {
 "S1 literal executed-config values (expect DENIED on HEAD)": ("DENIED", [
  f"git -c core.fsmonitor={H} status",
  f"git -c core.fsmonitor={H} status .project/archive",
  f"cd {A} && git -c core.fsmonitor={H} status",
  f"git -c core.hooksPath=/tmp/hooks status",
  f"git -c core.editor={H} log -1",
  f"git -c sequence.editor={H} log -1",
  f"git -c core.pager={H} log",
  f"git -c core.sshCommand={H} ls-remote .",
  f"git -c core.askPass={H} ls-remote .",
  f"git -c filter.x.clean={H} status",
  f"git -c filter.x.smudge={H} status",
  f"git -c filter.x.process={H} status",
  f"git -c diff.external={H} diff",
  f"git -c diff.x.textconv={H} diff",
  f"git -c merge.x.driver={H} status",
  f"git --config-env=core.fsmonitor=FSM status",
 ]),
 "S2 adversarial spellings (expect DENIED on HEAD)": ("DENIED", [
  f"git -c CORE.FSMONITOR={H} status",
  f"git -ccore.fsmonitor={H} status",
  f"git -c 'core.fsmonitor={H}' status",
  f'git -c "core.fsmonitor={H}" status',
  f"git -C . -c core.fsmonitor={H} status",
  f"git --no-pager -c core.fsmonitor={H} status",
  f"/usr/bin/git -c core.fsmonitor={H} status",
  f"command git -c core.fsmonitor={H} status",
  f"env git -c core.fsmonitor={H} status",
  f"nice git -c core.fsmonitor={H} status",
  f"sudo -uroot git -c core.fsmonitor={H} status",
  f"timeout 5 git -c core.fsmonitor={H} status",
  f"sh -c 'git -c core.fsmonitor={H} status'",
  f"git -c {{core.fsmonitor={H},status}}",
  f'read x < v.txt; git -c "core.fsmonitor=$x" status',
  f"git config core.fsmonitor {H}",
  f"git config core.fsmonitor {H} && git status",
  f'git -c "pager.status=touch {A}/N" status',
  f"git -c pager.log={H} log",
  f"git -c PAGER.LOG={H} log",
  f"git config pager.log {H}",
 ]),
 "S3 ordinary settings and disabling forms (expect ALLOWED on HEAD)": ("ALLOWED", [
  "git status",
  "git -c user.name=Test log -1",
  "git -c color.ui=never status",
  "git -c core.quotepath=false status",
  "git -c core.fsmonitor=false status",
  "git -c core.fsmonitor= status",
  "git -c core.fsmonitor status",
  "git -c pager.status=false status",
  "git -c pager.log=false log -1",
  "git config pager.log",
  "git config core.fsmonitor",
  "git --no-pager log -1",
  "nice make",
  "timeout 5 npm test",
  "sudo -upostgres psql",
  f"cd {A} && git diff --text",
  "git log --grep='fix$'",
 ]),
}
fails = 0
for title, (want, cmds) in groups.items():
    print(f"\n## {title}\n{'base':8} {'HEAD':8} command")
    for c in cmds:
        b, _ = verdict(base, c); h, msg = verdict(head, c)
        ok = h == want; fails += not ok
        print(f"{b:8} {h:8} {c.replace(H, '/abs/h.sh')}{'' if ok else '   <-- UNEXPECTED: ' + msg[:160]}")
print(f"\nunexpected HEAD verdicts: {fails}")
_, m = verdict(head, f"git -c core.fsmonitor={H} status")
print("\nHEAD denial message for the reported command:\n" + m)
Evidence: Worktree placement pin and release transcript

GSD_PATH_WORKTREE_ROOT=<scratch>/moved, linked worktrees = 1 T003 would be placed at: ~/.gsd-path/projects/<repo>/<id>/task/T003 -> pin HELD (not re-rooted mid-milestone) retired T002, linked worktrees = 0 T003 worktree: <scratch>/moved/<repo>/<id>/task/T003 -> pin RELEASED, new root applied

T001 worktree: ~/.gsd-path/projects/dd67711bbaa48421098e29d68574ea901b321da31430326e6f4d55c178d363fd/69eeb1f4d6875831fbc2369fd257aebeb2a6bf580d892244a4f241cb649cb90e/task/T001
T002 worktree: ~/.gsd-path/projects/dd67711bbaa48421098e29d68574ea901b321da31430326e6f4d55c178d363fd/69eeb1f4d6875831fbc2369fd257aebeb2a6bf580d892244a4f241cb649cb90e/task/T002

GSD_PATH_WORKTREE_ROOT=<scratch>/moved, linked worktrees = 1
T003 would be placed at: ~/.gsd-path/projects/dd67711bbaa48421098e29d68574ea901b321da31430326e6f4d55c178d363fd/69eeb1f4d6875831fbc2369fd257aebeb2a6bf580d892244a4f241cb649cb90e/task/T003
-> pin HELD (not re-rooted mid-milestone)

retired T002, linked worktrees = 0
T003 worktree: <scratch>/moved/dd67711bbaa48421098e29d68574ea901b321da31430326e6f4d55c178d363fd/69eeb1f4d6875831fbc2369fd257aebeb2a6bf580d892244a4f241cb649cb90e/task/T003
-> pin RELEASED, new root applied
Evidence: Placement driver script
"""Drive skills/path/scripts/isolation.py against a real git repository:
the placement pin holds while a linked worktree exists and releases at zero."""
import os, subprocess, sys
from pathlib import Path
sys.path.insert(0, sys.argv[1] + "/skills/path/scripts")
import isolation
root = Path(sys.argv[2]).resolve(); repo = root / "repo"; repo.mkdir(parents=True)
g = lambda *a: subprocess.run(["git", "-C", str(repo), *a], check=True, capture_output=True, text=True).stdout.strip()
g("init", "-q", "-b", "gsd-path/M001"); (repo / "f").write_text("x"); g("add", "-A")
g("-c", "user.name=t", "-c", "user.email=t@t", "commit", "-qm", "init"); base = g("rev-parse", "HEAD")
show = lambda p: str(p).replace(str(root), "<scratch>")
linked = lambda: len(g("worktree", "list", "--porcelain").split("\n\n")) - 1
t1 = isolation.isolate_task(repo, base, "T001", 2); t2 = isolation.isolate_task(repo, base, "T002", 2)
print("T001 worktree:", show(t1["worktree"])); print("T002 worktree:", show(t2["worktree"]))
isolation.retire(repo, Path(t1["worktree"]), t1["task_branch"], False)
os.environ["GSD_PATH_WORKTREE_ROOT"] = str(root / "moved")
print(f"\nGSD_PATH_WORKTREE_ROOT=<scratch>/moved, linked worktrees = {linked()}")
pinned = isolation.sidecar_root(repo, "task", "T003")
print("T003 would be placed at:", show(pinned))
assert pinned == Path(t2["worktree"]).parent / "T003", "pin must hold while a linked worktree exists"
print("-> pin HELD (not re-rooted mid-milestone)")
isolation.retire(repo, Path(t2["worktree"]), t2["task_branch"], False)
print(f"\nretired T002, linked worktrees = {linked()}")
t3 = isolation.isolate_task(repo, base, "T003", 2)
print("T003 worktree:", show(t3["worktree"]))
assert Path(t3["worktree"]).is_relative_to(root / "moved"), "new root must apply at zero linked worktrees"
print("-> pin RELEASED, new root applied")
isolation.retire(repo, Path(t3["worktree"]), t3["task_branch"], False)

Pipeline

Updates from git push no-mistakes

... (15 earlier update rounds omitted to keep the PR body within GitHub's 65536-char limit; full history is in the run log.)

⚠️ **Review** - 3 infos

🔧 Fix applied.
3 issues (2 warnings, 1 info) still open:

  • ⚠️ scripts/guard_hook.py:2310 - The round-7 rewrite of the line-continuation logic in shell_tokens is fail-open for two continuations in sequence, and it removes a denial that HEAD1 had. joins (lines 2310-2313) tests the characters next to each continuation in command, where each continuation is already a space. Thus, for \&lt;newline&gt;\&lt;newline&gt;, each continuation sees the other one as a space, no join is recorded, and the words on the two sides stay literal == &#34;bare&#34;. Probes from a project root with .project/archive/001-mvp: git config core.fsmoni\&lt;newline&gt;\&lt;newline&gt;tor /abs/h.sh is DENIED on HEAD1 and ALLOWED on HEAD (regression in this round). git c\&lt;newline&gt;\&lt;newline&gt;onfig core.fsmonitor /abs/h.sh is ALLOWED on HEAD and HEAD~1, so the round-7 claim (a subcommand split by a line continuation is denied) does not hold for this form; git con\&lt;newline&gt;\&lt;newline&gt;\&lt;newline&gt;fig ... and the \r\n form are also ALLOWED. I ran the key form and the subcommand form with bash and zsh (git 2.54.0) in a scratch repository: each one set core.fsmonitor to the program path. The owner instructions for rounds 6 and 7 require a DENIED verdict for a continuation that splits the key or the subcommand word, and require that each prior verdict stays. The new tests contain only one continuation, so they pass with this defect. The base commit ALLOWS these forms (it has no git config check), so this is not a regression against base. The defect is in code that the fix rounds added and that exceeds the intent, which names only -c/--config keys. The remedy needs the owner's decision: (a) revert the persisted-form check to the minimal fix (remove denied_git_config_argument, ShellToken.redirection, ShellToken.literal, the continuation mark logic, GIT_CONFIG_EXPANDING_WORD, the subcommand literal check, their tests and HOOKS.md text) and keep the -c/--config-env denial; or (b) keep the check and repair joins: find the adjacent characters in marked and skip a run of marks, so a run of continuations between two non-space characters is one join. Add the two probes above as DENIED regression tests if (b) is selected. I did not test remedy (b).
  • ⚠️ scripts/guard_hook.py:2611 - The intent failure stays reachable from the project root through a brace list in the value word of a git global option. The guard reads the word after -c, -C, --git-dir, --work-tree, --namespace or --config-env as one value (lines 2608-2612), but the shell makes several arguments from an unquoted brace list. The round-7 literal-word check applies only to the subcommand word and the git config arguments. Probe: git -c {core.fsmonitor=/abs/h.sh,status} is ALLOWED on HEAD. The guard reads the key as {core.fsmonitor, finds no subcommand, and git_command returns None. I ran it with bash and zsh in a scratch repository: the shell ran git -c core.fsmonitor=/abs/h.sh status and git executed the program. This is the sequence that intent item 1 closes (a literal git -c core.fsmonitor=&lt;path&gt; passed the guard while git executes the configured program during the read-only status subcommand). The same form is DENIED after cd .project/archive/001-mvp (by other rules), and the plain, quoted, attached, escaped and single-continuation -c forms are all DENIED on HEAD. Also ALLOWED on HEAD: git -C {.,config,core.fsmonitor,/abs/h.sh}, git -c {a.b=c,config,core.fsmonitor,/abs/h.sh}, git --git-dir {.git,config,core.fsmonitor,/abs/h.sh}; the -C form set core.fsmonitor in the scratch repository with bash and zsh. The base commit ALLOWS all of these, so this is not a regression. The remedy needs the owner's decision because it changes the verdict for global option values of each git command: the earliest shared boundary is the global-option loop in git_command; deny a separate value word that is bare and matches GIT_CONFIG_EXPANDING_WORD (brace list or range) with the existing cannot be resolved message. A narrower form covers only -c and --config-env, but then the -C/--git-dir forms stay open for git config. The executable word has the same class of gap on base and HEAD ({git,config,core.fsmonitor,/abs/h.sh}, g\&lt;newline&gt;it config ..., also {git,reset,--hard}); it comes from the base tokenizer, not from this change. I did not test the remedy.
  • ℹ️ scripts/guard_hook.py:2639 - The round-7 subcommand and comment rules do what the owner specified for the named probes, and the common forms keep their verdicts. I compared base, HEAD~1 and HEAD for about 180 commands. DENIED on HEAD: git {config,core.fsmonitor,/abs/h.sh}, git con\&lt;newline&gt;fig ..., git con&#39;&#39;fig ..., git config core.fsmonitor /tmp/h.sh # --get (also with a tab or a redirection before #). ALLOWED and unchanged: git status with ;, |, &amp;&amp;, &gt;, 2&gt;&amp;1, a subshell, $(...), backticks, bash -c, eval, sudo, env, a quoted subcommand, a continuation after a space, a heredoc with a continuation in its body, and non-ASCII text. New denials on forms that base ALLOWS: git st\atus, git &#39;stat&#39;us, git {status,log}, git re\&lt;newline&gt;set --hard, git status\&lt;newline&gt;;ls (continuation directly between the subcommand and an operator), and git config --get user.name # note (each git config with a trailing comment). The owner instruction for round 7 requires the comment denial, and the other forms are not usual. The generated script copies in skills/*/scripts are identical to scripts/. The new tests execute the guard and assert its verdict, so they obey the test-quality rule. I did not run the test suite.

🔧 Fix applied.
4 issues (1 error, 2 warnings, 1 info) still open:

  • 🚨 scripts/guard_hook.py:2685 - An unquoted parameter in the value word of a git global option passes when the guard sees no subcommand word. git_command returns None at line 2685 before it raises for parameter_arguments, so nothing is inspected. Probes from a project root with .project/archive/001-mvp: read x &lt; v.txt; git -c $x, git -c $x, git --git-dir $x and git --git-dir=$x are ALLOWED on HEAD. The base commit DENIES all four (git argument $x cannot be resolved by the guard). The verdict changed in commit f6f2067 (fix: guard hook precision for read-only commands), which is in this change; each later commit keeps it. I ran read x &lt; v.txt; git -c $x with bash (git 2.54.0) in a scratch repository where v.txt contains core.fsmonitor=/abs/h.sh status: bash split the value and git executed the program. zsh does not split an unquoted $x, so the path is bash only. This is the sequence that intent item 1 closes, and the intent requires the denial with literal values as with parameters. The same commands with a visible subcommand (git -c $x status) are DENIED, and the form with an assigned value (x=&#39;core.fsmonitor=/abs/h.sh status&#39;; git -c $x) is DENIED. Remedy: raise the existing cannot be resolved error when parameter_arguments is not empty before the return None at line 2685, which restores the base verdict. Add the four probes as DENIED regression tests. I did not test the remedy.
  • ⚠️ scripts/guard_hook.py:2646 - The intent failure stays reachable through a brace list in a git global option word that takes no value. The round-8 fix applies require_resolved_git_word to the value words of -c, -C, --git-dir, --work-tree, --namespace, --config-env and to the subcommand word, but the loop at line 2646 skips each other word that starts with - without a check. Probes: git -{p,c} core.fsmonitor=/abs/h.sh status and X=/abs/h.sh git --{no-pager,config-env=core.fsmonitor=X} status are ALLOWED on HEAD. The guard reads -{p,c} as one option with no value and reads core.fsmonitor=/abs/h.sh as the subcommand. I ran the two forms with bash and zsh (git 2.54.0) in a scratch repository: the shell made git -p -c core.fsmonitor=/abs/h.sh status and git executed the program in each of the four runs. The base commit ALLOWS these forms, so this is not a regression. The new tests contain a brace list only in a value word or the subcommand word, so they pass with this gap. The remedy, not the defect, needs the owner's decision, because it changes the verdict for the option words of each git command and continues the non-literal word machinery that the fix rounds built (this is one more round with a fail-open next to it): (a) apply require_resolved_git_word to each word that the global-option loop reads, so a non-literal option word gets the existing cannot be resolved error; or (b) stop here and accept that non-literal words outside the named forms stay open. Add the two probes as DENIED regression tests if (a) is selected. I did not test the remedy.
  • ⚠️ scripts/guard_hook.py:251 - credential.helper is a config key whose value git executes, and it is not in GIT_EXECUTED_CONFIG_KEYS or GIT_EXECUTED_CONFIG_SECTIONS. Probe: git -c credential.helper=/abs/h.sh ls-remote http://127.0.0.1:&lt;port&gt;/x.git is ALLOWED on base and HEAD; git -c credential.https://x.helper=/abs/h.sh ls-remote origin and git config credential.helper /abs/h.sh are ALLOWED too. I ran the first form (git 2.54.0) against a local HTTP server that answers 401 with WWW-Authenticate: Basic: git executed the program during ls-remote, which is in CLOSED_READ_GIT_COMMANDS. The path needs a remote that asks for credentials. The intent criterion is denying -c/--config keys whose value git executes; its list (fsmonitor, hooksPath, editors, pagers, sshCommand, askPass, filter/diff/merge drivers) does not name credential helpers. Thus the owner must decide if the list is complete or gives examples. The remedy needs the owner's decision because credential.helper has common legitimate values (store, osxkeychain, cache) and a denial changes the verdict for them: add credential.helper and the credential.&lt;url&gt;.helper form to the executed keys (the empty disabling value git -c credential.helper= fetch then still passes), or leave the key out as outside the intent list. I could not confirm an execution for pager.&lt;cmd&gt; or core.alternateRefsCommand, so I do not report them.
  • ℹ️ scripts/guard_hook.py:2313 - The head commit does what the owner specified for the two round-8 closures. I compared base, HEAD1 and HEAD for about 140 commands. DENIED on HEAD: git config core.fsmoni\&lt;newline&gt;\&lt;newline&gt;tor /abs/h.sh, git c\&lt;newline&gt;\&lt;newline&gt;onfig ..., git -c\&lt;newline&gt;\&lt;newline&gt;core.fsmonitor=/abs/h.sh status, git -c {core.fsmonitor=/abs/h.sh,status}, the brace forms after -C, --git-dir, --work-tree, --namespace, and git -c &#39;a.b=c&#39;{,config,...}. ALLOWED and unchanged: git -c user.name=&#39;A B&#39; log -1, git -c user.name=&#34;A B&#34; commit -m x, git -C &#34;sub&#34; status, git -Csub status, git --git-dir=.git --work-tree=. status, continuations after a space in multi-line git commands, and git config reads. New denials on forms that HEAD1 ALLOWS: git -c core.excludesFile=*.ign status, git -c log.date=format:%Y?%m log -1, git -c a.b=&#39;it&#39;\&#39;&#39;s&#39; log -1, git -C sub\ dir status; the owner instruction for round 8 requires a denial for a backslash and for each sequence that can become several words, and these forms are not usual. The mixed state is set only when the word text with its plain quotes removed equals the token, so a tokenizer offset error can only add a denial. The new tests execute the guard and assert its verdict, so they obey the test-quality rule. I did not run the test suite.

🔧 Fix applied.
2 issues (1 warning, 1 info) still open:

  • ⚠️ scripts/guard_hook.py:439 - The intent item 1 failure stays reachable when a program wrapper that the guard does not know comes before git. COMMAND_WRAPPERS contains only builtin, call, command, exec, so git_command sees nice, nohup, timeout, sudo or noglob as the executable, returns None at line 2567, and no git check runs. Probes from a project root with .project/archive/001-mvp: nice git -c core.fsmonitor=/abs/h.sh status, nohup git -c core.fsmonitor=/abs/h.sh status, timeout 5 git -c core.fsmonitor=/abs/h.sh status, sudo git -c core.fsmonitor=/abs/h.sh status, noglob git -c core.fsmonitor=/abs/h.sh status, nice git config core.fsmonitor /abs/h.sh, nice git -{p,c} core.fsmonitor=/abs/h.sh status and timeout 5 git -c $x are ALLOWED on base, HEAD~1 and HEAD. I ran nice git -c core.fsmonitor=&lt;script&gt; status (git 2.54.0) in a scratch repository: git executed the script and it created .project/archive/001-mvp/PWNED.md. This is not a regression and not caused by the head commit: the same gap exists for each git rule on the base commit (nice git reset --hard, sudo git push --force, nohup git clean -fdx are ALLOWED there). The guard also ALLOWS ./h.sh, nice /abs/h.sh and python3 /abs/h.py directly, so the owner must decide if this path is in the scope of the intent. The remedy, not the defect, needs the owner's decision, because it changes the verdict of each git rule in the guard and extends the change beyond the -c key denial: (a) unwrap these wrappers at the shared boundary wrapped_command_tokens (add them to COMMAND_WRAPPERS with their option and duration operands), which makes all git checks apply; or (b) stop here and accept the gap as a known limit of the guard. Add the nice and timeout 5 probes as DENIED regression tests if (a) is selected. I did not test the remedy.
  • ℹ️ scripts/guard_hook.py:2655 - The head commit does what the owner specified for the three round-9 closures. I compared base, HEAD1 and HEAD for about 180 commands in scratch repositories. (1) git -c $x, git --git-dir $x, git --git-dir=$x and bash -c &#39;git -c $x&#39; are DENIED on base, ALLOWED on HEAD1 and DENIED on HEAD. (2) git -{p,c} core.fsmonitor=/abs/h.sh status, the --{no-pager,config-env=...} form, git -p{,} status, and the same forms after -C ., --git-dir .git, command, env, sh -c, eval and in $(...) are DENIED on HEAD. (3) git -c credential.helper=/abs/h.sh ls-remote ..., the credential.&lt;url&gt;.helper form in each letter case, and git config credential.helper /abs/h.sh are DENIED; git -c credential.helper= ls-remote origin, git config --get credential.helper, git config --unset credential.helper and git -c credential.username=me fetch stay ALLOWED. Unchanged and ALLOWED: git --no-pager log -1, git -p log -1, git --exec-path=/usr/lib/git status, git --version, git -C sub, git --no-pager log --format=&#34;$t %H&#34;, git --no-pager show &#34;HEAD:$f&#34;. Expected new denials from the owner's decision: git -c credential.helper=store fetch, git config --global credential.helper osxkeychain, git -c credential.helper=&#39;!gh auth git-credential&#39; push. The only other new denial I found is a global option with a parameter and no subcommand (git --git-dir=&#34;$PWD/.git&#34;), which the base commit denies too. The new tests execute the guard and assert its verdict, so they obey the test-quality rule. HOOKS.md agrees with the code. I did not run the test suite.

🔧 Fix applied.
2 issues (1 warning, 1 info) still open:

  • ⚠️ scripts/guard_hook.py:2562 - The round-10 check (MODELED_PROGRAMS and the new branch in command_invocation) gives wrong verdicts in two directions. I ran the guard from HEAD~1 and from HEAD, each in a scratch repository, on about 75 commands.

(1) New false denials. These commands are ALLOWED on HEAD~1 and DENIED on HEAD with executable ... can run git in a way the guard cannot inspect; run git directly, without &lt;program&gt;: which git, type git, hash git, man git, whereis git, pgrep git, brew install git, apt-get install -y git, gh pr create --title git, gh repo clone cli/cli git, pytest tests/git, python3 scripts/tool.py git, diff a git, awk &#39;{print}&#39; git, make git, docker run --rm alpine git --version, ssh host git status. None of them runs git through a path that the guard could inspect, and most run no git at all. The message tells the user to run git without which, which is not a usable instruction. The check matches the basename of each argument, so a path argument such as tests/git also denies. daemon/gsd_daemon/launch.py:269 tells the user to run sudo apt install git; the same rule denies that form. This is a new guard false positive on read-only commands, the class that this branch removes.

(2) The wrapper failure stays reachable. These commands are ALLOWED on HEAD: nice sh -c &#39;git -c core.fsmonitor=/abs/h.sh status&#39;, g=git; nice $g -c core.fsmonitor=/abs/h.sh status, nice &#34;$g&#34; -c core.fsmonitor=/abs/h.sh status, nice gi{t,t} status, nice gi? -c core.fsmonitor=/abs/h.sh status, nice $(echo git) -c core.fsmonitor=/abs/h.sh status. The check reads only literal argument words, and it does not look behind a known wrapper that comes after the unknown program. I did not execute these forms in a shell; the round-10 review executed the plain nice git -c core.fsmonitor=&lt;script&gt; status form. The new tests contain only the literal git word, so they pass with this defect.

This component is not required by the intent. The intent names denying -c/--config keys whose value git executes ... with literal values as with parameters; it does not name programs before git. The guard also allows ./h.sh, nice /abs/h.sh and python3 /abs/h.py directly, so the check does not close the execution path that it targets. The defects are in code that a fix round added beyond the original finding, so I report one finding and not more repairs. The remedy needs the owner's decision, because round 10 selected this rule: (a) recommended: revert commit 27cc609 (remove GIT_PROGRAMS, MODELED_PROGRAMS, the branch at line 2562, its tests and the HOOKS.md sentence) and accept the wrapper gap as a known limit that exists on the base commit for each git rule; or (b) narrower form: deny only when the executable is one of a named list of command-runner programs (nice, nohup, timeout, sudo, doas, noglob, time with options) and deny each invocation of those programs that the guard cannot unwrap, which removes the false denials but keeps a list to maintain. If (b) is selected, add which git, man git, brew install git and pytest tests/git as ALLOWED regression tests and nice sh -c &#39;git -c core.fsmonitor=/abs/h.sh status&#39; as a DENIED one.

  • ℹ️ scripts/guard_hook.py:2562 - The head commit gives the verdicts that the owner listed for round 10. DENIED on HEAD and ALLOWED on HEAD~1: nice -n 5 git status, nice &#39;git&#39; -c core.fsmonitor=/abs/h.sh status, nice GIT ..., nice ./git status, sudo -u root git -c core.fsmonitor=/abs/h.sh status, env nice git ..., nice env git ..., nice command git ..., watch git status, time -p git -c core.fsmonitor=/abs/h.sh status. Unchanged and ALLOWED: ls vendor/git, cd vendor/git, mkdir git, find . -name git, echo git | tee out.txt, printf &#39;%s\n&#39; git, time git status, ls .git. Unchanged and DENIED: time git -c core.fsmonitor=/abs/h.sh status, xargs git -c core.fsmonitor=/abs/h.sh status, find . -exec git -c core.fsmonitor=/abs/h.sh status \;, and the if, !, { } and ( ) forms. The new tests execute the guard and assert its verdict, so they obey the test-quality rule. I did not run the test suite.

🔧 Fix applied.
2 issues (1 warning, 1 info) still open:

  • ⚠️ scripts/guard_hook.py:2551 - The round-11 rule (EXEC_PREFIX_PROGRAMS and the branch in command_invocation) still gives wrong verdicts in two directions. I ran the guard from base, HEAD~1 and HEAD, each in an initialized scratch tree.

(1) False denials that stay. These commands are ALLOWED on base and DENIED on HEAD: sudo apt install git, sudo apt-get install -y git, timeout 60 brew install git, sudo -u git psql, nice grep -r git README.md, sudo ls vendor/git, sudo rm -rf vendor/git, nohup npm run git, watch -n 5 which git. None of them runs git. daemon/gsd_daemon/launch.py:269 and daemon/app/src/screen.ts:49 tell the user to run sudo apt install git. The check matches the basename of each argument, so a path argument such as vendor/git denies; the owner's rule said exactly git or git.exe, but the verified round-10 denials nice ./git status and nice /usr/bin/git ... need the basename match.

(2) The new executable.startswith(&#34;-&#34;) condition is not in the owner's rule. It exists because time is removed as a shell word before this check, so -p becomes the executable. It denies each segment whose first word starts with - and that has a git word: time -p grep git README.md, time -p ls git and time -p make git are DENIED on HEAD and ALLOWED on base, while time ls git and time which git are ALLOWED. The message says run git directly, without -p, which is not a usable instruction.

(3) The wrapper failure stays reachable. ALLOWED on HEAD: doas git -c core.fsmonitor=/abs/h.sh status, and the same with ionice, chronic, caffeinate, gtimeout 5, nice.exe, sudo.exe, and nice sh -c &#39;git -c core.fsmonitor=/abs/h.sh status&#39;. noglob git -c core.fsmonitor=/abs/h.sh status is DENIED on HEAD~1 and ALLOWED on HEAD: the round-10 instruction listed noglob as DENIED, the round-11 set does not contain it. HOOKS.md now states that these forms are not covered, so the document agrees with the code.

This component is not required by the intent, which names denying -c/--config keys whose value git executes, not programs before git. The owner selected the rule in rounds 10 and 11, so items (1) and (3) are mostly the result of that decision; item (2) and the noglob change are new. The remedy needs the owner's decision: (a) recommended: revert commits 27cc609 and 37ab072 (remove GIT_PROGRAMS use in this branch, EXEC_PREFIX_PROGRAMS, the branch at line 2551, its tests and the HOOKS.md sentences) and accept the wrapper gap as a known limit that exists on the base commit for each git rule; or (b) accept the current verdicts as they are. I recommend no more repairs on this machinery.

  • ℹ️ scripts/guard_hook.py:543 - The head commit gives the verdicts that the owner listed for round 11. ALLOWED again on HEAD (DENIED on HEAD~1): which git, and the test list type git, hash git, man git, whereis git, pgrep git, brew install git, apt-get install -y git, gh pr create --title git, gh repo clone cli/cli git, pytest tests/git, make git. DENIED on HEAD: nice git status, nice -- git status, nice git -c core.fsmonitor=/abs/h.sh status, /usr/bin/nice git status, NICE git status, &#39;nice&#39; git status, time -p git status, env env git status, ls | nice git status, echo x &amp;&amp; sudo git push. Unchanged: git status, time git status, env FOO=1 git status, nice make, sudo systemctl status ALLOWED; git -c core.fsmonitor=/abs/h.sh status, time git -c core.fsmonitor=... and env git -c core.fsmonitor=... DENIED by the -c key rule. The env and time entries of the set apply only after another prefix (env env, env time), because both words are removed before the check. The changed tests execute the guard and assert its verdict, so they obey the test-quality rule. I did not run the test suite.

🔧 Fix applied.
3 issues (1 error, 1 warning, 1 info) still open:

  • 🚨 scripts/guard_hook.py:2543 - The round-12 unwrap opens a new archive write path. The loop at line 2543 discards the options of the command runner and their values without a check, then the guard judges only the payload. Two runners write a file through their own options. I ran base, HEAD~1 and HEAD in scratch trees with .project/archive/001-mvp.

(1) time -o FILE writes FILE (BSD and GNU time). ALLOWED on HEAD, DENIED on base and HEAD~1: nice time -o .project/archive/001-mvp/N ls, env time -o .project/archive/001-mvp/N ls, /usr/bin/time -o .project/archive/001-mvp/N ls, timeout 5 time -o .project/archive/001-mvp/N ls, nice time --output=.project/archive/001-mvp/N ls, nice time -ao .project/archive/001-mvp/N ls, nice time -o .project/archive/001-mvp/N git status, and cd .project/archive/001-mvp &amp;&amp; nice time -o N ls. I executed nice time -o .project/archive/001-mvp/N ls and cd .project/archive/001-mvp &amp;&amp; env time -o N2 ls on macOS: each one created the file in the archive.

(2) sudo -e (sudoedit) opens each operand in an editor. ALLOWED on HEAD, DENIED on base and HEAD~1: sudo -e cat .project/archive/001-mvp/plan/PLAN.md and cd .project/archive/001-mvp &amp;&amp; sudo -e cat plan/PLAN.md. The guard reads them as cat &lt;file&gt;. I did not execute this form.

The cause is one change: on base, a runner in archive context was an unknown program and was denied; on HEAD the payload (ls, cat, git log) is a read command, so the segment passes, and the skipped runner words are not examined. The round-12 tests contain no runner option that names a file, so they pass with this defect.

This defect is in machinery that fix rounds 10 to 12 built. The intent names denying -c/--config keys whose value git executes; it does not name programs before git. Round 12 already recommended a revert, and this is the third round with a wrong verdict in this component, now a fail-open that the base commit does not have. The remedy needs the owner's decision: (a) recommended: revert commits 27cc609, 37ab072 and 71e75f8 (remove EXEC_PREFIX_VALUE_OPTIONS, TIMEOUT_DURATION, the -p skip and the unwrap branch in command_invocation, their tests and the HOOKS.md sentences), and accept the runner gap for the git -c rule as a known limit that the base commit has for each git rule; or (b) keep the unwrap and deny time -o/--output/-a and sudo -e/--edit, and deny each runner when the working directory or an argument is in the archive. I do not recommend (b): it adds one more rule set to this component.

  • ⚠️ scripts/guard_hook.py:2560 - The unwrap denies ordinary commands that base and HEAD1 allow. timeout &#34;$T&#34; make and timeout ${T:-5} make are DENIED on HEAD with executable $T cannot be resolved by the guard; name the program literally: TIMEOUT_DURATION (line 2560) accepts only a literal number, so the duration parameter becomes the executable. The message names the wrong word. Also DENIED on HEAD and ALLOWED on base: nice $CMD, sudo &#34;$CMD&#34;, nohup $CMD &amp;, timeout 10 $RUNNER test, watch -n 2 $CMD, sudo $(which node) x.js. The second group agrees with the base rule for a bare $CMD (DENIED on base), so only the duration case is a clear error; the test timeout $t git status asserts that denial as intended. The opposite direction also exists but is not a regression against base: timeout .5 git -c core.fsmonitor=/abs/h.sh status and watch -q 3 git -c core.fsmonitor=/abs/h.sh status are ALLOWED on HEAD (DENIED on HEAD1, ALLOWED on base), because .5 and 3 become the executable. These defects are in the same round-12 unwrap component; the revert in finding guard-unwrap-wrapper-options-open-archive-write removes them. I recommend no separate repair.
  • ℹ️ scripts/guard_hook.py:2499 - The head commit gives the verdicts that the owner listed for round 12. ALLOWED on HEAD and DENIED on HEAD~1: sudo apt install git, sudo -u git psql. ALLOWED on all three: nice make, timeout 5 npm test, sudo -E -u root ls, nohup npm run build, time -p make, sudo --preserve-env=PATH make install, stdbuf -oL -eL python3 x.py. DENIED on HEAD by the ordinary git rule: nice git -c core.fsmonitor=/abs/h.sh status. Archive writes behind a runner stay DENIED: nice touch .project/archive/001-mvp/N, sudo -u root rm -rf .project/archive/001-mvp, sudo -D .project/archive/001-mvp touch N, cd .project/archive/001-mvp &amp;&amp; nice git diff --output=N, and the watch forms with &gt; or ; in a quoted word. The changed tests execute the guard and assert its verdict, so they obey the test-quality rule. I did not run the test suite.

🔧 Fix applied.
3 issues (2 warnings, 1 info) still open:

  • ⚠️ scripts/guard_hook.py:2598 - The round-13 runner option scan does not parse a short option with an attached value, so it gives wrong verdicts in the two directions. I ran base, HEAD~1 and HEAD in scratch trees.

(1) New false denials. Line 2598 tests each letter of a short word against the file options, including the letters of an attached value. sudo -upostgres psql, sudo -udeploy ls, sudo -ujenkins make and sudo -uDeploy ls are DENIED on HEAD with sudo -upostgres writes, edits or enters a file or directory, because the user name contains e, D or R. Base and HEAD~1 ALLOW them; sudo -u postgres psql is ALLOWED on HEAD. The message is false: the command names no file.

(2) The payload is skipped. runner_option_in (line 2515) reads the last letter of the word as the option. sudo -uroot git -c core.fsmonitor=/abs/h.sh status, sudo -hlocalhost git -c core.fsmonitor=/abs/h.sh status and sudo -uroot git config core.fsmonitor /abs/h.sh are ALLOWED on HEAD: the last letter t is the value option -t, so the guard takes git as its value and judges a later word as the program. sudo -u root git -c core.fsmonitor=/abs/h.sh status is DENIED. Base ALLOWS these forms, so this is not a regression against base, but HOOKS.md says that the guard checks the command a runner runs. HEAD~1 has the same defect.

Minor items in the same code: sudo -h (help) and nice -n are DENIED on HEAD with lacks a value (line 2608; ALLOWED on base and HEAD1), and timeout infinity make and timeout 1e3 make are DENIED on HEAD (line 2623; GNU timeout accepts the two forms; ALLOWED on base and HEAD1).

The round-13 tests contain no attached short value, so they pass with these defects. The defects are in the runner unwrap that fix rounds 10 to 13 built; the intent names only the git -c/--config keys. The owner kept the unwrap in rounds 12 and 13, so the remedy needs the owner's decision: (a) revert commits 27cc609, 37ab072, 71e75f8 and 181c357 and accept the runner gap that the base commit has; or (b) keep the unwrap and parse a short word from left to right: a file option letter denies, the first value option letter stops the scan, and the remaining text of the word (or the next word when no text remains) is its value. With (b), add sudo -upostgres psql as an ALLOWED test and sudo -uroot git -c core.fsmonitor=/abs/h.sh status as a DENIED test.

  • ⚠️ scripts/guard_hook.py:2523 - The new file option denial does not hold when the option word is a parameter in double quotes. unresolved_runner_word accepts each double-quoted parameter (line 2523), and the file option check then reads only the literal text. read x &lt; v.txt; nice /usr/bin/time &#34;-$x&#34; ls, read x &lt; v.txt; nice time &#34;-$x&#34; git status and read x &lt; v.txt; sudo &#34;-$x&#34; touch N are ALLOWED on HEAD. I ran the first command with zsh in a scratch tree where v.txt contains o.project/archive/001-mvp/N: time created .project/archive/001-mvp/N. A value D.project/archive/001-mvp gives the same result for sudo -D. Base and HEAD~1 also ALLOW these forms, so this is not a regression; but the commit and HOOKS.md claim that time -o, sudo -e, sudo -D, sudo -R and watch -s are denied, and HOOKS.md documents the exception (each other runner option must be a literal word or a parameter in double quotes). sudo &#34;--chdir=$x&#34; touch N is DENIED, because the option name is literal. The component parameter in double quotes in a runner option word exceeds the round-13 instruction, which asked to deny a parameter where the unwrap expects a runner operand. The remedy narrows allowed commands (watch --interval=&#34;$t&#34; ls changes to DENIED), so the owner must decide: the narrower form denies each parameter in a word that starts with - when the parameter comes before = or the word has no =, and keeps the double-quote exception only for a separate value word (sudo -u &#34;$USER&#34; ls). Add the nice time &#34;-$x&#34; ls probe as a DENIED test and correct the HOOKS.md sentence. The revert in finding guard-runner-short-cluster-attached-value option (a) also removes this component.
  • ℹ️ scripts/guard_hook.py:2578 - The head commit gives the verdicts that the owner listed for round 13. DENIED on HEAD and ALLOWED on HEAD1: nice time -o .project/archive/001-mvp/N ls, and the env, timeout 5, /usr/bin/time, --output= and -ao forms. timeout &#34;$T&#34; make is DENIED with a message that names the literal duration. With an archive working directory or an archive operand a runner is not unwrapped and has the base verdict again (cd .project/archive/001-mvp &amp;&amp; nice ls, nice cat .project/archive/001-mvp/plan/PLAN.md: DENIED on base and HEAD, ALLOWED on HEAD1). The round-12 allowed forms keep their verdict (nice make, timeout 5 npm test, sudo -E -u root ls, time -p make, stdbuf -oL -eL python3 x.py). The intent item 1 holds: git -c core.fsmonitor=/abs/h.sh status, core.pager and core.hooksPath are DENIED and git -c user.name=x log is ALLOWED. The owner's instruction also makes these commands DENIED outside the archive: /usr/bin/time -o /tmp/t.txt make, sudo -D /srv ls, sudo -R /mnt ls, nice -n $N make. The changed tests execute the guard and assert its verdict, so they obey the test-quality rule. I did not run the test suite.

🔧 Fix applied.
2 issues (1 warning, 1 info) still open:

  • ⚠️ scripts/guard_hook.py:251 - The intent item 1 names pagers in the set of -c/--config keys whose value git executes, but the guard denies only core.pager. git also runs the value of pager.&lt;cmd&gt; as the pager program when the value is not a boolean. GIT_EXECUTED_CONFIG_KEYS (line 251) and GIT_EXECUTED_CONFIG_SECTIONS (line 255) contain no pager section. Probes on HEAD from a project root with .project/archive/001-mvp: git -c &#34;pager.status=touch .project/archive/001-mvp/N&#34; status, git -c pager.log=/abs/h.sh log, git -c PAGER.LOG=/abs/h.sh log and the persisted form git config pager.log /abs/h.sh are ALLOWED, while git -c core.pager=/abs/h.sh log and git config core.pager /abs/h.sh are DENIED. I ran git -c &#34;pager.status=&lt;script&gt;&#34; status with git 2.54.0 in a scratch repository: with a terminal on stdout (through script -q /dev/null) git ran the script; with no terminal git did not run it. core.pager has the same terminal condition and is denied, so the two keys must have the same verdict. Remedy: treat each key in the pager section as an executed key, and let only a git boolean value pass (true, false, yes, no, on, off, 1, 0, empty; no case sensitivity), so git -c pager.diff=false diff stays ALLOWED. Add git -c pager.log=/abs/h.sh log and git config pager.log /abs/h.sh as DENIED tests and git -c pager.diff=false diff as an ALLOWED test, and name pager.&lt;cmd&gt; in the HOOKS.md key list.
  • ℹ️ scripts/guard_hook.py:2519 - The head commit gives the verdicts that the owner listed for round 14. I compared base, HEAD1 and HEAD in scratch repositories. ALLOWED on HEAD: sudo -upostgres psql, sudo -udeploy ls, sudo -ujenkins make, sudo -u &#34;$USER&#34; ls, sudo -h, nice -n, timeout infinity make, timeout 1e3 make, stdbuf -oL -eL python3 x.py, watch -tn1 ls, and about 60 other usual runner forms. DENIED on HEAD: sudo -uroot git -c core.fsmonitor=/abs/h.sh status, sudo -hlocalhost git -c ..., nice time -oN ls, nice time -pao N ls, nice time &#34;-o&#34; F ls, nice time -&#39;o&#39; F ls, nice time $&#39;-o&#39; F ls, nice time -\o N ls, sudo -ED dir touch N, and the quoted parameter option words. The only verdict changes from HEAD1 to DENIED are option words that are not fully literal (sudo --user=&#34;$USER&#34; ls, nice -n&#34;$N&#34; make, stdbuf -o&#34;$M&#34; make, watch -n&#34;$t&#34; ls); the owner's round-14 instruction requires them. A quote, comment or continuation in a different part of the command does not cause a false denial of a literal runner option. One limit stays: sudo -s &#39;&lt;command string&gt;&#39; and sudo -i &#39;&lt;command string&gt;&#39; are ALLOWED on base, HEAD~1 and HEAD, the same class as the watch one-word form that HOOKS.md already documents as not checked. The new tests execute the guard and assert its verdict, so they obey the test-quality rule. I did not run the test suite.

🔧 Fix applied.
3 infos still open:

  • ℹ️ scripts/guard_hook.py:262 - The head commit gives the verdicts that the owner listed for round 15. I compared base, HEAD1 and HEAD, each with its own scripts/ tree in a scratch repository that contains .project/archive/001-mvp. DENIED on HEAD and ALLOWED on base and HEAD1: git -c &#34;pager.status=touch .project/archive/001-mvp/N&#34; status, git -c pager.log=/abs/h.sh log, git -c PAGER.LOG=/abs/h.sh log, git config pager.log /abs/h.sh, git --config-env=pager.log=X log. ALLOWED on HEAD: git -c pager.status=false status, git -c pager.log=false log, git -c pager.log= log, git -c pager.log log, git -c pager.branch=false branch, git config pager.log, git config --get pager.log, git config --unset pager.log, git -c color.pager=false log, git -c user.name=x log. The (&#34;&#34;,) suffix makes config_key_in match each key that has the pager. prefix and a non-empty remainder; it does not match core.pagerx or color.pager. The HOOKS.md sentence agrees with the code. The new tests execute the guard and assert its verdict, and the DENIED cases fail on HEAD~1, so they obey the test-quality rule. I did not run the test suite.
  • ℹ️ scripts/guard_hook.py:2811 - For the owner's awareness: the head commit also changes these commands from ALLOWED (base and HEAD1) to DENIED, although git executes nothing for them: git -c pager.log=true log, git -c pager.diff=no diff (also 0, off), git config pager.log false and git config --global pager.branch false. The cause is the existing model: GIT_CONFIG_DISABLING_VALUES contains only the empty value and false, and the persisted git config form accepts no disabling value (git config core.fsmonitor false is DENIED on HEAD1 for the same reason). The round-15 instruction asked for the existing disabling-value exceptions (bare key, empty value, false), so the code does what the owner specified; I report no defect. git -c pager.&lt;cmd&gt;=false &lt;cmd&gt; and git --no-pager &lt;cmd&gt; stay ALLOWED as the supported forms.
  • ℹ️ scripts/guard_hook.py:262 - PAGER=/abs/h.sh git log and export PAGER=/abs/h.sh; git log are ALLOWED on base, HEAD~1 and HEAD, while GIT_PAGER=/abs/h.sh git log is DENIED on all three. git uses PAGER as the pager program when GIT_PAGER and core.pager are not set, with the same terminal condition as core.pager. I did not execute this form, I only read the guard verdict. This is not a regression and it is not a -c/--config key, so it is outside the stated intent; a denial would also block the usual form PAGER=cat git log. No action in this change.
✅ **Test** - passed

✅ No issues found.

  • Live validation: ✅ go - 6 of 6 scenarios driven live against the product
Scenario Result Live Evidence
An agent runs git -c core.fsmonitor=&lt;path&gt; status with a literal value (from the project root, with an archive operand, and after cd into the archive); the guard denies it and names the key ✅ pass live guard_transcript.txt, group S1: base ALLOWED, HEAD DENIED; message git -c core.fsmonitor configures a program git executes. exploit_repro.txt shows that real git runs the program and writes into the…
An agent passes each other executed key from the intent with a literal value (hooksPath, editors, pager, sshCommand, askPass, filter/diff/merge drivers, --config-env); the guard denies each one ✅ pass live guard_transcript.txt, group S1: 16 of 16 DENIED on HEAD; base ALLOWED 15 of them
Adversarial: an agent hides the executed key (upper case, attached -ckey=, quotes, -C . first, /usr/bin/git, command/env/nice/sudo/timeout runners, sh -c, brace expansion, persisted… ✅ pass live guard_transcript.txt, group S2: 21 of 21 DENIED on HEAD; base ALLOWED 20 of them
An agent uses ordinary settings and disabling forms (-c user.name=, -c color.ui=, core.fsmonitor=false, empty or bare key, pager.log=false, git config &lt;key&gt; read, plain runners, `git diff --… ✅ pass live guard_transcript.txt, group S3: 17 of 17 ALLOWED on HEAD and on base
A user sets a new GSD_PATH_WORKTREE_ROOT while one linked worktree exists; the next sidecar stays in the pinned location (no re-root during a milestone) ✅ pass live placement_transcript.txt: with 1 linked worktree, T003 resolves next to T002 in the pinned root; tests.test_worktree_placement also passes
A user sets a new GSD_PATH_WORKTREE_ROOT after the last linked worktree retires; the next worktree is created below the new root ✅ pass live placement_transcript.txt: with 0 linked worktrees, T003 is created below &lt;scratch&gt;/moved; tests.test_worktree_placement.test_release_workspace_lets_a_new_root_apply_after_retirement also passes
  • Reproduced the reported write on real git 2.54.0: git -c core.fsmonitor=&lt;script&gt; status created .project/archive/001-mvp/PWNED.md in a scratch repository
  • python3 drive_guard.py &lt;base project&gt; &lt;head project&gt; &lt;hook&gt;: sent 54 Bash hook events on stdin to scripts/guard_hook.py, installed at &lt;project&gt;/scripts/ in two scratch projects (base 565e40a9 and HEAD 9b36e3ab), and compared the verdicts
  • python3 drive_placement.py &lt;worktree&gt; &lt;scratch&gt;: ran isolation.isolate_task, sidecar_root and retire on a real scratch repository on branch gsd-path/M001 with GSD_PATH_WORKTREE_ROOT changed
  • python3 -m unittest tests.test_worktree_placement tests.test_guard_hook.GuardHookTests.test_denies_executed_config_keys_but_allows_plain_settings tests.test_guard_hook.GuardHookTests.test_allows_parameters_in_read_only_git_arguments (11 tests, OK)
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

check_member's reserved-prefix refusal is a join-time admission rule
(docs/multi-repo-work.md). Shared with validate/repair it made every
locked member fail 'member refs collide with coordinator names' from
build start onward -- permanently after a first ship, since the member
milestone tag is never deleted. Refuse reserved refs only at add time.
Fixes one item of #353.
The plan gate rejected any same-wave overlap without consulting the
dependency graph it already built, while build_state.ready enforces the
real invariant (overlap only among concurrent tasks) and the runtime
dispatches same-wave dependencies in order. Skip the rejection when one
task transitively depends on the other; unordered overlap, cycles, and
later-wave dependencies are unchanged.
Fixes one item of #353.
- git arguments may carry $var when the subcommand is read-only
  (log/show/diff/status); write-capable subcommands keep the refusal
- archive context evaluates read-only-ness per pipeline segment, so
  cat | wc of allowlisted reads passes; heredoc bodies no longer flip
  a wrapped payload into archive context
- cd targets expand tracked assignments (dir=/x && cd "$dir")
- quoted destructive paths may contain ASCII spaces inside one shell
  unit; injection-relevant characters stay denied
- unprovable archive commands (ssh, python -) deny with an honest
  not-provably-read-only reason instead of claiming a write
Fixes one item of #353.
…cher

_runtime_status still executed the legacy per-project
.gsd-path/runtime/pipeline_state.py that current installs no longer
write and migration removes, so every declared-runtime project --
stock or hotfixed -- rendered Unverified amber. Invoke the project
launcher first and keep the legacy exec as the fallback.
Fixes one item of #353.
--runtime-upgrade gains --runtime-provenance-source/-patch/-note
flags storing owner-recorded audit metadata next to the digest.
Provenance is metadata only: it never joins the hashed manifest input
and never weakens validate_runtime; absent provenance equals stock.
Upgrades carry an existing entry forward instead of wiping it, and a
git checkout source records its HEAD when no patch ref is given.
Fixes one item of #353.
- activate-task without --task-branch refuses serial activation while
  the task's parallel worktree is live, instead of silently flipping
  the primary copy and stranding finish on a sidecar it never had
- finish recreates a deleted serial verify sidecar from the recorded
  base (ensure_verify_sidecar), lifting reproduction into isolation as
  reproduce_verify_sidecar; a stale registration or branch clears
- serial landing tolerates uncommitted primary bookkeeping
  (BOOKKEEPING_PATHS), matching the parallel gate, attest, and the
  landing proof; unrelated dirt stays refused with a clearer message
- sidecar retirement is submodule-aware: deinit, forced removal, then
  a long-path-safe delete fallback, instead of failing forever on
  worktrees whose submodules were ever initialized; dirty sidecars
  keep git's refusal without --force
- retiring the last sidecar releases the pinned workspace receipt, so
  GSD_PATH_WORKTREE_ROOT applies again at the next milestone
Fixes the remaining items of #353.
- a provenance entry carries forward only while the pinned digest is
  unchanged; upgrading to different bytes drops the stale hotfix record
- --runtime-provenance-* errors whenever --runtime-upgrade is absent
- the direct-delete fallback runs only for git's tree-deletion
  failures, so any other cleanup error leaves the sidecar intact
  (test_member_cleanup_surfaces_git_failures) and rmtree failures
  surface as IsolationError
- fd-qualified redirections (2>) count as archive write attempts
- release comment matches the idle-time release semantics
@jeremymcs jeremymcs changed the title fix: address the 1.4.0 Windows multi-repo field report fix: correct dispatch, guard, probe, and sidecar defects from the 1.4.0 multi-repo field report Oct 4, 2026
…ption

Unbraced zsh expansion flags ($=x, $~x, $^x, $+x) matched no parameter
syntax, so they never entered the parameter proof while zsh word-splits
them exactly like the braced forms the proof denies. A backslash-escaped
marker kept a proven-quoted flag although the shell executes the literal
text and the token keeps the bare marker. Both now deny: any $-position
covered by no recognized form routes into the proof chain, and an escaped
marker is never a proven parameter.
@jeremymcs jeremymcs changed the title fix: correct dispatch, guard, probe, and sidecar defects from the 1.4.0 multi-repo field report fix: close guard hook, dispatch, and runtime gaps from the 353 field report Oct 4, 2026
The guard scanned -c/--config values only for parameters, so a literal
core.fsmonitor path passed while git executes the configured program
during an otherwise read-only subcommand (demonstrated archive write
via git -c core.fsmonitor=<hook> status .project/archive). Keys whose
value git runs as a program -- fsmonitor, hooksPath, editors, pagers,
sshCommand, askPass, filter/diff/merge drivers -- are now denied with
literal values as with parameters; ordinary settings keep working.
@jeremymcs jeremymcs changed the title fix: close guard hook, dispatch, and runtime gaps from the 353 field report fix: close guard hook git gaps and dispatch field-report defects Oct 5, 2026
@jeremymcs
jeremymcs merged commit 47dc43d into main Oct 7, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant