Record the two write-guard limits a live sweep exposed - #539
Merged
Conversation
Both were found while driving seventeen pull requests through the review loop, and neither was written down. A write inside a script file is unseen. The hook reads the command the agent runs, so for a script invocation that is one string and every mutation inside it reaches the server unparsed. This is the wider of the two, and it is one an agent opens by accident: batching fifteen repetitive pushes into a loop looks like tidiness rather than like switching a guard off. The boundary is the write, not the script, so a script that only reads or prepares local commits is unaffected. A push followed by a newline over-blocks. The argument list ends at && and not at a newline, so a later line's tokens are read as refspecs and the push denies as a direct push to a protected branch it never targeted. Safe in direction, but the denial names a bypass that was not attempted, and a guard that cries wolf is one an agent starts working around. The gh pr edit breakage found in the same session is already documented in the review runbook, workaround included, so nothing is added for it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Documents two newly observed limitations of the gh-write-guard hook in the agent-safety host setup docs, so future sweeps and agent sessions avoid inadvertently bypassing or being misled by the guard.
Changes:
- Add a limitation noting that writes executed inside a shell script are not visible to the hook, with a one-write-per-command workaround.
- Add a limitation noting that a
git pushfollowed by a newline can be mis-parsed and over-blocked, with the same workaround.
The bullet read as though the shell carried newlines into the push argument list and as though git denied the push. Neither is true: the hook splits the command string itself, and its split ends an argument list at && but not at a newline, so it is the hook that mis-parses and the hook that denies. Naming the wrong actor in a limits section is worse than saying nothing, since a reader debugging it would go looking at git. Also drops the unexplained "hub" qualifier on TODO.md, which reads oddly in the hub's own tree where TODO.md is simply TODO.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.
Suppressed comments (1)
host-setup/agent-safety/README.md:74
- In this bullet header, "never batch writes into one" is ambiguous (one what: one script, one command, one line). Since this section is documenting guard limits, tighten the wording to explicitly say "one script" (the actual limitation described).
- **A write inside a script file is unseen, so never batch writes into one.** The hook reads the command the agent runs, which for `bash deploy.sh` is that one string, so a `git push` or a `gh` mutation inside the script reaches the server without the hook ever parsing it. This is the widest hole in the kit and it is one an agent opens by accident, since collecting fifteen repetitive pushes into a loop looks like tidiness rather than like disabling a guard. Issue each write as its own command. A script that only reads, computes, or prepares local commits is fine, because the boundary is the write and not the script.
The bullet header read 'never batch writes into one', where one could be a script, a command, or a line. It is a script, which is the limitation the bullet then describes, so the header now says so. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ptr727
added a commit
that referenced
this pull request
Aug 3, 2026
…ndor groundwork (#542) Six commits, all from one session. Merged as a **merge commit**, never squashed, per the branching model. | Commit | PR | What it does | | --- | --- | --- | | `1ab9b32` | #537 | Deletes three stale PhotoCleaner `driftNotes` that described finished work | | `bf746aa` | #535 | Buckets `TODO.md` by the surface each entry changes, records five issues verified complete, refreshes the divergence ledger | | `69276d1` | #536 | Adds the byte-locked `AGENTS.md` "Fleet Bootstrap" section, the host-wide `fleet-bootstrap` block, and the `AUDIT.md` routing that sends an un-stood-up repo to `STANDUP.md` first | | `530dc0d` | #539 | Records two `gh-write-guard` limits found by running it: a write inside a script is unseen, and a push followed by a newline over-blocks | | `5f4c17f` | #540 | Makes `configure.sh check` verify what the payloads declare, closing a false clean where a `jq` failure left every setting reported as passing while nothing was compared | | `a549572` | #541 | Reports a verbatim section that never arrived as **absent** rather than as hand-modified | ## Why this promotion matters more than most Two of these change what the fleet is measured against, so until they reach `main` every downstream audit compares against ground truth that predates them. **#536 is the one with reach.** A downstream agent had no way to discover this repository. `AGENTS.md` is carried at `intent`, and the hub's only self-reference described *the hub*, which is false about a downstream repo, so an agent adapting the file correctly deleted it. Four repos now hold no hub reference at all and one holds no `AGENTS.md` either. The replacement is `verbatim` so adaptation cannot remove it, declared in `spec/files.json` and classified in `spec/section-model.md` so the audit reports its absence as drift rather than losing it silently. **#540 closes a false clean.** `check_settings` streamed its payload from a process substitution, and a `jq` failure there leaves the loop body unexecuted without tripping `set -e`. Every static setting reported as checked and passing while nothing was compared. Verified by negative test rather than by a clean pass: a malformed `settings.json` now fails with exit 1, an extra live rule fails the rule-set assertion, and a changed `copilot_code_review` parameter fails the parameters assertion. **#541 corrects a report that was actively misleading.** It accused seventeen repos of hand-editing `AGENTS.md` sections they had never been given. They still hold the pre-split monolithic file, so the sections are simply absent. A hand-edit is reconciled against a local decision, an absence is carried, and the report was naming the wrong one. ## Fleet state behind this Seventeen downstream re-vendor pull requests merged alongside this work, each gate-checked at merge time and each verified after. Four repos had `configure.sh` withdrawn from their re-vendor when review found they carry `ruleset-*.json` payload names the canonical does not resolve, which would have left `apply` and `check` aborting on files that do not exist. That payload migration is still owed and is tracked in `TODO.md`. ## Expected immediately after this merges The divergence ledger currently reports the hub itself as not carrying "Fleet Bootstrap" and as owing a `configure.sh` re-vendor. That is the ledger reading each repo's `main`, honestly, before this promotion. Regenerating after the merge is what makes the report meaningful again, and the `configure.sh` re-vendor list will collapse as repos take the corrected script. ## Verification `spec/validate.py` OK (21 cataloged), diff-scoped `prose_lint` clean, `markdownlint-cli2` clean, editorconfig clean, `gh-write-guard --selftest` PASS, 157 `prose_lint` unit tests pass.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two limits of the
gh-write-guardhook, both found while driving seventeen pull requests through the review loop in one session, and neither written down anywhere. They go in the kit's own "Scope and Limits" rather than the review runbook, since they are properties of the hook rather than of the Copilot workflow.A write inside a script file is unseen
The hook reads the command the agent runs. For
bash sweep.shthat is one string, so agit pushor aghmutation inside the script reaches the server without the hook ever parsing it.This is the wider of the two and the one worth acting on. It is a hole an agent opens by accident: collecting fifteen repetitive pushes into a loop reads as tidiness, not as switching a guard off. I nearly did exactly that in this sweep, and stopped only because the reason to script it and the reason not to were the same fact.
The rule that follows is short: issue each write as its own command. The boundary is the write and not the script, so a script that reads, computes, or prepares local commits is unaffected, which is how the sweep's fifteen commits were actually prepared.
A
git pushfollowed by a newline over-blocksThe push argument list terminates at
&&but not at a newline, so every token on a later line of the same command is read as a refspec. Measured against the installed hook:The push then denies as a direct push to a protected branch it never targeted. The direction is safe, since it blocks rather than admits, but the denial tells the agent it is bypassing a branch rule with admin power when it is pushing an ordinary feature branch. A guard that cries wolf is one an agent starts working around, which is the actual risk. The fix is tracked in
TODO.md; this records the limit and the workaround meanwhile, which is the same one-write-per-command rule as above.Not added, because it is already there
The same session hit
gh pr edit --bodyfailing on this host's gh 2.46.0 with a Projects-classic deprecation error. That is already documented in the review runbook, including the exactgh api -X PATCH ... -F body=@fileworkaround, so nothing is added for it. Checked before writing rather than after, since duplicating a rule into a rules file is a known past failure here.Verification
prose_lint.py: cleanmarkdownlint-cli2: 0 issues