From 1465e5d1457ce403f0b25f20c299d810b0060514 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 25 Jul 2026 23:21:05 -0700 Subject: [PATCH 1/4] Document Rule 4 and the refresh procedure in the safety-kit README The guard now also denies a git operation that would only land by bypassing an active branch rule (Rule 4), not just the three write footguns. Update What It Installs to describe it, add a Scope note that the branch-bypass rule fails closed on the protected-default branches (unlike the precision-over- recall write rules), and add a Refreshing section that makes the re-run-the-installer update path explicit - the mechanism #365 tracks. Co-Authored-By: Claude Opus 4.8 --- host-setup/agent-safety/README.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/host-setup/agent-safety/README.md b/host-setup/agent-safety/README.md index ee1062b4..8f0738dd 100644 --- a/host-setup/agent-safety/README.md +++ b/host-setup/agent-safety/README.md @@ -1,12 +1,12 @@ # 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). +Per-machine, user-account-scoped guards against an agent making a mis-targeted GitHub **write** under the maintainer's identity, or a **git operation that would only succeed by bypassing an active branch rule**. 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. +- **`hooks/gh-write-guard.py`** - a PreToolUse hook that denies two classes of agent footgun. First, the GitHub **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`. Second, a **git operation that would only land by bypassing an active branch rule**: a direct push to a branch that requires a pull request, a force-push where history is protected, a branch delete where deletion is blocked, and the explicit-bypass flags (`gh pr merge --admin`, `git commit`/`git push --no-verify`) - judged against the branch's **live** rules, so a code-style `develop` is denied while a config-style `develop` is allowed with no hardcoded list, and the protected-default branches fail closed when the rules cannot be read. Reads and everything else pass through. It fires even in autonomous / bypass-permissions sessions, which is how the incident happened. - **A `## GitHub Write Safety (Any Project, Every Session)` 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. @@ -27,6 +27,10 @@ Both are thin wrappers around `install.py`, so every OS runs one tested code pat **Restart Claude Code sessions on the machine afterward** so the new hook and CLAUDE.md load. +## Refreshing After an Upstream Change + +The deployed copy on each machine is a snapshot, so when the guard changes upstream (a new rule or a fix) every machine keeps running the old hook until it is refreshed. The installer **is** the refresh: pull the latest template and re-run `install.sh` (or `install.ps1`) on each machine. It re-copies the hook, re-runs the self-test, and re-registers in place, so a re-run is safe and updates the deployed copy. [#365][issue-365] tracks the per-machine rollout and its re-runs. + ## Verify (POSIX Shell) ```sh @@ -64,7 +68,8 @@ The installer writes this. It is here so you can inspect or hand-place it: ## 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. +- **Precision over recall for the write footguns.** The hook denies the specific dangerous write 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. +- **The branch-bypass rule fails closed.** Unlike the write-footgun rules, a push to `main`/`master`/`develop` is denied even when the branch rules cannot be read, because the harm there is a silent success under the maintainer's admin bypass. The rule reads each branch's live rules, so it adapts to every repo (a code-style `develop` denies, a config-style `develop` allows) with no hardcoded list, and hands the exact command to the maintainer to run when a bypass is genuinely intended. - **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//` 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. From 33f28258ace776c694414267102081fb60d09c4e Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 25 Jul 2026 23:25:48 -0700 Subject: [PATCH 2/4] Separate the unconditional bypass flags from the branch-rule cases Copilot review of #452: the wording implied the admin-merge and verification-skipping flags are judged against live branch rules, but they are denied unconditionally (the flag is the bypass, no query). Split the bullet so the branch-rule cases keep the live-rules/fail-closed description and the explicit-bypass flags are described as unconditional. Widen the intro so it covers the verification-skipping flags too, not only branch-rule bypass. Co-Authored-By: Claude Opus 4.8 --- host-setup/agent-safety/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/host-setup/agent-safety/README.md b/host-setup/agent-safety/README.md index 8f0738dd..e4df8ee6 100644 --- a/host-setup/agent-safety/README.md +++ b/host-setup/agent-safety/README.md @@ -1,12 +1,12 @@ # Agent Write-Safety Kit -Per-machine, user-account-scoped guards against an agent making a mis-targeted GitHub **write** under the maintainer's identity, or a **git operation that would only succeed by bypassing an active branch rule**. 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). +Per-machine, user-account-scoped guards against an agent making a mis-targeted GitHub **write** under the maintainer's identity, or a **git operation that bypasses a repository protection** - a push, force, or delete an active branch rule forbids, or a verification-skipping flag like `--admin`/`--no-verify`. 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 two classes of agent footgun. First, the GitHub **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`. Second, a **git operation that would only land by bypassing an active branch rule**: a direct push to a branch that requires a pull request, a force-push where history is protected, a branch delete where deletion is blocked, and the explicit-bypass flags (`gh pr merge --admin`, `git commit`/`git push --no-verify`) - judged against the branch's **live** rules, so a code-style `develop` is denied while a config-style `develop` is allowed with no hardcoded list, and the protected-default branches fail closed when the rules cannot be read. Reads and everything else pass through. It fires even in autonomous / bypass-permissions sessions, which is how the incident happened. +- **`hooks/gh-write-guard.py`** - a PreToolUse hook that denies two classes of dangerous action. First, the GitHub **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`. Second, a **git operation that bypasses a repository protection**. The branch-rule cases - a direct push to a branch that requires a pull request, a force-push where history is protected, a branch delete where deletion is blocked - are judged against the branch's **live** rules, so a code-style `develop` is denied while a config-style `develop` is allowed with no hardcoded list, and the protected-default branches fail closed when the rules cannot be read. The explicit-bypass flags (`gh pr merge --admin`, `git commit`/`git push --no-verify`) are denied **unconditionally**: the flag is the bypass, so no branch query is needed. Reads and everything else pass through. It fires even in autonomous / bypass-permissions sessions, which is how the incident happened. - **A `## GitHub Write Safety (Any Project, Every Session)` 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. From 0bc5027d0064253a7bbfcf0665ac23c648f12c94 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 25 Jul 2026 23:29:44 -0700 Subject: [PATCH 3/4] Fix grammar and clarify live-rules vs fail-closed wording Copilot review of #452: add a missing "that" in the intro sentence, and replace "no hardcoded list" with "no per-repo configuration" (two spots) so it no longer reads as contradicting the fixed default-branch fail-closed fallback described alongside it. Co-Authored-By: Claude Opus 4.8 --- host-setup/agent-safety/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/host-setup/agent-safety/README.md b/host-setup/agent-safety/README.md index e4df8ee6..18f1b955 100644 --- a/host-setup/agent-safety/README.md +++ b/host-setup/agent-safety/README.md @@ -1,12 +1,12 @@ # Agent Write-Safety Kit -Per-machine, user-account-scoped guards against an agent making a mis-targeted GitHub **write** under the maintainer's identity, or a **git operation that bypasses a repository protection** - a push, force, or delete an active branch rule forbids, or a verification-skipping flag like `--admin`/`--no-verify`. 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). +Per-machine, user-account-scoped guards against an agent making a mis-targeted GitHub **write** under the maintainer's identity, or a **git operation that bypasses a repository protection** - a push, force, or delete that an active branch rule forbids, or a verification-skipping flag like `--admin`/`--no-verify`. 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 two classes of dangerous action. First, the GitHub **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`. Second, a **git operation that bypasses a repository protection**. The branch-rule cases - a direct push to a branch that requires a pull request, a force-push where history is protected, a branch delete where deletion is blocked - are judged against the branch's **live** rules, so a code-style `develop` is denied while a config-style `develop` is allowed with no hardcoded list, and the protected-default branches fail closed when the rules cannot be read. The explicit-bypass flags (`gh pr merge --admin`, `git commit`/`git push --no-verify`) are denied **unconditionally**: the flag is the bypass, so no branch query is needed. Reads and everything else pass through. It fires even in autonomous / bypass-permissions sessions, which is how the incident happened. +- **`hooks/gh-write-guard.py`** - a PreToolUse hook that denies two classes of dangerous action. First, the GitHub **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`. Second, a **git operation that bypasses a repository protection**. The branch-rule cases - a direct push to a branch that requires a pull request, a force-push where history is protected, a branch delete where deletion is blocked - are judged against the branch's **live** rules, so a code-style `develop` is denied while a config-style `develop` is allowed with no per-repo configuration, and the protected-default branches fail closed when the rules cannot be read. The explicit-bypass flags (`gh pr merge --admin`, `git commit`/`git push --no-verify`) are denied **unconditionally**: the flag is the bypass, so no branch query is needed. Reads and everything else pass through. It fires even in autonomous / bypass-permissions sessions, which is how the incident happened. - **A `## GitHub Write Safety (Any Project, Every Session)` 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. @@ -69,7 +69,7 @@ The installer writes this. It is here so you can inspect or hand-place it: - **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 for the write footguns.** The hook denies the specific dangerous write 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. -- **The branch-bypass rule fails closed.** Unlike the write-footgun rules, a push to `main`/`master`/`develop` is denied even when the branch rules cannot be read, because the harm there is a silent success under the maintainer's admin bypass. The rule reads each branch's live rules, so it adapts to every repo (a code-style `develop` denies, a config-style `develop` allows) with no hardcoded list, and hands the exact command to the maintainer to run when a bypass is genuinely intended. +- **The branch-bypass rule fails closed.** Unlike the write-footgun rules, a push to `main`/`master`/`develop` is denied even when the branch rules cannot be read, because the harm there is a silent success under the maintainer's admin bypass. The rule reads each branch's live rules, so it adapts to every repo (a code-style `develop` denies, a config-style `develop` allows) with no per-repo configuration, and hands the exact command to the maintainer to run when a bypass is genuinely intended. - **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//` 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. From c170987b2af75c1e71c4795127e7a55e3bd5ebf3 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Sat, 25 Jul 2026 23:32:13 -0700 Subject: [PATCH 4/4] Use "force-push" in the intro list for consistency Copilot review of #452: "force" as a bare noun read ambiguously and was inconsistent with the "force-push" wording used later; make the intro list say "force-push". Co-Authored-By: Claude Opus 4.8 --- host-setup/agent-safety/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/host-setup/agent-safety/README.md b/host-setup/agent-safety/README.md index 18f1b955..18767384 100644 --- a/host-setup/agent-safety/README.md +++ b/host-setup/agent-safety/README.md @@ -1,6 +1,6 @@ # Agent Write-Safety Kit -Per-machine, user-account-scoped guards against an agent making a mis-targeted GitHub **write** under the maintainer's identity, or a **git operation that bypasses a repository protection** - a push, force, or delete that an active branch rule forbids, or a verification-skipping flag like `--admin`/`--no-verify`. 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). +Per-machine, user-account-scoped guards against an agent making a mis-targeted GitHub **write** under the maintainer's identity, or a **git operation that bypasses a repository protection** - a push, force-push, or delete that an active branch rule forbids, or a verification-skipping flag like `--admin`/`--no-verify`. 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