Skip to content

Commit dfa58cc

Browse files
os-zhuangclaude
andcommitted
fix(agents): guard resolves file's nearest existing ancestor dir (no fail-open)
Follow-up to #2517. The guard ran `git -C "$(dirname file)"`, which fails when the edited file's parent directory doesn't exist yet (a Write creating a new nested dir) — and `|| exit 0` then let the edit through on the shared checkout. Now it walks up to the nearest existing ancestor before resolving the repo, so new-file-in-new-dir writes are guarded too. (Also applied to objectui#2160 / cloud#721.) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 18b431e commit dfa58cc

1 file changed

Lines changed: 26 additions & 29 deletions

File tree

.claude/hooks/guard-main-checkout.sh

Lines changed: 26 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,27 @@
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

2121
set -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.
2625
input="$(cat 2>/dev/null || true)"
2726
file=""
2827
if 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)"
3332
fi
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.
4242
case "$gitdir" in
4343
*/worktrees/*) exit 0 ;;
4444
esac
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 '?')"
4948
name="$(basename "$root")"
5049
cat >&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
6562
Deliberate non-task exception: re-run with OS_ALLOW_MAIN_EDITS=1.
6663
EOF

0 commit comments

Comments
 (0)