Skip to content

Commit 85eeac7

Browse files
os-zhuangclaude
andcommitted
chore(agents): harden worktree-first guard + surface Directive #11 via CLAUDE.md
The guard-main-checkout PreToolUse hook had two holes agents fell through: 1. It checked `branch != main`, so creating a feature branch on the SHARED primary checkout passed the guard while the tree could still be switched/reset under you. It now checks "is the edited file in a linked worktree" — a branch on the primary checkout is blocked. Enforces the directive's intent, not a proxy. 2. It only inspected $CLAUDE_PROJECT_DIR, so editing a sibling repo (objectui / cloud) on its shared checkout from this session was silently unguarded. It now parses the edited file's path and checks THAT repo's checkout — cross-repo safe. Also: - Add CLAUDE.md (auto-loaded into agent context, unlike AGENTS.md) that inlines the worktree-first rule + the command and points to AGENTS.md for the full playbook. The prior gap: AGENTS.md #11 was clear but never reached the agent's context. - Update AGENTS.md #11 to describe the hardened hook behaviour. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c250ab6 commit 85eeac7

3 files changed

Lines changed: 79 additions & 32 deletions

File tree

Lines changed: 51 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,67 @@
11
#!/usr/bin/env bash
22
# 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.
55
#
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.
1211
#
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.
1520

1621
set -uo pipefail
1722

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)"
2133
fi
2234

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
2437

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
2740

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
3245

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)
3553
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:
3858
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
4161
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.
4664
47-
exit 0
65+
Deliberate non-task exception: re-run with OS_ALLOW_MAIN_EDITS=1.
66+
EOF
67+
exit 2

AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ Other scripts: `objectui:bump` (pull only), `objectui:build`, `objectui:clean`.
7272

7373
When renaming a legacy var, use `readEnvWithDeprecation('OS_NEW', 'LEGACY')` from `@objectstack/types` (keeps legacy working one release). Third-party exceptions kept as-is: `NODE_ENV`, `HOME`, `OPENAI_API_KEY`, `TURSO_*`, OAuth `*_CLIENT_ID/SECRET`, `RESEND_API_KEY`, `POSTMARK_TOKEN`, `AI_GATEWAY_*`, `SMTP_*`. See #1382.
7474
10. **File issues for out-of-scope findings — don't silently expand scope or leave them buried.** When you hit a bug, gap, or unenforced capability that's unrelated to the current task, or too large to fix in scope, open a GitHub issue (`gh issue create`) with a clear repro/decision and link it from your PR. Corollary: **never advertise or demo a capability the runtime doesn't actually deliver** (declared ≠ enforced) — fix it, trim it, or file an issue, but don't fake coverage. Example: the spec declares 9 validation-rule types but the write-path validator enforces only 3 (`state_machine`/`script`/`cross_field`); the other 6 are tracked in #1475 rather than demoed in the showcase.
75-
11. **Worktree-first — never edit on the shared `main` checkout.** This repo is edited by **multiple agents at once**; the shared `main` tree has its HEAD switched and reset *under you*, silently clobbering uncommitted work. Before your **first file edit**, you MUST be in a dedicated worktree on a feature branch: `git worktree add ../framework-<task> -b <branch> main && cd ../framework-<task> && pnpm install`. A PreToolUse hook (`.claude/hooks/guard-main-checkout.sh`) **enforces** this — it blocks `Edit`/`Write`/`NotebookEdit` whenever HEAD is on `main` (override for a deliberate non-task fix with `OS_ALLOW_MAIN_EDITS=1`). Full playbook below.
75+
11. **Worktree-first — never edit on the shared `main` checkout.** This repo is edited by **multiple agents at once**; the shared `main` tree has its HEAD switched and reset *under you*, silently clobbering uncommitted work. Before your **first file edit**, you MUST be in a dedicated worktree on a feature branch: `git worktree add ../framework-<task> -b <branch> main && cd ../framework-<task> && pnpm install`. A PreToolUse hook (`.claude/hooks/guard-main-checkout.sh`) **enforces** this — it blocks `Edit`/`Write`/`NotebookEdit` unless the edited file is in a dedicated **worktree** — a feature branch on the *shared* checkout is **not** enough (it still gets switched under you) — and it checks the **edited file's own repo**, so sibling repos (`objectui`/`cloud`) you touch are covered too (override for a deliberate non-task fix with `OS_ALLOW_MAIN_EDITS=1`). Full playbook below.
7676
12. **Contract-first — fix the metadata, not the runtime.** This is a metadata-driven framework: `packages/spec` is the one contract between metadata *producers* and the runtime/renderers that *consume* it. When a piece of metadata "doesn't work," ask **first**: *is it spec-compliant? is this the long-term-correct direction?* If the metadata is wrong, fix it at the **producer** and **reject it at authoring/publish** (validation / lint) so the error surfaces loudly — do **not** add a lenient alias or `??` fallback in the consumer (a node executor, the REST layer, a renderer) to tolerate off-spec input. A tolerant fallback fossilizes the wrong convention into a second de-facto contract, dilutes the spec, and hides the producer's bug — one strict contract beats N dialects. This is an **internal** contract (we own both ends), so "be liberal in what you accept" (Postel) does **not** apply — that's for untrusted boundaries. Change the **spec** only when the spec itself is genuinely wrong, and then deliberately (edit the Zod schema + migrate), never by accreting consumer-side fallbacks. The existing `cfg.filter ?? cfg.filters` / `cfg.objectName ?? cfg.object` in the flow executors are **debt to pay down, not a pattern to copy**. *Worked example:* an AI-authored `create_record` used `fieldValues` / `today()` / `{{trigger.record.id}}` while the executor reads `fields` / `{TODAY()}` / `{record.id}` → the fix was correcting the authoring skill + a publish-gate lint that rejects the wrong shape (cloud#688), **not** a `cfg.fields ?? cfg.fieldValues` runtime alias (framework#2419, rejected). Strengthens #5.
7777

7878
---

CLAUDE.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# CLAUDE.md
2+
3+
**[AGENTS.md](./AGENTS.md) is the source of truth for working in this repo — read it.**
4+
Its Prime Directives are binding. Do not rely on this file alone; the one rule that must
5+
never be missed is inlined here because missing it corrupts other agents' work.
6+
7+
## ⛔ Worktree-first — before your FIRST file edit (AGENTS.md Prime Directive #11)
8+
9+
This repo — **and every sibling repo you touch (`objectui`, `cloud`)** — is edited by
10+
**multiple agents at once**. The shared primary checkout has its HEAD switched and its
11+
tree reset *under you*, silently clobbering uncommitted work. **A feature branch on the
12+
shared checkout is NOT enough** — it still gets switched under you. You MUST be in a
13+
**dedicated per-task worktree**:
14+
15+
```
16+
git worktree add ../<repo>-<task> -b <branch> main && cd ../<repo>-<task> && pnpm install
17+
```
18+
19+
Then make all edits there. This applies **per repo**: if a task spans `framework` and
20+
`objectui`, create a worktree in *each*. A PreToolUse hook
21+
(`.claude/hooks/guard-main-checkout.sh`) enforces this — it blocks `Edit`/`Write`/
22+
`NotebookEdit` unless the edited file is in a linked worktree, and it checks the edited
23+
file's own repo (so sibling repos are covered). Deliberate non-task exception:
24+
`OS_ALLOW_MAIN_EDITS=1`. Follow the rule because it's correct, not because the hook fires.
25+
26+
See **AGENTS.md** for the full playbook: branch hygiene, the dev stack, PR flow, and the
27+
rest of the Prime Directives.

0 commit comments

Comments
 (0)