From 69b2f262dfd8eeaa95105a5cebf35c2a0b654fa4 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Fri, 31 Jul 2026 15:30:11 -0700 Subject: [PATCH 1/2] Prefix the PowerShell invocation, and name the interpreter per platform Two corrections from the #483 Windows and WSL2 testing, both of which disproved something the merged contract asserted. The PowerShell installer invocation needs the .\ prefix. PowerShell does not load a command from a relative path without it, and warns that it does not assume the current location. I declined this during the #482 review, arguing that a multi-segment path resolves where a bare filename does not, and flagged that I could not run PowerShell to check. The maintainer's direct experience is that both the current-directory and the subdirectory form need either .\ or an absolute path. Both files that carry the line are corrected, since docs/host-setup.md and the kit README shipped the same unprefixed form. The interpreter is not called python3 everywhere, so the contract's presence check was wrong on the platform it most needed to be right about. On native Windows the installer registers python, py and python3.13 but not python3, where that name resolves to a Microsoft Store alias stub that reports the interpreter as missing. A correctly set-up Windows host therefore failed the check and read as broken. Stock Debian is the mirror image, carrying python3 and no bare python. The row now names py -3 for native Windows, and the prose states the split and why a cross-platform script should still prefer python3. Three other findings from that testing are deliberately not carried here. Docker Desktop performs the WSL2 enabling itself and exposes the per-distro integration toggle the matrix already documents. The WSL interop credential helper failure was an upstream bug since fixed, so the durable part is only that Docker Desktop must be quit rather than paused before wsl --update. And the /mnt/c permission behavior does not apply to a workflow that treats WSL2 as an ordinary Debian host and never cross-shares files. The kit README's dash backlog is cleared, per the corrected-as-next-edited rule. It is hub-only, so no re-vendor debt. Co-Authored-By: Claude Opus 5 (1M context) --- docs/host-setup.md | 10 ++++++---- host-setup/agent-safety/README.md | 16 ++++++++-------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/docs/host-setup.md b/docs/host-setup.md index 561f9de8..db22432b 100644 --- a/docs/host-setup.md +++ b/docs/host-setup.md @@ -18,11 +18,13 @@ This section is the **contract**: which tools a host needs and which repo proced | --- | --- | --- | | `git` | everything, and the identity and signing contract in [`STANDUP.md`][standup] step 0 | `git --version` | | `gh` | the PR and review loop, `gh api` queries, `repo-config/configure.sh` | `gh --version` | -| `python3` | `scripts/` and `spec/` (standard library only, no packages to install) | `python3 --version` | +| Python 3 | `scripts/` and `spec/` (standard library only, no packages to install) | `python3 --version`, or `py -3 --version` on native Windows | | `docker` | the four linters, which run as pinned images rather than local installs | `docker --version` | | `uv` / `uvx` | coverage runs, and the Python toolchain (`ruff`, `pyright` or `mypy`) in a Python repo | `uv --version` | -Two consequences worth reading off the table rather than discovering later. **`python3` needs no packages**, because every script here is standard library only, so a bare interpreter is enough. And **the linters need only `docker`**, not `node`, `dotnet` or a local `markdownlint`, since each runs as a pinned image, which is what keeps a local run and CI the same check. +Two consequences worth reading off the table rather than discovering later. **Python 3 needs no packages**, because every script here is standard library only, so a bare interpreter is enough. And **the linters need only `docker`**, not `node`, `dotnet` or a local `markdownlint`, since each runs as a pinned image, which is what keeps a local run and CI the same check. + +**The interpreter is not called `python3` everywhere.** On native Windows the installer registers `python`, `py` and `python3.13` but **not** `python3`, where that name instead resolves to a Microsoft Store alias stub that reports the interpreter as missing, so a correctly set-up host fails a `python3` check. Stock Debian is the mirror image, carrying `python3` and no bare `python`. Use `py -3` on native Windows and `python3` elsewhere, and prefer `python3` in any script that must run on both, since WSL2 shadows the Windows stub. A missing tool is a host gap, not a repo problem. Install it and re-run, rather than working around it in a repo. @@ -147,7 +149,7 @@ host-setup/agent-safety/install.sh # Linux, WSL, macOS, Proxmox ``` ```powershell -host-setup\agent-safety\install.ps1 # Windows +.\host-setup\agent-safety\install.ps1 # Windows, and the .\ prefix is required ``` Both wrap one `install.py`, so every platform runs the same tested path. Restart Claude Code sessions on the machine afterward so the hook and the `CLAUDE.md` block load. Details, verification, and scope limits are in [`host-setup/agent-safety/README.md`][agent-safety]. @@ -173,7 +175,7 @@ If signing fails locally, the devcontainer will fail too, so fix here first. | --- | --- | | Stand up a new repo through [`STANDUP.md`][standup] | step 0 verifies identity and signing, and its window closes at the first commit | | Run the four linters locally, matching CI | `docker` runs each as the same pinned image CI uses | -| Run the repo's own gates and tests | `python3` covers `scripts/` and `spec/` with no packages to install | +| Run the repo's own gates and tests | Python 3 covers `scripts/` and `spec/` with no packages to install | | Drive the PR and Copilot review loop | `gh` and an authenticated session | | Let an agent work with the `gh` credentials live | the write-safety kit is installed | diff --git a/host-setup/agent-safety/README.md b/host-setup/agent-safety/README.md index 630610f8..b9c25318 100644 --- a/host-setup/agent-safety/README.md +++ b/host-setup/agent-safety/README.md @@ -1,17 +1,17 @@ # 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 branch rule or overrides a required check** - a push, force-push, or delete that an active branch rule forbids, or an override flag (`--admin` past the server-side merge gate, `--no-verify` past the local git hooks). 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 branch rule or overrides a required check**, meaning a push, force-push, or delete that an active branch rule forbids, or an override flag (`--admin` past the server-side merge gate, `--no-verify` past the local git hooks). 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 per-repo configuration. A push to a protected-default branch fails closed when its rules cannot be determined - the API is unreachable, or the checkout's origin cannot be resolved to query them. The explicit-bypass flags are denied **unconditionally**, since the flag is itself the bypass and needs no branch query: `gh pr merge --admin` overrides the server-side merge gate, and `git commit`/`git push --no-verify` skips the local git hooks. 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 `GOVERNANCE.md` "Repository Boundaries and Write Safety" rules, which only reach fleet repos. +- **`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. A push to a protected-default branch fails closed when its rules cannot be determined, either because the API is unreachable or because the checkout's origin cannot be resolved to query them. The explicit-bypass flags are denied **unconditionally**, since the flag is itself the bypass and needs no branch query: `gh pr merge --admin` overrides the server-side merge gate, and `git commit`/`git push --no-verify` skips the local git hooks. 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 `GOVERNANCE.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 GOVERNANCE.md rules are the behavioral layer. Prose alone is not enough - the incident happened under prose rules - so both ship. +The hook is the mechanical backstop. The CLAUDE.md rules and the carried GOVERNANCE.md rules are the behavioral layer. Prose alone is not enough, since the incident happened under prose rules, so both ship. -## Install (Idempotent - Safe to Re-Run to Update) +## Install (Idempotent, Safe to Re-Run to Update) ```sh # Linux / WSL / macOS / Proxmox @@ -19,8 +19,8 @@ host-setup/agent-safety/install.sh ``` ```powershell -# Windows -host-setup\agent-safety\install.ps1 +# Windows - the .\ prefix is required, PowerShell does not run a script from a relative path without it +.\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. @@ -70,7 +70,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 its rules cannot be determined (the API is unreachable, or the checkout's origin cannot be resolved to query them), 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. +- **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, since 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 2c32090d482b1166957f60c49340e5ae4ca5b7d2 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Fri, 31 Jul 2026 15:35:31 -0700 Subject: [PATCH 2/2] Say how the verify block translates on native Windows The consolidated verify block still ran python3 --version, the one name this same PR establishes a correctly set-up Windows host does not have, so the block contradicted the contract table a few sections above it and would report a healthy machine as broken. The caveat now sits under the block: read the interpreter line as py -3 on native Windows, and note that Git Bash inherits the Windows PATH, so python3 reaches the same Store alias stub there as in PowerShell. That case is not obvious, since the shell note already sends Windows readers to WSL2 or Git Bash and only WSL2 shadows the stub. A PowerShell equivalent of the block is deliberately not written. It has not been run on a Windows host, and this PR exists because an unverified PowerShell invocation shipped and was wrong. #483 is where a verified one belongs, under its rule that a cell is filled only by someone who has run it. Co-Authored-By: Claude Opus 5 (1M context) --- docs/host-setup.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/host-setup.md b/docs/host-setup.md index db22432b..ae4cb16e 100644 --- a/docs/host-setup.md +++ b/docs/host-setup.md @@ -169,6 +169,8 @@ gh auth status If signing fails locally, the devcontainer will fail too, so fix here first. +**This block is POSIX, and on native Windows the interpreter line needs translating**, since `python3` is the one name a correctly set-up Windows host does not have. Read it as `py -3 --version` there, matching the contract table above, and run the rest from WSL2 or Git Bash per the shell note. Git Bash inherits the Windows `PATH`, so `python3` reaches the same Store alias stub it does in PowerShell and reports a working interpreter as missing. A PowerShell equivalent of this block is deliberately **not** given here, because it has not been run on a Windows host, and an unverified verification command is worse than none. [#483][issue-483] is where one belongs once someone has executed it. + **What the host can do once this passes**, which is the point of the contract above: | Now possible | Because | @@ -191,6 +193,7 @@ A host that fails any row is not ready for the procedure that row names, and the [agent-safety]: ../host-setup/agent-safety/README.md [devcontainer]: ./devcontainer.md [governance-git-and-commit-rules]: ../GOVERNANCE.md#git-and-commit-rules +[issue-483]: https://github.com/ptr727/ProjectTemplate/issues/483 [ssh-signing]: ./ssh-signing.md [standup]: ../STANDUP.md