|
1 | 1 | #!/usr/bin/env bash |
2 | 2 | # guard-main-checkout.sh — PreToolUse guard enforcing AGENTS.md Prime Directive #11 |
3 | | -# (worktree-first). Blocks Edit / Write / NotebookEdit while the session's checkout |
4 | | -# is on the shared `main` branch. |
| 3 | +# (worktree-first). Blocks Edit / Write / NotebookEdit unless the file being edited |
| 4 | +# lives in a dedicated git WORKTREE — not the shared primary checkout. |
5 | 5 | # |
6 | | -# Why: this repo is worked on by multiple agents in parallel. The shared `main` |
7 | | -# checkout has its HEAD switched and the tree reset *under you* by other agents, |
8 | | -# silently clobbering uncommitted edits (observed: a full session's work reverted |
9 | | -# twice). Dedicated per-task worktrees are physically isolated, so edits there are |
10 | | -# safe. This guard turns the documented discipline into a hard stop for the one |
11 | | -# place it actually fails — editing on `main`. |
| 6 | +# Why: this repo (and siblings objectui/cloud) are edited by MULTIPLE agents at once. |
| 7 | +# The shared primary checkout has its HEAD switched and its tree reset *under you* by |
| 8 | +# other agents, silently clobbering uncommitted work. A feature branch on the shared |
| 9 | +# checkout is NOT enough — it still gets switched under you. Only a dedicated per-task |
| 10 | +# worktree is physically isolated. |
12 | 11 | # |
13 | | -# Deliberate exception (a human quick-fix that will still land via PR, never task |
14 | | -# work committed straight to main): export OS_ALLOW_MAIN_EDITS=1 for the session. |
| 12 | +# Hardened vs the old guard (two holes agents fell through): |
| 13 | +# 1. Checks "am I in a linked worktree?" — not merely "branch != main". Creating a |
| 14 | +# feature branch on the shared checkout used to pass the guard; now it's blocked. |
| 15 | +# 2. Checks the EDITED FILE's repo — not just $CLAUDE_PROJECT_DIR. So editing a |
| 16 | +# sibling repo (objectui/cloud) on its shared checkout from this session is |
| 17 | +# guarded too, instead of silently allowed. |
| 18 | +# |
| 19 | +# Deliberate exception (a human quick-fix that still lands via PR): OS_ALLOW_MAIN_EDITS=1. |
15 | 20 |
|
16 | 21 | set -uo pipefail |
17 | 22 |
|
18 | | -# Escape hatch. |
19 | | -if [ "${OS_ALLOW_MAIN_EDITS:-}" = "1" ]; then |
20 | | - exit 0 |
| 23 | +[ "${OS_ALLOW_MAIN_EDITS:-}" = "1" ] && exit 0 |
| 24 | + |
| 25 | +# PreToolUse passes the tool call as JSON on stdin; pull out tool_input.file_path. |
| 26 | +input="$(cat 2>/dev/null || true)" |
| 27 | +file="" |
| 28 | +if command -v jq >/dev/null 2>&1; then |
| 29 | + file="$(printf '%s' "$input" | jq -r '.tool_input.file_path // empty' 2>/dev/null || true)" |
| 30 | +fi |
| 31 | +if [ -z "$file" ]; then |
| 32 | + file="$(printf '%s' "$input" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/^.*"\([^"]*\)"$/\1/' || true)" |
21 | 33 | fi |
22 | 34 |
|
23 | | -dir="${CLAUDE_PROJECT_DIR:-$PWD}" |
| 35 | +# Directory whose checkout we judge: the edited file's dir, else the project/cwd. |
| 36 | +if [ -n "$file" ]; then chk="$(dirname "$file")"; else chk="${CLAUDE_PROJECT_DIR:-$PWD}"; fi |
24 | 37 |
|
25 | | -# Not a git repo (or git unavailable) → nothing to guard, allow. |
26 | | -branch="$(git -C "$dir" rev-parse --abbrev-ref HEAD 2>/dev/null)" || exit 0 |
| 38 | +# Resolve the git dir for that path. Not a git repo (or git missing) → nothing to guard. |
| 39 | +gitdir="$(git -C "$chk" rev-parse --git-dir 2>/dev/null)" || exit 0 |
27 | 40 |
|
28 | | -if [ "$branch" = "main" ]; then |
29 | | - root="$(git -C "$dir" rev-parse --show-toplevel 2>/dev/null || printf '%s' "$dir")" |
30 | | - cat >&2 <<EOF |
31 | | -⛔ Blocked: editing files while on the shared 'main' checkout (branch=main, root=$root). |
| 41 | +# A linked worktree's git-dir lives under <common>/.git/worktrees/<name> → isolated → allow. |
| 42 | +case "$gitdir" in |
| 43 | + */worktrees/*) exit 0 ;; |
| 44 | +esac |
32 | 45 |
|
33 | | -This repo is worked on by multiple agents in parallel — the shared 'main' tree gets |
34 | | -its HEAD switched and reset under you, silently clobbering uncommitted edits. |
| 46 | +# Otherwise this is the shared PRIMARY checkout → block regardless of branch. |
| 47 | +root="$(git -C "$chk" rev-parse --show-toplevel 2>/dev/null || printf '%s' "$chk")" |
| 48 | +branch="$(git -C "$chk" rev-parse --abbrev-ref HEAD 2>/dev/null || printf '?')" |
| 49 | +name="$(basename "$root")" |
| 50 | +cat >&2 <<EOF |
| 51 | +⛔ Blocked: editing on the shared PRIMARY checkout, not a worktree. |
| 52 | + repo: $root (branch: $branch) |
35 | 53 |
|
36 | | -Per AGENTS.md Prime Directive #11 (worktree-first), create a dedicated worktree and |
37 | | -re-run your edits from there: |
| 54 | +Per AGENTS.md Prime Directive #11 (worktree-first): this repo is edited by multiple |
| 55 | +agents at once — the shared checkout gets its HEAD switched and tree reset under you, |
| 56 | +silently clobbering uncommitted work. A feature branch on the shared checkout is NOT |
| 57 | +enough; you must be in a dedicated worktree: |
38 | 58 |
|
39 | | - git worktree add ../framework-<task> -b <branch> main |
40 | | - cd ../framework-<task> && pnpm install |
| 59 | + git worktree add ../${name}-<task> -b <branch> main |
| 60 | + cd ../${name}-<task> && pnpm install # then re-run your edits there |
41 | 61 |
|
42 | | -Deliberate exception (not task work): re-run with OS_ALLOW_MAIN_EDITS=1. |
43 | | -EOF |
44 | | - exit 2 |
45 | | -fi |
| 62 | +This guard now checks the edited file's OWN repo, so sibling repos (objectui / cloud) |
| 63 | +are covered too — not just this project. |
46 | 64 |
|
47 | | -exit 0 |
| 65 | +Deliberate non-task exception: re-run with OS_ALLOW_MAIN_EDITS=1. |
| 66 | +EOF |
| 67 | +exit 2 |
0 commit comments