You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@magpie.apache.org by po...@apache.org on 2026/08/27 12:17:21 UTC

(magpie) branch main updated: fix(setup): wire the agent-guard hook into settings.local.json, not settings.json (#1115)

This is an automated email from the ASF dual-hosted git repository.

potiuk pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/magpie.git


The following commit(s) were added to refs/heads/main by this push:
     new 9d5382c5 fix(setup): wire the agent-guard hook into settings.local.json, not settings.json (#1115)
9d5382c5 is described below

commit 9d5382c50fb170d6d384c774e573a691e5b28123
Author: Amir Fathi <am...@gmail.com>
AuthorDate: Thu Aug 27 06:17:16 2026 -0600

    fix(setup): wire the agent-guard hook into settings.local.json, not settings.json (#1115)
    
    * fix(setup): wire the agent-guard hook into settings.local.json, not settings.json
    
    The adopt flow committed the agent-guard PreToolUse wiring into the
    tracked .claude/settings.json while the script it invokes
    (.claude/hooks/agent-guard.py) stayed gitignored, synced in only by
    /magpie-setup. That split forced two prior workarounds for the same
    root cause: #786 guarded the command against a fresh clone that has
    the wiring but not yet the script, and #823 rewrote that guard for
    Windows, where the POSIX conditional does not run.
    
    Move the wiring itself into the gitignored, per-machine
    settings.local.json, written by Step 12 pass 1 at the same moment
    the script is deposited. Once wiring and script exist together on
    the same schedule, the fresh-clone race is gone and the command
    collapses to a plain `python3 "..."` call that runs the same on
    POSIX and Windows, so both the #786 guard and the #823 rewrite
    become unnecessary. This supersedes #823 and retro-justifies
    removing the #786 guard.
    
    Updates the merge/read/clean side for the same wiring across
    adopt.md (Step 12 pass 1, Step 2), worktree-init.md (Step 1d),
    unadopt.md, and verify.md, plus tools/agent-guard/README.md's
    documented snippet. The idempotent-merge Python (skip if already
    wired, preserve every other settings.local.json key and
    hooks.PreToolUse matcher, leave a hand-edited/corrupt file alone) is
    unchanged from the existing Step 12 pass 3 pattern for the
    sandbox-allowlist entry.
    
    Fixes #824
    
    Generated-by: Claude Code (Sonnet 5)
    
    * fix(setup): point the last worktree-init.md adopt.md reference at install.md
    
    The #913 rebase repointed two of the three adopt.md links in this file
    but missed the one inside the settings.local.json wiring bullet this PR
    adds, which is what the CI link validator (prek, validate skills and
    tool contracts) has been failing on.
---
 skills/setup/install.md       | 128 +++++++++++++++++++++++++++++-------------
 skills/setup/uninstall.md     |  23 ++++----
 skills/setup/upgrade.md       |   3 +-
 skills/setup/verify.md        |  25 +++++----
 skills/setup/worktree-init.md |  32 +++++++----
 tools/agent-guard/README.md   |  10 ++--
 6 files changed, 144 insertions(+), 77 deletions(-)

diff --git a/skills/setup/install.md b/skills/setup/install.md
index a5c29152..8a3a7504 100644
--- a/skills/setup/install.md
+++ b/skills/setup/install.md
@@ -630,13 +630,14 @@ it is framework code synced from the snapshot
 not an adopter artefact, so it is regenerated on `/magpie-setup`
 rather than committed (the same rule that keeps the snapshot and
 the framework-skill symlinks out of git — the only committed
-framework artefact is `magpie-setup`). The committed
-`.claude/settings.json` still references it; the **wiring** is
-committed, the **script** is not. Because the script is gitignored,
-**no** worktree inherits it via git — every worktree is seeded from
-the main checkout by the post-checkout hook
+framework artefact is `magpie-setup`). The `hooks.PreToolUse`
+**wiring** that references it also lives in `.claude/settings.local.json`
+(above), gitignored alongside the script itself. Neither is
+committed, so no worktree inherits either via git. Every worktree is
+seeded from the main checkout by the post-checkout hook
 ([Step 10](#step-10--worktree-aware-post-checkout-hook-fresh-only))
-or by [`worktree-init` Step 1d](worktree-init.md#step-1d--seed-the-worktrees-agent-guard-pretooluse-hook).
+or by [`worktree-init` Step 1d](worktree-init.md#step-1d--seed-the-worktrees-agent-guard-pretooluse-hook),
+which seed the wiring together with the script.
 An adopter who keeps their own non-framework guards under
 `.claude/hooks/guards.d/` and wants them tracked should commit them
 with `git add -f` (the directory is ignored by default).
@@ -681,9 +682,11 @@ framework skill.
 
 `.claude/settings.local.json` is the project-local
 per-machine settings file that
+[Step 12 pass 1](#step-12--post-install-sync--worktree-propagation--sandbox-allowlist--sanity-check)
+populates with the agent-guard `hooks.PreToolUse` wiring and that
 [Step 12 pass 3](#step-12--post-install-sync--worktree-propagation--sandbox-allowlist--sanity-check)
-populates with the project-root sandbox-allowlist entry (and
-that each worktree carries independently). Most adopters
+separately populates with the project-root sandbox-allowlist entry
+(and that each worktree carries independently). Most adopters
 already gitignore this file by Claude Code convention; the
 install flow checks for the line and adds it if missing.
 
@@ -1135,22 +1138,26 @@ guarded independently so neither can gate the git operation:
    — see
    [`setup-isolated-setup-install/SKILL.md` → Step P](../setup-isolated-setup-install/SKILL.md#step-p--project-root-coverage-in-the-sandbox-allowlists)).
 
-2. **agent-guard seeding.** The committed `.claude/settings.json`
-   wires the deterministic `PreToolUse` guard
+2. **agent-guard seeding.** The gitignored, per-machine
+   `.claude/settings.local.json` wires the deterministic
+   `PreToolUse` guard
    ([`tools/agent-guard`](../../tools/agent-guard/README.md)) at
    `$CLAUDE_PROJECT_DIR/.claude/hooks/agent-guard.py` — a
-   **per-worktree** path. The script + its `guards.d/` are
-   adopter-installed local files synced into the **main** checkout
+   **per-worktree** path. The script, its `guards.d/`, and the
+   wiring entry itself are all adopter-installed local files synced
+   into the **main** checkout
    by [Step 12 pass 1](#step-12--post-install-sync--worktree-propagation--sandbox-allowlist--sanity-check)
    and **gitignored** ([Step 7](#step-7--gitignore-entries-fresh-only)).
-   Because they are gitignored, **no** worktree inherits them via
-   `git worktree add` — every freshly-created worktree starts
-   without the script and would run with the guard **silently
-   inactive**. The hook seeds them from the main checkout's
-   already-synced copy when — and only when — this worktree has
-   none, so the guard is live in every worktree from its first
-   checkout. It never overwrites a copy the worktree already
-   carries (which may hold worktree-local guards).
+   Because they are gitignored, **no** worktree inherits any of them
+   via `git worktree add`. Every freshly-created worktree starts
+   without the script or the wiring and would run with the guard
+   **silently inactive**. The hook seeds the script from the main
+   checkout's already-synced copy only when this worktree has none
+   (never overwriting a copy the worktree already
+   carries, which may hold worktree-local guards), then wires it into
+   this worktree's own `settings.local.json` by the same idempotent
+   merge as Step 12 pass 1, so the guard is live in every worktree
+   from its first checkout.
 
 The hook is a small shell script. Surface the exact content to
 the user before writing:
@@ -1170,7 +1177,7 @@ if [ -x "$HOME/.claude/scripts/sandbox-add-project-root.sh" ]; then
   "$HOME/.claude/scripts/sandbox-add-project-root.sh" || true
 fi
 
-# (b) agent-guard PreToolUse guard: .claude/settings.json resolves it at
+# (b) agent-guard PreToolUse guard: settings.local.json resolves it at
 #     $CLAUDE_PROJECT_DIR/.claude/hooks/agent-guard.py (per-worktree). Seed
 #     this worktree from the main checkout's already-synced copy when it has
 #     none — never overwrite a copy the worktree already carries.
@@ -1185,6 +1192,42 @@ if [ -n "$wt" ] && [ "$main" != "$wt" ] \
     cp "$main/.claude/hooks/guards.d/"*.py "$wt/.claude/hooks/guards.d/" 2>/dev/null || true
 fi
 
+# (c) agent-guard PreToolUse wiring: settings.local.json is gitignored and
+#     per-worktree too, so the wiring entry is not inherited via
+#     `git worktree add` any more than the script is. Merge it into this
+#     worktree's own settings.local.json whenever the script is present here
+#     (just seeded above, or already carried) and the entry is missing.
+if [ -n "$wt" ] && [ -f "$wt/.claude/hooks/agent-guard.py" ]; then
+  python3 - "$wt/.claude/settings.local.json" <<'PYEOF' || true
+import json, sys
+
+path = sys.argv[1]
+command = 'python3 "$CLAUDE_PROJECT_DIR/.claude/hooks/agent-guard.py"'
+
+try:
+    with open(path) as f:
+        data = json.load(f)
+except FileNotFoundError:
+    data = {}
+except json.JSONDecodeError:
+    sys.exit(0)  # hand-edited or corrupt file, leave it alone
+
+pre_tool_use = data.setdefault("hooks", {}).setdefault("PreToolUse", [])
+for entry in pre_tool_use:
+    if entry.get("matcher") == "Bash" and any(
+        "agent-guard.py" in h.get("command", "") for h in entry.get("hooks", [])
+    ):
+        sys.exit(0)  # already wired
+
+pre_tool_use.append(
+    {"matcher": "Bash", "hooks": [{"type": "command", "command": command, "timeout": 30}]}
+)
+with open(path, "w") as f:
+    json.dump(data, f, indent=2)
+    f.write("\n")
+PYEOF
+fi
+
 exit 0
 ```
 
@@ -1199,7 +1242,9 @@ they later install the secure setup, no hook re-write is needed:
 the next `post-checkout` fires the helper automatically. Likewise
 (b) is a no-op when the main checkout has no agent-guard yet (the
 `-f "$main/.claude/hooks/agent-guard.py"` test fails) or when the
-worktree already carries its own copy.
+worktree already carries its own copy, and (c) is a no-op once this
+worktree's `settings.local.json` already carries the wiring (or when
+(b) found no script to wire).
 
 **Why agent-guard seeding is shell-safe but symlink
 reconciliation is not.** Seeding agent-guard is a plain
@@ -1398,30 +1443,33 @@ Four passes, in this order:
      The dispatcher auto-discovers every `*.py` in the `guards.d`
      sibling of the script — adding a skill (or a skill adding a
      guard) needs no re-wiring, only this re-sync (see the tool README).
-   - **Wire the hook once** in `.claude/settings.json` under
-     `hooks.PreToolUse` (matcher `Bash`). Because the committed
-     `.claude/settings.json` is agent-edit-denied, **surface the
-     exact snippet for the maintainer to apply** (or route it
-     through the `update-config` skill) rather than writing it:
+   - **Wire the hook once** in the **gitignored, per-machine**
+     `.claude/settings.local.json` under `hooks.PreToolUse` (matcher
+     `Bash`), at the same moment this pass deposits `agent-guard.py`.
+     Read the file if it exists (`{}` if absent), then merge in the
+     entry. **Idempotent**: preserve every other top-level key and
+     every other `hooks.PreToolUse` matcher untouched, and skip the
+     write if a `Bash` entry already invokes `agent-guard.py`:
 
      ```json
      { "matcher": "Bash", "hooks": [ { "type": "command",
-       "command": "python3 -c \"import os,sys,subprocess; p=os.path.join(os.environ.get('CLAUDE_PROJECT_DIR',''),'.claude','hooks','agent-guard.py'); sys.exit(subprocess.call([sys.executable,p]) if os.path.isfile(p) else 0)\"",
+       "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/agent-guard.py\"",
        "timeout": 30 } ] }
      ```
 
-     The command no-ops when `agent-guard.py` is absent (a **fresh
-     clone**, before `/magpie-setup` has synced the gitignored script in)
-     and runs the guard otherwise — so the committed hook never execs a
-     missing file on a Bash call (a `PreToolUse` error can block the
-     tool). It is written as an inline `python3 -c` existence check
-     rather than a shell one-liner (`[ -f … ] && … || true`) so it works
-     on **Windows** (PowerShell) too, not only POSIX shells.
-
-     Wiring happens **only once**; thereafter guards are
-     added/removed purely by syncing `guards.d` — no settings.json
-     change. If the `hooks.PreToolUse` entry is already present,
-     this pass only re-syncs the script + `guards.d`.
+     Because the wiring is now written in the same pass that deposits
+     the script (never committed ahead of it), the command needs no
+     existence guard: unlike the old committed-`settings.json` shape,
+     there is no fresh-clone state where the wiring exists but the
+     script does not. A plain `python3 "..."` call runs identically on
+     POSIX and Windows, so the inline `python3 -c` existence check and
+     the earlier POSIX-only `[ -f ... ] && ... || true` guard are both
+     unnecessary here.
+
+     Wiring happens **only once** per machine. Thereafter guards are
+     added or removed purely by syncing `guards.d`, with no
+     settings.local.json change. If the `hooks.PreToolUse` entry is
+     already present, this pass only re-syncs the script + `guards.d`.
 
    **Codex project policy is the reviewed Codex half of the runtime
    support.** Merge the snapshot's `.codex/config.toml` and
diff --git a/skills/setup/uninstall.md b/skills/setup/uninstall.md
index c54970c1..2154a766 100644
--- a/skills/setup/uninstall.md
+++ b/skills/setup/uninstall.md
@@ -10,13 +10,12 @@ symlinks **in every active target dir** ([`agents.md`](agents.md)
 — `.agents/skills/`, `.claude/skills/`, `.github/skills/`, plus
 any present holdout), the matching `.gitignore` blocks,
 post-checkout hook, the gitignored agent-guard hook
-(`.claude/hooks/agent-guard.py` + `guards.d/`), the Magpie-owned
+(`.claude/hooks/agent-guard.py` + `guards.d/` + the
+`settings.local.json` `hooks.PreToolUse` wiring), the Magpie-owned
 Codex project policy (`.codex/config.toml` values +
 `.codex/rules/magpie.rules`), the adoption
 sections in `README.md` / `AGENTS.md` / `CONTRIBUTING.md`, and the
-committed `setup` skill itself. (The committed
-`.claude/settings.json` `hooks.PreToolUse` wiring is adopter-owned
-and agent-edit-denied — surfaced for manual removal, never edited.)
+committed `setup` skill itself.
 
 > **Critical — tear down *all* target dirs.** Removing only the
 > `.claude/skills/` + `.github/skills/` pair would **orphan** the
@@ -107,7 +106,7 @@ every artefact).
 | `.gitignore` entries | `<repo-root>/.gitignore` | which of the entries from [`install.md` Step 7](install.md) are present |
 | Framework-skill symlinks | **Every active target dir** ([`agents.md`](agents.md)): the canonical `.agents/skills/` (always present), the `.claude/skills/` + `.github/skills/` relay pair, and any present holdout (`.windsurf/skills/`, `.goose/skills/`) | each `magpie-*` symlink — canonical entries resolving into `<snapshot-dir>/skills/`, relays resolving into `.agents/skills/magpie-*` — in **each** target dir |
 | Post-checkout hook | `<repo-root>/.git/hooks/post-checkout` | exists + invokes `~/.claude/scripts/sandbox-add-project-root.sh` and/or seeds `.claude/hooks/agent-guard.py` |
-| agent-guard hook | `<repo-root>/.claude/hooks/agent-guard.py` + `<repo-root>/.claude/hooks/guards.d/` | exist (gitignored framework code). The committed `.claude/settings.json` `hooks.PreToolUse` wiring is **adopter-owned** — surface it for the user to remove by hand (settings.json is agent-edit-denied); do not edit it. |
+| agent-guard hook | `<repo-root>/.claude/hooks/agent-guard.py` + `<repo-root>/.claude/hooks/guards.d/` + the `hooks.PreToolUse` entry in `<repo-root>/.claude/settings.local.json` | exist (all gitignored, per-machine). Unlike the committed `settings.json`, `settings.local.json` is agent-writable, so remove the entry directly rather than surfacing it for manual removal. |
 | Codex policy | `.codex/config.toml`, `.codex/rules/magpie.rules` | identify Magpie-owned values separately from unrelated adopter Codex configuration |
 | Doc section: `README.md` | `<repo-root>/README.md` | contains the `## Agent-assisted contribution (apache-magpie)` heading |
 | Doc section: `AGENTS.md` | `<repo-root>/AGENTS.md` | contains the `## apache-magpie framework` heading |
@@ -142,6 +141,7 @@ The following will be REMOVED:
     .git/hooks/post-checkout              (if it contains the magpie recipe)
     .claude/hooks/agent-guard.py          (gitignored framework code)
     .claude/hooks/guards.d/               (gitignored; bundled + skill-owned guards)
+    .claude/settings.local.json           (hooks.PreToolUse entry removed; other keys kept)
     # Target dirs (per agents.md): canonical .agents/skills/, the
     #   .claude/skills/ + .github/skills/ relay pair, plus any present
     #   holdout — each carries one magpie-<n> entry per linked skill.
@@ -262,10 +262,11 @@ pointing at a deleted snapshot.
    them and `git rm` only those by name; do not delete an
    adopter-authored tracked guard silently. Leave the
    `.claude/hooks/` directory itself if it holds non-framework
-   hooks. The committed `.claude/settings.json` `hooks.PreToolUse`
-   wiring is **adopter-owned and agent-edit-denied** — surface the
-   exact entry for the user to delete by hand; do not edit
-   `settings.json`.
+   hooks. Remove the matching `hooks.PreToolUse` entry from
+   `<repo-root>/.claude/settings.local.json` directly (idempotent
+   merge, same file the adopt flow wrote it into). Unlike the
+   committed `settings.json`, `settings.local.json` is gitignored and
+   agent-writable, so no manual step is needed here.
    For the committed Codex policy: remove `magpie.rules` when it is
    stock, and remove `config.toml` only when it contains no unrelated
    keys. Otherwise surface a minimal patch that removes only the
@@ -324,8 +325,8 @@ After the deletions, verify the post-state:
 - `.gitignore` no longer contains the magpie entries.
 - `.claude/hooks/agent-guard.py` and `.claude/hooks/guards.d/`
   do not exist (save any adopter-authored guards the user chose
-  to keep); the `.claude/settings.json` `hooks.PreToolUse` entry
-  was surfaced for manual removal.
+  to keep); the matching `hooks.PreToolUse` entry in
+  `.claude/settings.local.json` is gone too.
 - Magpie-owned Codex policy is gone while unrelated `.codex`
   configuration remains.
 - The doc sections are gone from the affected files.
diff --git a/skills/setup/upgrade.md b/skills/setup/upgrade.md
index cef3ec93..c49a4ff9 100644
--- a/skills/setup/upgrade.md
+++ b/skills/setup/upgrade.md
@@ -449,7 +449,8 @@ rather than pulls in via symlink. Examples:
   `guards.d/*.py` **and** every skill-owned `skills/*/guards/*.py`
   in the snapshot. Re-syncing it is how a new skill — or a skill
   that newly adds a guard — reaches an already-adopted repo; the
-  `settings.json` `hooks.PreToolUse` wiring is unchanged.
+  `settings.local.json` `hooks.PreToolUse` wiring is unchanged (already
+  wired, or re-added via the same idempotent merge if missing).
 - The committed `.codex/config.toml` and `.codex/rules/magpie.rules`:
   compare them with the current snapshot policy. Preserve unrelated
   Codex settings; surface conflicts and hand edits rather than
diff --git a/skills/setup/verify.md b/skills/setup/verify.md
index b67d2759..e9490a94 100644
--- a/skills/setup/verify.md
+++ b/skills/setup/verify.md
@@ -339,27 +339,30 @@ Three sub-checks for the deterministic guard
    copy; extra locally-added `*.py` are fine. A missing skill guard
    means that skill's deterministic protection is silently inactive
    — remediation is `/magpie-setup` (adopt/upgrade), which re-collects.
-3. **Hook wired in settings.json.** `<repo-root>/.claude/settings.json`
+3. **Hook wired in settings.local.json.** `<repo-root>/.claude/settings.local.json`
    has a `hooks.PreToolUse` entry (matcher `Bash`) whose command
    runs `agent-guard.py`.
-   - ⚠ if missing — the script is present but not active; print
-     the one-time wiring snippet (see
-     [`install.md` Step 12](install.md#step-12--post-install-sync--worktree-propagation--sandbox-allowlist--sanity-check))
-     for the maintainer to apply (settings.json is agent-edit-denied).
+   - If missing, the script is present but not active. Write the
+     entry directly (idempotent merge, per
+     [`install.md` Step 12](install.md#step-12--post-install-sync--worktree-propagation--sandbox-allowlist--sanity-check)),
+     no operator prompt needed since `settings.local.json` is
+     gitignored and agent-writable, unlike the committed `settings.json`.
 
 The script + `guards.d` are **gitignored** framework code
 ([`install.md` Step 7](install.md#step-7--gitignore-entries-fresh-only)),
 synced from the snapshot rather than committed — so a *missing*
 script is the expected state of a fresh checkout, not a defect, and
 the fix is always a re-sync (never `git add`). When this check runs
-**inside a worktree**, the script + `guards.d` are per-worktree
-files (the `settings.json` wiring resolves
-`$CLAUDE_PROJECT_DIR/.claude/hooks/agent-guard.py` against the
-worktree root). The remediation for a *missing* script in a worktree
-is not the main-checkout sync but
+**inside a worktree**, the script, `guards.d`, **and** the
+`settings.local.json` wiring are all per-worktree (each worktree's own
+`settings.local.json` resolves
+`$CLAUDE_PROJECT_DIR/.claude/hooks/agent-guard.py` against that
+worktree's own root, and is not inherited via git). The remediation
+for a *missing* script or wiring entry in a worktree is not the
+main-checkout sync but
 [`worktree-init.md` Step 1d](worktree-init.md#step-1d--seed-the-worktrees-agent-guard-pretooluse-hook)
 (or the post-checkout hook on the next `git worktree add`), which
-seeds it from the main checkout's already-synced copy.
+seeds both from the main checkout's already-synced copy.
 
 ### 8b. Sandbox-allowlist coverage of the current worktree
 
diff --git a/skills/setup/worktree-init.md b/skills/setup/worktree-init.md
index 90428601..bb47c52a 100644
--- a/skills/setup/worktree-init.md
+++ b/skills/setup/worktree-init.md
@@ -189,25 +189,26 @@ secure-agent isolation is independent of framework adoption.
 
 ## Step 1d — Seed the worktree's agent-guard PreToolUse hook
 
-The committed `.claude/settings.json` wires the deterministic
-guard ([`tools/agent-guard`](../../tools/agent-guard/README.md))
-at `$CLAUDE_PROJECT_DIR/.claude/hooks/agent-guard.py` — a
-**per-worktree** path. The script + its `guards.d/` are
-adopter-installed local files synced into the **main** checkout by
+The gitignored, per-machine `.claude/settings.local.json` wires the
+deterministic guard ([`tools/agent-guard`](../../tools/agent-guard/README.md))
+at `$CLAUDE_PROJECT_DIR/.claude/hooks/agent-guard.py`, a
+**per-worktree** path. The script, its `guards.d/`, and the
+`hooks.PreToolUse` wiring itself are all adopter-installed local
+files synced into the **main** checkout by
 [`install.md` Step 12 pass 1](install.md#step-12--post-install-sync--worktree-propagation--sandbox-allowlist--sanity-check)
 / [`upgrade.md` Step 6b](upgrade.md#step-6b--sync-locally-installed-hooks-and-configuration)
 and **gitignored** ([`install.md` Step 7](install.md#step-7--gitignore-entries-fresh-only)).
-Because they are gitignored, no worktree inherits them via git —
-every worktree starts without the script and would run with the
-guard **silently inactive** until seeded.
+Because they are gitignored, no worktree inherits any of them via
+git. Every worktree starts without the script or the wiring and
+would run with the guard **silently inactive** until seeded.
 
 This is the agent-driven counterpart of the
 [post-checkout hook's agent-guard seeding](install.md#step-10--worktree-aware-post-checkout-hook-fresh-only):
 the git hook covers `git worktree add`, this step covers worktrees
 that pre-date the hook or where its best-effort copy did not run.
 
-Seed from the main checkout's already-synced copy — a plain file
-copy, the same `<main>` resolved in Step 0:
+Seed the script from the main checkout's already-synced copy, a
+plain file copy of the same `<main>` resolved in Step 0:
 
 ```bash
 # Only when the main has a guard and this worktree has none — never
@@ -230,6 +231,17 @@ for Step 2:
 - ⚠ main has no agent-guard yet — run `/magpie-setup` (or
   `/magpie-setup upgrade`) from the main checkout to sync it.
 
+Then seed the wiring into this worktree's own `settings.local.json`
+the same way [`install.md` Step 12 pass 1](install.md#step-12--post-install-sync--worktree-propagation--sandbox-allowlist--sanity-check)
+does for the main checkout: an idempotent merge that preserves every
+other key and is skipped once a `Bash` entry already invokes
+`agent-guard.py`. Do this whenever the script was just copied above,
+and also when the worktree already has the script but is missing
+the wiring (an older worktree created before this step covered
+wiring, or one where a prior pass copied the script but not the
+entry). Add a `+ wired` row alongside the recap above when this
+ran and the script was already present.
+
 `worktree-init` does **not** fail when the main carries no
 agent-guard; the guard is an opt-in adopter-side file, and the
 worktree's framework-skill symlinks are usable without it.
diff --git a/tools/agent-guard/README.md b/tools/agent-guard/README.md
index 686ce158..a1f91a99 100644
--- a/tools/agent-guard/README.md
+++ b/tools/agent-guard/README.md
@@ -101,8 +101,10 @@ for (default `ready for maintainer review`).
 
 ## Wiring
 
-The guard is registered as a `PreToolUse` hook on the `Bash` matcher in
-`.claude/settings.json`:
+The guard is registered as a `PreToolUse` hook on the `Bash` matcher in the
+gitignored, per-machine `.claude/settings.local.json` (not the committed
+`.claude/settings.json`, so no worktree inherits the wiring via git: each
+worktree is seeded independently, same as the script itself):
 
 ```json
 {
@@ -111,7 +113,7 @@ The guard is registered as a `PreToolUse` hook on the `Bash` matcher in
       {
         "matcher": "Bash",
         "hooks": [
-          { "type": "command", "command": "python3 -c \"import os,sys,subprocess; p=os.path.join(os.environ.get('CLAUDE_PROJECT_DIR',''),'.claude','hooks','agent-guard.py'); sys.exit(subprocess.call([sys.executable,p]) if os.path.isfile(p) else 0)\"", "timeout": 30 }
+          { "type": "command", "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/agent-guard.py\"", "timeout": 30 }
         ]
       }
     ]
@@ -123,7 +125,7 @@ The guard is registered as a `PreToolUse` hook on the `Bash` matcher in
 into the adopter tree (`.claude/hooks/agent-guard.py`) and into the user-scope
 secure setup (`~/.claude/scripts/agent-guard.py`); `/magpie-setup upgrade`,
 `verify`, and the `setup-isolated-setup-install` / `…-update` skills keep it and
-the settings.json entry in sync. See those skills for the exact steps.
+the settings.local.json entry in sync. See those skills for the exact steps.
 
 ### OpenCode