From dfa58ccf5e00fc776842717867acdf2dd1aa10d9 Mon Sep 17 00:00:00 2001 From: os-zhuang Date: Thu, 2 Jul 2026 21:24:21 +0800 Subject: [PATCH] fix(agents): guard resolves file's nearest existing ancestor dir (no fail-open) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .claude/hooks/guard-main-checkout.sh | 55 +++++++++++++--------------- 1 file changed, 26 insertions(+), 29 deletions(-) diff --git a/.claude/hooks/guard-main-checkout.sh b/.claude/hooks/guard-main-checkout.sh index d72df19294..b68dd4f8de 100755 --- a/.claude/hooks/guard-main-checkout.sh +++ b/.claude/hooks/guard-main-checkout.sh @@ -1,20 +1,20 @@ #!/usr/bin/env bash -# guard-main-checkout.sh — PreToolUse guard enforcing AGENTS.md Prime Directive #11 -# (worktree-first). Blocks Edit / Write / NotebookEdit unless the file being edited -# lives in a dedicated git WORKTREE — not the shared primary checkout. +# guard-main-checkout.sh — PreToolUse guard enforcing AGENTS.md worktree-first rule. +# Blocks Edit / Write / NotebookEdit unless the file being edited lives in a dedicated +# git WORKTREE — not the shared primary checkout. # -# Why: this repo (and siblings objectui/cloud) are edited by MULTIPLE agents at once. -# The shared primary checkout has its HEAD switched and its tree reset *under you* by -# other agents, silently clobbering uncommitted work. A feature branch on the shared -# checkout is NOT enough — it still gets switched under you. Only a dedicated per-task -# worktree is physically isolated. +# Why: this repo (and its sibling repos) are edited by MULTIPLE agents at once. The +# shared primary checkout has its HEAD switched and its tree reset *under you*, silently +# clobbering uncommitted work. A feature branch on the shared checkout is NOT enough — +# it still gets switched under you. Only a dedicated per-task worktree is isolated. # -# Hardened vs the old guard (two holes agents fell through): -# 1. Checks "am I in a linked worktree?" — not merely "branch != main". Creating a -# feature branch on the shared checkout used to pass the guard; now it's blocked. -# 2. Checks the EDITED FILE's repo — not just $CLAUDE_PROJECT_DIR. So editing a -# sibling repo (objectui/cloud) on its shared checkout from this session is -# guarded too, instead of silently allowed. +# Hardened over the original guard (holes agents fell through): +# 1. Checks "am I in a linked worktree?" — not merely "branch != default". A feature +# branch on the shared checkout no longer passes. +# 2. Checks the EDITED FILE's repo — not just $CLAUDE_PROJECT_DIR — so sibling repos +# edited from this session are guarded too. +# 3. Resolves the file's nearest EXISTING ancestor dir, so creating a new file in a +# new directory can't fail-open past the guard. # # Deliberate exception (a human quick-fix that still lands via PR): OS_ALLOW_MAIN_EDITS=1. @@ -22,7 +22,6 @@ set -uo pipefail [ "${OS_ALLOW_MAIN_EDITS:-}" = "1" ] && exit 0 -# PreToolUse passes the tool call as JSON on stdin; pull out tool_input.file_path. input="$(cat 2>/dev/null || true)" file="" if command -v jq >/dev/null 2>&1; then @@ -32,35 +31,33 @@ if [ -z "$file" ]; then file="$(printf '%s' "$input" | grep -o '"file_path"[[:space:]]*:[[:space:]]*"[^"]*"' | head -1 | sed 's/^.*"\([^"]*\)"$/\1/' || true)" fi -# Directory whose checkout we judge: the edited file's dir, else the project/cwd. -if [ -n "$file" ]; then chk="$(dirname "$file")"; else chk="${CLAUDE_PROJECT_DIR:-$PWD}"; fi +# Judge the checkout at the file's nearest existing ancestor dir (handles new files in +# not-yet-created directories). Fall back to the project dir when no file path is given. +if [ -n "$file" ]; then d="$(dirname "$file")"; else d="${CLAUDE_PROJECT_DIR:-$PWD}"; fi +while [ -n "$d" ] && [ "$d" != "/" ] && [ ! -d "$d" ]; do d="$(dirname "$d")"; done +[ -d "$d" ] || d="${CLAUDE_PROJECT_DIR:-$PWD}" -# Resolve the git dir for that path. Not a git repo (or git missing) → nothing to guard. -gitdir="$(git -C "$chk" rev-parse --git-dir 2>/dev/null)" || exit 0 +gitdir="$(git -C "$d" rev-parse --git-dir 2>/dev/null)" || exit 0 -# A linked worktree's git-dir lives under /.git/worktrees/ → isolated → allow. case "$gitdir" in */worktrees/*) exit 0 ;; esac -# Otherwise this is the shared PRIMARY checkout → block regardless of branch. -root="$(git -C "$chk" rev-parse --show-toplevel 2>/dev/null || printf '%s' "$chk")" -branch="$(git -C "$chk" rev-parse --abbrev-ref HEAD 2>/dev/null || printf '?')" +root="$(git -C "$d" rev-parse --show-toplevel 2>/dev/null || printf '%s' "$d")" +branch="$(git -C "$d" rev-parse --abbrev-ref HEAD 2>/dev/null || printf '?')" name="$(basename "$root")" cat >&2 < -b main cd ../${name}- && pnpm install # then re-run your edits there -This guard now checks the edited file's OWN repo, so sibling repos (objectui / cloud) -are covered too — not just this project. +This guard checks the edited file's OWN repo, so sibling repos are covered too. Deliberate non-task exception: re-run with OS_ALLOW_MAIN_EDITS=1. EOF