-
Notifications
You must be signed in to change notification settings - Fork 0
Agent write-safety: repo-scope + never-fabricate-id + never-suppress-write rules, runbook fix, and a portable enforcement kit (#364/#365/#366) #367
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 15 commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
d28a2ff
Add Repository Boundaries and Write Safety rules; fix the runbook foo…
ptr727 e0c1ecc
Add portable per-machine agent write-safety kit (#365)
ptr727 3c5fe71
Fix 2>&1 suppression detection, quote the launcher, house-style the k…
ptr727 20f9ffa
Tighten node-id detection, harden python selection, title-case + de-s…
ptr727 75b460d
Restore install.ps1 to CRLF
ptr727 7c427ae
Fix the 2>&1 wording in the runbook and a semicolon in install.py (Co…
ptr727 d4d224d
Address Copilot round 4 on the write-safety kit (#367)
ptr727 9325942
Use a reference-style link in the kit README (#367)
ptr727 8dfc5ff
Fix CLAUDE_HOME tilde expansion; record gh review-request API traps (…
ptr727 3fd3c5a
Actually block the || : force-success tail; align the pattern lists (…
ptr727 c38d9ed
Fail gracefully on corrupt settings.json; propagate exit code on Wind…
ptr727 0fab303
Align doc snippets with actual behavior (#367)
ptr727 d699ecf
Deny quoted -R cross-repo writes; title-case README H1; verify python…
ptr727 cf9b1e3
Do not false-deny a suppression token quoted inside a write body (#367)
ptr727 fe4f733
Handle escaped quotes when stripping; mark Verify shell explicitly (#…
ptr727 8353f17
Match the README to the installed CLAUDE.md heading (#367)
ptr727 6b377c3
Guarantee a single hook registration across multiple Bash groups (#367)
ptr727 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,8 @@ | ||
| { | ||
| // claude-md-safety.md is a fragment the installer appends into ~/.claude/CLAUDE.md (which already | ||
| // has its own H1), so it intentionally opens at H2. MD041 (first line must be a top-level heading) | ||
| // does not apply to an appended snippet. This nested config affects only this directory. | ||
| "config": { | ||
| "MD041": false | ||
| } | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| # Agent Write-Safety Kit | ||
|
|
||
| Per-machine, user-account-scoped guards against an agent making a mis-targeted GitHub **write** under the maintainer's identity. Deploy it as the **first thing on any new system** where Claude Code runs with the `gh` credentials logged in (WSL, Linux, macOS, Proxmox, Windows). | ||
|
|
||
| ## What It Installs | ||
|
|
||
| Into `~/.claude/` (or `%USERPROFILE%\.claude\` on Windows): | ||
|
|
||
| - **`hooks/gh-write-guard.py`** - a PreToolUse hook that denies the three write footguns behind the cross-repo comment incident: a state-changing `gh` call whose output is discarded, a GraphQL mutation passing a **literal** node id instead of a `$variable`, and a `gh` write whose explicit target is outside the checkout's `origin`. Reads and everything else pass through. It fires even in autonomous / bypass-permissions sessions, which is how the incident happened. | ||
| - **A `## GitHub write safety` section in `CLAUDE.md`** - the same three rules as behavioral guidance, loaded into every session on the machine (including ad-hoc work outside any project). It mirrors the committed `AGENTS.md` "Repository Boundaries and Write Safety" rules, which only reach fleet repos. | ||
|
|
||
| The hook is the mechanical backstop. The CLAUDE.md rules and the carried AGENTS.md rules are the behavioral layer. Prose alone is not enough - the incident happened under prose rules - so both ship. | ||
|
|
||
| ## Install (Idempotent - Safe to Re-Run to Update) | ||
|
|
||
| ```sh | ||
| # Linux / WSL / macOS / Proxmox | ||
| host-setup/agent-safety/install.sh | ||
| ``` | ||
|
|
||
| ```powershell | ||
| # Windows | ||
| host-setup\agent-safety\install.ps1 | ||
| ``` | ||
|
|
||
| Both are thin wrappers around `install.py`, so every OS runs one tested code path. The installer self-tests the hook before registering it, merges the settings.json entry without clobbering other keys, and updates the CLAUDE.md block in place (marker-delimited) rather than duplicating it. | ||
|
|
||
| **Restart Claude Code sessions on the machine afterward** so the new hook and CLAUDE.md load. | ||
|
|
||
| ## Verify (POSIX Shell) | ||
|
|
||
| ```sh | ||
| python3 ~/.claude/hooks/gh-write-guard.py --selftest # decision matrix: all cases pass | ||
| grep -c 'agent-safety v' ~/.claude/CLAUDE.md # expect 2 (start + end marker) | ||
| ``` | ||
|
|
||
| On Windows PowerShell: | ||
|
|
||
| ```powershell | ||
| py -3 "$env:USERPROFILE\.claude\hooks\gh-write-guard.py" --selftest # all cases pass | ||
| (Select-String 'agent-safety v' "$env:USERPROFILE\.claude\CLAUDE.md").Count # expect 2 | ||
| ``` | ||
|
|
||
| Live end-to-end (in any repo): attempt a discarded-output write and confirm the Bash tool is blocked: | ||
|
|
||
| ```sh | ||
| gh api graphql -f query='mutation{noop}' -F t="PRRT_x" >/dev/null 2>&1 || true # blocked by the hook | ||
| ``` | ||
|
|
||
| ## Manual settings.json Shape (for Reference) | ||
|
|
||
| The installer writes this. It is here so you can inspect or hand-place it: | ||
|
|
||
| ```json | ||
| { | ||
| "hooks": { | ||
| "PreToolUse": [ | ||
| { "matcher": "Bash", "hooks": [ { "type": "command", "command": "\"python3\" \"<home>/.claude/hooks/gh-write-guard.py\"" } ] } | ||
| ] | ||
|
ptr727 marked this conversation as resolved.
|
||
| } | ||
| } | ||
| ``` | ||
|
|
||
| ## Scope and Limits | ||
|
|
||
| - **Per-machine.** `~/.claude/` does not travel, so run the installer on each box. This is the rollout that [#365][issue-365] tracks. | ||
| - **Precision over recall.** The hook denies the specific dangerous shapes with high confidence rather than gating every write, so it never blocks legitimate work. A shape it does not catch still falls under the behavioral rules. | ||
| - **Opaque targets are unseen.** The hook cannot see the repository behind a GraphQL node id, which is exactly why rule 2 blocks a *literal* id at all - a captured `$variable` is trusted. Likewise, the cross-origin check only runs when an `origin` can be resolved and the write names an explicit `-R`/`repos/<owner>/<repo>` target. A write from a non-git directory, or one whose target is only a node id, is evaluated by rules 1 and 2 alone. | ||
| - **Not a credential control.** A fine-grained PAT limited to owned repositories is a separate, stronger structural guard (a hard `403` on any non-owned repo) and is left to per-machine credential setup, out of this kit. | ||
|
|
||
| <!-- Repo --> | ||
| [issue-365]: https://github.com/ptr727/ProjectTemplate/issues/365 | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| <!-- agent-safety v1 start --> | ||
| ## GitHub Write Safety (Any Project, Every Session) | ||
|
|
||
| A `gh` / GitHub API write runs under the logged-in identity, so a mis-targeted write acts publicly as that account on someone else's repository - outward-facing and hard to reverse. These rules bound every write (a git push, an API mutation, a comment, a label, a merge) in every session on this machine, including ad-hoc work outside any project. Reads are unrestricted. A committed repo's `AGENTS.md` "Repository Boundaries and Write Safety" states the same rules for its fleet, and the two are kept in sync deliberately, because this file also covers sessions that `AGENTS.md` never reaches. The `gh-write-guard` PreToolUse hook enforces the mechanical half. | ||
|
|
||
| - **Write only to the current project's own repository.** Every state-changing call targets this checkout's `origin` and nothing else. A broad or logged-in identity is capability, not permission. Another repository needs explicit, per-session human permission for that specific repository, and a "harmless test" write is still a write. | ||
| - **Never fabricate, guess, or reuse an identifier passed to a write.** Every id a write consumes (a node id, a numeric id, a thread or comment id) is captured from a live query in the same session into a variable and passed from there. Ids resolve globally, so a wrong-but-valid id does not fail - it writes to the wrong target in another repository. If a query returns no id, stop rather than invent one. | ||
| - **A write is never a probe, and a write's output is never suppressed.** Never fire a state-changing call to see whether it works, and never append an output-discarding or force-success tail (for example `>/dev/null`, `2>/dev/null`, `&>/dev/null`, `|| true`, `|| :`, `|| echo`) to a mutation. A write that appears to fail is verified, not assumed harmless - it may have succeeded on the server. | ||
| <!-- agent-safety v1 end --> |
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.