From 152c8fb832ea6a886a1e0e8bd9f394142c4f4932 Mon Sep 17 00:00:00 2001 From: Claude Code Bot Date: Tue, 27 Oct 2026 17:10:13 -0700 Subject: [PATCH 1/5] docs(plans): propose one permission policy across all block layers An inventory of all 413 session transcripts found 633 blocks since 2026-10-15: hooks 220 (every affected session), sandbox network 307 rows, harness guards 42, sandbox filesystem 37, classifier 15, permission rules 0. The plan gives every blocked pattern one answer: A (make it work) or B (change the procedure so it never happens). "Ask Andrew to run it" is not an answer. It also records the worktree procedure tested on 2026-10-27 (EnterWorktree, switch -c, ExitWorktree remove), general Edit and autoMode rules, and the hook changes, which are the largest lever. Decisions are marked for Andrew. STATUS.md gets a dated entry. --- docs/STATUS.md | 13 +- docs/plans/2026-10-27-permission-policy.md | 256 +++++++++++++++++++++ 2 files changed, 267 insertions(+), 2 deletions(-) create mode 100644 docs/plans/2026-10-27-permission-policy.md diff --git a/docs/STATUS.md b/docs/STATUS.md index 9acb33b..e813181 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -2,8 +2,8 @@ **As of 2026-09-16, with 2026-10-01, 2026-10-02 (three), 2026-10-05 (three), 2026-10-06, 2026-10-06/07, 2026-10-07 evening, 2026-10-08, -2026-10-12, 2026-10-13 to 10-16, 2026-10-19, 2026-10-19 to 10-21 and -2026-10-22 updates below.** +2026-10-12, 2026-10-13 to 10-16, 2026-10-19, 2026-10-19 to 10-21, +2026-10-22 and 2026-10-27 updates below.** Point-in-time snapshot of the infrastructure backlog (`docs/superpowers/specs/2026-09-01-infrastructure-backlog-design.md`). The design doc is authoritative on *what* each item is and why; this file records @@ -680,6 +680,15 @@ or be explicitly parked with a resume note, by then — see the tracker. `dev-env#224` reverted the dev-env part. Design note: `docs/superpowers/specs/2026-10-22-text-gate-v3.md`. +**2026-10-27 update (Pacific): permission policy:** + +- **Proposal, waiting on decisions:** + `docs/plans/2026-10-27-permission-policy.md`. An inventory of every block + since 2026-10-15 found 633: hooks 220 (every affected session), sandbox + network 307 rows, classifier 15, permission rules 0. Every blocked action + gets A (make it work) or B (change the procedure), never "ask Andrew". + Worktrees move to `EnterWorktree`/`ExitWorktree`, tested end to end. + **Kanban tracker: GitHub Issues + a Project board in `smartwatermelon/dev-env`**, using the org's `Priority`/`Target date`/`Effort` issue fields. Board columns still need one-time setup (see dev-env issue tracker); this file stays the diff --git a/docs/plans/2026-10-27-permission-policy.md b/docs/plans/2026-10-27-permission-policy.md new file mode 100644 index 0000000..e3f5f03 --- /dev/null +++ b/docs/plans/2026-10-27-permission-policy.md @@ -0,0 +1,256 @@ +# Plan: one permission policy for sandbox, rules, classifier and hooks + +**Status:** proposal, waiting for Andrew's decisions (marked **Decide**) +**Date:** 2026-10-27 +**Author:** arich (drafted by Claude Code) + +## Problem + +Since the Bash sandbox went live (2026-10-22 20:49 PDT) and auto mode before +it (2026-10-15), agents keep hitting blocks, and the usual fallback is "ask +Andrew to run it." That fallback does not scale. On 2026-10-27 it also caused +harm: `git worktree add` failed on a sandbox write, the agent kept going, and a +`git -C reset --hard HEAD` ran in the shared primary checkout +of `scripts`. + +## The rule this policy adds + +Every blocked action gets exactly one of two answers: + +- **A. Agents should be able to do it.** Change the configuration, hook or + tool so that it works. +- **B. Agents should not do it.** Change the procedure so that it never needs + to happen, and make the block say what to do instead. + +"The agent fails and asks the operator to do it" is not an allowed answer. +An agent that still hits a block stops, and the block is treated as a bug in +this policy: policy (A or B not yet decided) or misconfiguration. + +A related rule: **no error output that cannot be acted on.** Either fix the +cause, or stop the condition that prints it. Noise trains agents to skip +error lines, and that is how the 2026-10-27 `worktree add` failure was missed. + +## Evidence + +A script read all 413 transcripts under `~/.claude/projects` (subagents +included) and paired every tool call with its result. Output, kept outside the +repo: `scratchpad/denials-v2/` (`denials.jsonl`, `patterns.csv`, +`report.md`) in session `cbe34707`. Probes were counted separately (43). + +| Layer | Denials | Sessions | +| --- | --- | --- | +| Hooks | 220 | 29 | +| Sandbox network | 307 rows from 89 calls | 8 | +| Harness guards | 42 | 9 | +| Sandbox filesystem | 37 | 8 | +| Auto-mode classifier | 15 (matches `auto-denials.jsonl`) | 8 | +| User rejected the call | 8 | 7 | + +No permission-rule denials were found. **Andrew's own PreToolUse hooks are +the largest source: 220 blocks, in every affected session.** The classifier +caused 15. A hook block runs before permission rules and wins over any allow +rule, so new allow rules cannot remove these blocks. The hooks themselves have +to change. + +Known gaps: blocks inside headless reviewer runs, output hidden by +`2>/dev/null`, and the 11,138 `blocked-commands.log` records (only 38 match a +transcript block; the rest are probably hook test fixtures, not verified). + +## How the layers fit together + +From the Claude Code docs (2.1.312), quoted in the session report: + +1. **PreToolUse hooks run first.** A hook that blocks (exit 2) stops the call + before permission rules are read, so an allow rule cannot override it. +2. **Permission rules** (`deny` > `ask` > `allow`) resolve next. Deny blocks in + every mode. +3. **Auto mode** then approves edits in the working directory and sends the + rest to the classifier. Entering auto mode drops broad rules (`Bash(*)`, + interpreter wildcards, package-manager run commands, `Agent` and `Monitor` + allow rules). Narrow rules stay. +4. **Protected paths** (`.git`, `.claude`, `.gitconfig`, shell rc files and + others) always go to the classifier in auto mode. Allow rules do not + pre-approve them. +5. **The Bash sandbox** limits what a command can write and reach. + `autoAllowBashIfSandboxed` runs sandboxed Bash without a prompt. Inside the + working directory, it always denies writes to `.git/config` and + `.git/hooks`, and no setting except `filesystem.disabled` lifts that. +6. **`excludedCommands`** run fully outside the sandbox, still through + permission rules and the classifier. They match only the text of a bare + command: `cd x && gh pr merge 5 | grep y` stays sandboxed. + +## Decisions by pattern + +### Worktrees (sandbox `.git/config`, 7 + harness guard 31) + +**A, through the harness.** Tested end to end on 2026-10-27 in dev-env, a repo +where `.git/config` was confirmed write-protected first. + +1. Create: `EnterWorktree`, then `git switch -c /` + (`claude/...` on personal repos, `arich/...` on beacon-biosignals). + `git branch -m` is not used: it renames the ref but exits 128 on the config + update. The remote sees only the branch name. +2. Work, commit, `git push origin ` (no `-u`; it writes `.git/config`), + `gh pr create --head `. +3. After the merge: check that `gh pr view --json state,headRefName` shows + `MERGED` for the current branch and `git status` is clean. Then + `ExitWorktree` with `remove` and `discard_changes: true`. This rule + pre-authorizes that flag under those two conditions only. A squash merge + never puts the branch's commits on `main`, so the tool always asks for it. +4. Delete the branch made in step 1. **Test first:** + `git update-ref -d refs/heads/` should delete it without touching + `.git/config`. If it works, it replaces `git branch -D`, which deletes the + ref but prints an unactionable config warning. +5. `ExitWorktree remove` leaves an empty `.git/worktrees//` folder. The + sandbox denies removing that folder; which rule denies it is not + identified. The scheduled `my-repos` run (outside the sandbox) runs + `git worktree prune` in each repo. Prune is git's own cleanup, so no + hand-written delete is needed. + +Constraints learned: + +- Do not call `EnterWorktree` while background agents are running in the main + checkout. The isolation checks cover every subagent, and an agent whose + working directory is the main checkout loses Bash entirely. Start agents + after entering, or with `isolation: "worktree"`. +- Inside a worktree session, git commands must be simple: one git call, a + literal path, no `GIT_CONFIG_*`, no loops or `bash -c` around git. +- `hook-block-git-worktree.sh` (10 false positives on `$W/.claude/worktrees`) + becomes simpler: block raw `git worktree add` and point to `EnterWorktree`. +- Protocol 6's `git -C pull` after a merge is dropped. Primary + checkouts are updated by `my-repos`, not by agents (it also hit the + protected `CLAUDE.md` 4 times). + +### Hooks + +Hooks are the main source of friction, so they get the most change. Two +principles: + +1. **Check at the layer that has the facts.** A PreToolUse hook sees only + command text, before anything runs. A check about what a commit contains + belongs in git's own pre-commit hook, which sees the real staged index. +2. **Documentation is not a fix for a repeating block.** The repo-resolution + blocks below kept happening while CLAUDE.md already stated the rule. A + block that recurs across sessions is a hook to change (A), not a rule to + restate. + +| Pattern | Denials / sessions | Answer | +| --- | --- | --- | +| Copy gate: `git add` and `git commit` in one call | 56 / 20 | **A.** The PreToolUse hook reads the index before the call, so it cannot see what `git add` will stage. Move the copy check into the git pre-commit hook, which sees the final index. Then the call shape no longer matters. Until that ships, the hook message already names the split. | +| Repo cannot be resolved: `git -C $VAR`, `cd …`, `env -C` (personify, main-commit hooks) | 57 / 15+ | **A.** Most cases assign the variable in the same command (`W=/abs/path; git -C "$W" …`). Resolve literal assignments made earlier in the same command text, and `cd ` before git. Block only what stays unresolved, and say which part could not be resolved. | +| Worktree path check on `$W/...` | 10 / 7 | **B**, see Worktrees. | +| Copy gate: no approval for public copy | 31 / up to 17 | Correct block. No change. | +| Secret-leak hook | 15 / 10 | Mostly correct. One false positive on `env \| grep -c` (prints a count). No change; the hook message already gives the safe forms. | +| `merge-lock.sh authorize` matched inside quoted text | 3 / 2 | **A.** Match the command position, not a substring. Low priority. | +| Fail-closed when a hook cannot run | 3 / 2 | Correct block (deploy ordering). | + +### Sandbox filesystem + +| Pattern | Denials | Answer | +| --- | --- | --- | +| `gh pr merge` in a compound or piped command cannot write `~/.claude/merge-locks` | 10 | **B.** Excluded commands are written bare: no `cd`, pipe or `&&`. Add a PreToolUse check that rejects the compound form *before* it runs, with the bare form in the message, so the failure is instant and says what to do. | +| `gate-review.sh` compound form cannot write `~/.claude/gate-review` | 5 | **B**, same check. | +| Scripts that hard-code `/tmp` (`post-push-status.sh`, gh-wrapper tests) | 8 | **A.** Fix the scripts to use `$TMPDIR`. Do not add `/tmp` to `allowWrite`. | +| `ps` blocked | 1 | **Decide:** add `ps *` to `excludedCommands` (read-only) or leave it. | + +### Sandbox network + +| Pattern | Volume | Answer | +| --- | --- | --- | +| Hook reviewers (`claude -p` from git hooks) try MCP and telemetry hosts | ~280 rows, ~3,900 lines; nothing fails | **B.** Start reviewers with no MCP servers and no telemetry. Do not allow these hosts. The exact flags need checking against the docs. | +| `.local` names do not resolve when `ssh`/`rsync` is not the leading command | 21 | **Test.** This also decides whether `dotall-fleet` (ssh in a loop) can run from Claude. Options: add the script to `excludedCommands`, or find why mDNS fails in the sandbox. | +| pypi.org, my.1password.com | 1 each | Decide when they recur. | + +### Classifier (15 denials) + +Correct: Credential Materialization, Self-Approval, Security Weaken, +Credential Exploration (ssh to the company machine). False positives, fixed +with `autoMode.allow` prose in the per-machine settings: + +- "Auto-Mode Bypass" (3) and "Unrequested Commit" (2) on plain commits in + `.claude/worktrees/`. +- "External System Writes" (1) on closing an issue in Andrew's own repo + (already allowed in prose; re-check the wording). +- "Remote Shell Writes" (1) on `ssh tilsit.local 'git pull'`, which the + `dotall-fleet` work needs. + +### Noise with no action + +| Source | Answer | +| --- | --- | +| `fsmonitor_ipc__send_query` on every git command | The sandbox blocks the daemon socket, so sandboxed git already runs without fsmonitor. **Decide:** (a) turn it off in `dotfiles/git/config` (measured: `git status` without it takes 20 ms or less on the largest local repo, 1,163 files; repos on other machines not measured), or (b) turn it off for Claude sessions only with `GIT_CONFIG_*` in settings `env`. (b) is untested: the worktree guard refuses inline `GIT_CONFIG_COUNT`, and whether it sees the settings `env` is not known. | +| `branch -D` config warning | Worktrees step 4 (`update-ref -d`). | +| Reviewer network denials | Sandbox network, first row. | +| Pre-push reviewer: "Credit balance is too low; retrying on subscription" | An account state, not a sandbox issue. Every push runs a failed API-key attempt first. **Decide:** top up, or make the subscription the first choice. | + +## General permission rules + +These rules are the smaller lever: none of the 633 denials came from a +missing permission rule. They make the boundary explicit and remove classifier +false positives. Scope: with `autoAllowBashIfSandboxed`, Bash allow rules +change little. The +rules that change behavior are Edit path rules, `autoMode` prose and +`excludedCommands`. Rules are written by intent, not by file or line. + +**One write boundary.** Whatever sandboxed Bash may write, the Edit tool may +write. `Edit` rules cover every built-in file-editing tool (`Write(...)` rules +never match). Protected paths stay protected regardless. + +```json +{ + "permissions": { + "allow": [ + "Edit(~/Developer/**)", + "Edit(~/.local/state/**)" + ], + "deny": [ + "Bash(gh pr merge * --admin*)", + "Bash(gh pr merge * --auto*)" + ] + } +} +``` + +The deny rules guard the `autoMode.allow` merge entry below: a `*` in a Bash +rule matches spaces, so a narrower allow rule could still match `--admin`. + +`autoMode.allow` additions (prose, per machine, alongside `$defaults`): + +- "Committing and pushing on a branch inside `/.claude/worktrees/` in a + repo owned by smartwatermelon, nightowlstudiollc or twistedmelonman is + normal development work." +- "Running dev-environment sync commands (`my-repos`, `dotall`, `git pull`) + over ssh on asiago.local, tilsit.local, mimolette.local and parmesan.local + is allowed. arich-mac.local stays read-only." + +## Where the settings live + +The sandbox block and the `autoMode` block are in +`~/.config/claude-code/machine-settings.json`, a plain file with no repo and +no history. The 2026-10-20 plan keeps `autoMode` per machine (the Beacon +leak). **Decide:** + +- `sandbox` and `permissions` move to `claude-config` `settings.json` + (shared, tracked, reviewed). +- `autoMode` stays per machine, generated from a tracked per-host file by an + installer (the lock-sync pattern), so each machine's copy has a source. + +## Follow-up work, by repo + +None of this is done in this PR. + +- **claude-config, hooks (largest gain):** copy check moved to the git + pre-commit hook; repo resolution of same-command assignments and + `cd `; the compound excluded-command check; + `hook-block-git-worktree.sh` simplified; the `merge-lock` substring fix. + Each hook change is validated against the matching rows of + `denials.jsonl`: replay the blocked commands and confirm the false + positives pass and the correct blocks still block. +- **claude-config, policy:** Protocol 1 and 6 rewritten for the worktree + procedure; the settings move. +- **scripts:** `my-repos` runs `git worktree prune`; `dotall-fleet` waits on + the `.local` test. +- **dotfiles:** fsmonitor (after the decision); gh-wrapper test `/tmp` paths. +- **review hooks:** reviewers without MCP or telemetry. +- **Tests to run first:** `update-ref -d` branch delete; `.local` resolution + in the sandbox; settings `env` versus the worktree guard. From 8ce155aa15e6a873268a78adf30853bc2f82104b Mon Sep 17 00:00:00 2001 From: Claude Code Bot Date: Tue, 27 Oct 2026 17:11:29 -0700 Subject: [PATCH 2/5] docs(plans): split shared settings from per-machine overlays Shared rules go in claude-config settings.json; host-specific paths, defaultMode and autoMode environment text go in each Mac's overlay, generated from a tracked per-host file. --settings arrays concatenate, so the shared file is a floor every machine gets. --- docs/plans/2026-10-27-permission-policy.md | 32 +++++++++++++++++----- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/docs/plans/2026-10-27-permission-policy.md b/docs/plans/2026-10-27-permission-policy.md index e3f5f03..48c44d6 100644 --- a/docs/plans/2026-10-27-permission-policy.md +++ b/docs/plans/2026-10-27-permission-policy.md @@ -228,12 +228,28 @@ rule matches spaces, so a narrower allow rule could still match `--admin`. The sandbox block and the `autoMode` block are in `~/.config/claude-code/machine-settings.json`, a plain file with no repo and no history. The 2026-10-20 plan keeps `autoMode` per machine (the Beacon -leak). **Decide:** - -- `sandbox` and `permissions` move to `claude-config` `settings.json` - (shared, tracked, reviewed). -- `autoMode` stays per machine, generated from a tracked per-host file by an - installer (the lock-sync pattern), so each machine's copy has a source. +leak). Several sandbox entries are also specific to this Mac: its +`/var/folders/...` temp folder, the personal Google Drive path, and +`/private/tmp/claude-501` (a user ID that differs on the work Mac). + +**Rule: global settings everywhere, with a local overlay per machine.** +`--settings` arrays concatenate with the shared file and objects merge by key +(measured 2026-10-20), so an overlay can add entries but never remove shared +ones. That makes the shared file a floor every machine gets. **Decide:** + +- **Shared (`claude-config` `settings.json`, tracked and reviewed):** the + sandbox switches (`enabled`, `failIfUnavailable`, `allowUnsandboxedCommands`, + `autoAllowBashIfSandboxed`); `allowWrite` entries written with `~` and + valid on every Mac (`~/Developer`, `~/.local/state`, `~/.cache`, + `~/Library/Caches`); shared `excludedCommands`, network domains, + `permissions` rules; and `autoMode` entries true on every machine. +- **Per machine (the overlay, `machine-settings.json`):** host-specific paths + (temp folder, Drive folder, user-ID paths), `defaultMode`, and `autoMode` + `environment` text that names this machine's hosts and trust. Each host's + overlay is generated from a tracked per-host file by an installer (the + lock-sync pattern), so it has a source and history. +- The settings move needs a check on each machine before it ships: a session + on the work Mac must start with the shared sandbox and no ASIAGO paths. ## Follow-up work, by repo @@ -247,7 +263,9 @@ None of this is done in this PR. `denials.jsonl`: replay the blocked commands and confirm the false positives pass and the correct blocks still block. - **claude-config, policy:** Protocol 1 and 6 rewritten for the worktree - procedure; the settings move. + procedure; the shared half of the settings split. +- **claude-wrapper:** tracked per-host overlay files and the installer that + writes `machine-settings.json` from them. - **scripts:** `my-repos` runs `git worktree prune`; `dotall-fleet` waits on the `.local` test. - **dotfiles:** fsmonitor (after the decision); gh-wrapper test `/tmp` paths. From f0305f884211026242e3cb0c4f0a8cff3bac86a5 Mon Sep 17 00:00:00 2001 From: Claude Code Bot Date: Tue, 27 Oct 2026 17:24:03 -0700 Subject: [PATCH 3/5] docs(plans): record Andrew's 2026-10-27 policy decisions fsmonitor off for Claude sessions only (test the settings-env route first); ps added to excludedCommands; the API-credit fallback is working as designed; the settings split is accepted, with a fleet drift audit before it ships. --- docs/plans/2026-10-27-permission-policy.md | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/docs/plans/2026-10-27-permission-policy.md b/docs/plans/2026-10-27-permission-policy.md index 48c44d6..ea70fbc 100644 --- a/docs/plans/2026-10-27-permission-policy.md +++ b/docs/plans/2026-10-27-permission-policy.md @@ -1,6 +1,6 @@ # Plan: one permission policy for sandbox, rules, classifier and hooks -**Status:** proposal, waiting for Andrew's decisions (marked **Decide**) +**Status:** proposal; Andrew's decisions recorded 2026-10-27 **Date:** 2026-10-27 **Author:** arich (drafted by Claude Code) @@ -151,7 +151,7 @@ principles: | `gh pr merge` in a compound or piped command cannot write `~/.claude/merge-locks` | 10 | **B.** Excluded commands are written bare: no `cd`, pipe or `&&`. Add a PreToolUse check that rejects the compound form *before* it runs, with the bare form in the message, so the failure is instant and says what to do. | | `gate-review.sh` compound form cannot write `~/.claude/gate-review` | 5 | **B**, same check. | | Scripts that hard-code `/tmp` (`post-push-status.sh`, gh-wrapper tests) | 8 | **A.** Fix the scripts to use `$TMPDIR`. Do not add `/tmp` to `allowWrite`. | -| `ps` blocked | 1 | **Decide:** add `ps *` to `excludedCommands` (read-only) or leave it. | +| `ps` blocked | 1 | **A** (decided 2026-10-27): add `ps *` to the shared `excludedCommands`. It only reads the process list. | ### Sandbox network @@ -178,10 +178,10 @@ with `autoMode.allow` prose in the per-machine settings: | Source | Answer | | --- | --- | -| `fsmonitor_ipc__send_query` on every git command | The sandbox blocks the daemon socket, so sandboxed git already runs without fsmonitor. **Decide:** (a) turn it off in `dotfiles/git/config` (measured: `git status` without it takes 20 ms or less on the largest local repo, 1,163 files; repos on other machines not measured), or (b) turn it off for Claude sessions only with `GIT_CONFIG_*` in settings `env`. (b) is untested: the worktree guard refuses inline `GIT_CONFIG_COUNT`, and whether it sees the settings `env` is not known. | +| `fsmonitor_ipc__send_query` on every git command | The sandbox blocks the daemon socket, so sandboxed git already runs without fsmonitor. **Decided 2026-10-27: off for Claude sessions only**, on for Andrew's terminal. Candidate: `GIT_CONFIG_COUNT`/`KEY_0`/`VALUE_0` in the shared settings `env`. **Test first:** the worktree guard refuses inline `GIT_CONFIG_COUNT`; whether it also refuses when the value comes from settings `env` is not known. If it does, look for another Claude-only switch (for example a `GIT_CONFIG_GLOBAL` file that includes the real config and overrides `core.fsmonitor`). Fallback, only with Andrew's approval: off everywhere (measured 20 ms or less for `git status` on the largest local repo, 1,163 files). | | `branch -D` config warning | Worktrees step 4 (`update-ref -d`). | | Reviewer network denials | Sandbox network, first row. | -| Pre-push reviewer: "Credit balance is too low; retrying on subscription" | An account state, not a sandbox issue. Every push runs a failed API-key attempt first. **Decide:** top up, or make the subscription the first choice. | +| Pre-push reviewer: "Credit balance is too low; retrying on subscription" | Working as designed (Andrew, 2026-10-27): `claude -p` uses the monthly $100 Max API credit first and falls back to the subscription when it runs out. Optional: reword the line so it reads as expected behavior, not a failure. | ## General permission rules @@ -235,7 +235,10 @@ leak). Several sandbox entries are also specific to this Mac: its **Rule: global settings everywhere, with a local overlay per machine.** `--settings` arrays concatenate with the shared file and objects merge by key (measured 2026-10-20), so an overlay can add entries but never remove shared -ones. That makes the shared file a floor every machine gets. **Decide:** +ones. That makes the shared file a floor every machine gets. **Decided +2026-10-27.** Before the move, audit the fleet: each Mac may already have its +own `machine-settings.json`, `settings.local.json` or other drift, and each +must be compared with the split below so that nothing it relies on is lost. - **Shared (`claude-config` `settings.json`, tracked and reviewed):** the sandbox switches (`enabled`, `failIfUnavailable`, `allowUnsandboxedCommands`, @@ -268,7 +271,10 @@ None of this is done in this PR. writes `machine-settings.json` from them. - **scripts:** `my-repos` runs `git worktree prune`; `dotall-fleet` waits on the `.local` test. -- **dotfiles:** fsmonitor (after the decision); gh-wrapper test `/tmp` paths. +- **dotfiles / claude-config:** the Claude-only fsmonitor switch (after its + test); gh-wrapper test `/tmp` paths. +- **Fleet audit:** inventory each Mac's existing settings files and drift + before the settings split ships. - **review hooks:** reviewers without MCP or telemetry. - **Tests to run first:** `update-ref -d` branch delete; `.local` resolution in the sandbox; settings `env` versus the worktree guard. From db1622581b7d6fd32051fe2254802ea6846668d6 Mon Sep 17 00:00:00 2001 From: Claude Code Bot Date: Tue, 27 Oct 2026 17:25:11 -0700 Subject: [PATCH 4/5] docs(status): say the policy decisions are recorded and name what is open --- docs/STATUS.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/STATUS.md b/docs/STATUS.md index e813181..5f81d9a 100644 --- a/docs/STATUS.md +++ b/docs/STATUS.md @@ -682,12 +682,14 @@ or be explicitly parked with a resume note, by then — see the tracker. **2026-10-27 update (Pacific): permission policy:** -- **Proposal, waiting on decisions:** +- **Proposal, decisions recorded 2026-10-27:** `docs/plans/2026-10-27-permission-policy.md`. An inventory of every block since 2026-10-15 found 633: hooks 220 (every affected session), sandbox network 307 rows, classifier 15, permission rules 0. Every blocked action gets A (make it work) or B (change the procedure), never "ask Andrew". Worktrees move to `EnterWorktree`/`ExitWorktree`, tested end to end. + Still open: the three tests to run first, the fleet settings audit, and the + hook and settings work listed by repo. **Kanban tracker: GitHub Issues + a Project board in `smartwatermelon/dev-env`**, using the org's `Priority`/`Target date`/`Effort` issue fields. Board columns From 61c393a6cd0ce727c17330b318289a3eb64455c7 Mon Sep 17 00:00:00 2001 From: Claude Code Bot Date: Tue, 27 Oct 2026 17:33:00 -0700 Subject: [PATCH 5/5] docs(plans): add the kept-worktree cleanup path and the re-entry hook fix ExitWorktree remove only works while the session is still in the worktree it created. Kept worktrees are now removed by a nightly worktree-reaper.sh outside the sandbox (B), and hook-block-enter-worktree.sh must accept path re-entry (A). Corrects the claim that my-repos runs on a schedule; it does not. --- docs/plans/2026-10-27-permission-policy.md | 32 +++++++++++++++++----- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/docs/plans/2026-10-27-permission-policy.md b/docs/plans/2026-10-27-permission-policy.md index ea70fbc..a65f75a 100644 --- a/docs/plans/2026-10-27-permission-policy.md +++ b/docs/plans/2026-10-27-permission-policy.md @@ -101,11 +101,27 @@ where `.git/config` was confirmed write-protected first. `git update-ref -d refs/heads/` should delete it without touching `.git/config`. If it works, it replaces `git branch -D`, which deletes the ref but prints an unactionable config warning. -5. `ExitWorktree remove` leaves an empty `.git/worktrees//` folder. The - sandbox denies removing that folder; which rule denies it is not - identified. The scheduled `my-repos` run (outside the sandbox) runs - `git worktree prune` in each repo. Prune is git's own cleanup, so no - hand-written delete is needed. +5. `ExitWorktree remove` works only while the session is still inside the + worktree it created. It cannot remove a worktree left with `keep`, one + from an earlier session, or one re-entered by `path`. A sandboxed + `git worktree remove` fails too: Claude Code's own `.claude/.cc-writes` + folder inside the worktree is write-denied. It also leaves an empty + `.git/worktrees//` folder that the sandbox will not remove (rule not + identified). + + **B: agents never remove a kept worktree.** When the PR is still open, the + agent leaves with `keep`. A nightly `worktree-reaper.sh` (scripts repo, own + LaunchAgent, outside the sandbox) checks every worktree under + `~/Developer/*/.claude/worktrees/`. If the tree is clean and `gh` shows its + branch's PR as `MERGED`, it runs `git worktree remove`, deletes the branch + and runs `git worktree prune`. Anything else (dirty, no PR, PR open), it + only reports. Nothing is scheduled today: the nightly `updates` job runs + package updates only, and `my-repos` is not scheduled. +6. Coming back to a kept worktree (for example to fix a review finding) + uses `EnterWorktree` with `path`. **A:** `hook-block-enter-worktree.sh` + rejects `path` today, because it only validates the `name` of a new + worktree. It must accept a `path` that is an existing worktree under that + repo's `.claude/worktrees/`. Constraints learned: @@ -269,8 +285,10 @@ None of this is done in this PR. procedure; the shared half of the settings split. - **claude-wrapper:** tracked per-host overlay files and the installer that writes `machine-settings.json` from them. -- **scripts:** `my-repos` runs `git worktree prune`; `dotall-fleet` waits on - the `.local` test. +- **claude-config:** `hook-block-enter-worktree.sh` accepts `path` + re-entry into an existing worktree. +- **scripts:** `worktree-reaper.sh` and its LaunchAgent; `dotall-fleet` + waits on the `.local` test. - **dotfiles / claude-config:** the Claude-only fsmonitor switch (after its test); gh-wrapper test `/tmp` paths. - **Fleet audit:** inventory each Mac's existing settings files and drift