11#! /usr/bin/env bash
2- # guard-main-checkout.sh — PreToolUse guard enforcing AGENTS.md Prime Directive #11
3- # (worktree-first). Blocks Edit / Write / NotebookEdit unless the file being edited
4- # lives in a dedicated git WORKTREE — not the shared primary checkout.
2+ # guard-main-checkout.sh — PreToolUse guard enforcing AGENTS.md worktree-first rule.
3+ # Blocks Edit / Write / NotebookEdit unless the file being edited lives in a dedicated
4+ # git WORKTREE — not the shared primary checkout.
55#
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.
6+ # Why: this repo (and its sibling repos) are edited by MULTIPLE agents at once. The
7+ # shared primary checkout has its HEAD switched and its tree reset *under you*, silently
8+ # clobbering uncommitted work. A feature branch on the shared checkout is NOT enough —
9+ # it still gets switched under you. Only a dedicated per-task worktree is isolated.
1110#
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.
11+ # Hardened over the original guard (holes agents fell through):
12+ # 1. Checks "am I in a linked worktree?" — not merely "branch != default". A feature
13+ # branch on the shared checkout no longer passes.
14+ # 2. Checks the EDITED FILE's repo — not just $CLAUDE_PROJECT_DIR — so sibling repos
15+ # edited from this session are guarded too.
16+ # 3. Resolves the file's nearest EXISTING ancestor dir, so creating a new file in a
17+ # new directory can't fail-open past the guard.
1818#
1919# Deliberate exception (a human quick-fix that still lands via PR): OS_ALLOW_MAIN_EDITS=1.
2020
2121set -uo pipefail
2222
2323[ " ${OS_ALLOW_MAIN_EDITS:- } " = " 1" ] && exit 0
2424
25- # PreToolUse passes the tool call as JSON on stdin; pull out tool_input.file_path.
2625input=" $( cat 2> /dev/null || true) "
2726file=" "
2827if command -v jq > /dev/null 2>&1 ; then
@@ -32,35 +31,33 @@ if [ -z "$file" ]; then
3231 file=" $( printf ' %s' " $input " | grep -o ' "file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed ' s/^.*"\([^"]*\)"$/\1/' || true) "
3332fi
3433
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
34+ # Judge the checkout at the file's nearest existing ancestor dir (handles new files in
35+ # not-yet-created directories). Fall back to the project dir when no file path is given.
36+ if [ -n " $file " ]; then d=" $( dirname " $file " ) " ; else d=" ${CLAUDE_PROJECT_DIR:- $PWD } " ; fi
37+ while [ -n " $d " ] && [ " $d " != " /" ] && [ ! -d " $d " ]; do d=" $( dirname " $d " ) " ; done
38+ [ -d " $d " ] || d=" ${CLAUDE_PROJECT_DIR:- $PWD } "
3739
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
40+ gitdir=" $( git -C " $d " rev-parse --git-dir 2> /dev/null) " || exit 0
4041
41- # A linked worktree's git-dir lives under <common>/.git/worktrees/<name> → isolated → allow.
4242case " $gitdir " in
4343 * /worktrees/* ) exit 0 ;;
4444esac
4545
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 ' ?' ) "
46+ root=" $( git -C " $d " rev-parse --show-toplevel 2> /dev/null || printf ' %s' " $d " ) "
47+ branch=" $( git -C " $d " rev-parse --abbrev-ref HEAD 2> /dev/null || printf ' ?' ) "
4948name=" $( basename " $root " ) "
5049cat >&2 << EOF
5150⛔ Blocked: editing on the shared PRIMARY checkout, not a worktree.
5251 repo: $root (branch: $branch )
5352
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:
53+ This repo is edited by multiple agents at once — the shared checkout gets its HEAD
54+ switched and tree reset under you, silently clobbering uncommitted work. A feature
55+ branch on the shared checkout is NOT enough; you must be in a dedicated worktree:
5856
5957 git worktree add ../${name} -<task> -b <branch> main
6058 cd ../${name} -<task> && pnpm install # then re-run your edits there
6159
62- This guard now checks the edited file's OWN repo, so sibling repos (objectui / cloud)
63- are covered too — not just this project.
60+ This guard checks the edited file's OWN repo, so sibling repos are covered too.
6461
6562Deliberate non-task exception: re-run with OS_ALLOW_MAIN_EDITS=1.
6663EOF
0 commit comments