Repository navigation
chore(worktrees): fail-fast Docker gate, branch-synced env files, generated artifacts, main-checkout guard hook - #116
Merged
Conversation
…erated artifacts, main-checkout guard hook Port of Zealot-Monorepo#2411 adapted to this repo's Postgres/MinIO slots and bun run scripts. create.sh: Docker/Postgres pre-flight (--skip-db opt-out), main-checkout resolution via git-common-dir, slot ownership by marker or DB-name reference under a per-slot mkdir lock, WORKTREE_SLOT written first, env files synced against the branch's .env.*.example with slot rewrites in apps/api/.env.local too, orphan DBs dropped only when no worktree claims the slot, workspace resolution check, generate:routes + db:generate + generate:sdk, db:push:dev, green ready vs yellow WITH GAPS summary. destroy.sh: shared docker resolution, DROP DATABASE WITH (FORCE), Redis flush on whichever container serves 6379. lib.sh: shared helpers; list.sh uses them. .claude/hooks/bashMainCheckoutGuard.ts (+ tests, .claude/settings.json): PreToolUse on Bash and SessionStart; denies mutating commands from the main checkout on branch main once the session has worked in a worktree. Fixes the eight open review findings on #2411: slot ownership and lock, array-built psql, explicit cd/-C honoured only for real checkouts, global options before the subcommand, no bare-whitespace separator, try/finally HEAD restore, worktree add/remove in the guarded set with docs matching the regex. Docs: worktrees skill, AI/ENTRYPOINT.md, docs/claude/SCRIPTS.md, docs/claude/DOCKER.md. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…egistry, hardened create/destroy Guard hook rewritten as a segment analyser: newline/indent/continuation splitting, runner and shell -c / eval unwrapping, quoted-string and heredoc masking, per-segment directory cursor (cd moves it; git -C / bun --cwd are segment-local), --dry-run and --abort/--continue exclusions, exact db:migrate script name, REQUIRE_MAIN_BRANCH isolated and compared against origin/HEAD, state files pruned after a day. 31 tests. Scripts: slot registry under .worktrees/.slots/<N>, marker-or-reference slot ownership with conflict refusal, fork-onto-existing-branch refusal, whole multi-line quoted values in sync-env plus an env-source gap check, per-pid port kill, merged-branch warning for + prefixed branches, destroy restricted to registered worktrees, WORKTREE_SLOT validation, stale lock reclaim by dead pid or age, destroy skips DB drop when another worktree claims the slot, credential-less DATABASE_URL rewrite verified, PROJECT_NAME fallback aligned with docker-compose. Docs carry the identical deny condition and the new script behaviours. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…, bun aliases, hardened env copy and destroy Hook: bun a/rm/install <pkg>/i <pkg> and every --cwd shape of db:migrate are mutating; every cd / -C / --cwd target goes through realpath; a mutation is lifted only inside a worktree that git worktree list registers or in this main checkout named by its own absolute path (cd and -C alike); symlinks to main, relative paths into main, unregistered or foreign checkouts, and non-checkout directories never lift; glued separators tokenize. Test sandbox is a real git repo. Scripts: WORKTREE_SLOT stripped from copied root env files; main excluded from the claimant scan by realpath; sed replacements escaped and DATABASE_URL rewrite tolerant of & | / in the password; database inventory failure aborts; --skip-db needs neither credentials nor Docker; required env keys die by name; destroy prints merge status and refuses a dirty worktree unless --force. Docs carry the identical deny sentence and the destroy flag. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ks, subshell-aware guard cursor, honest bucket removal Scripts: registered-worktree check no longer greps a multi-line producer under pipefail; GNU/BSD stat branch for lock age; env reads and rewrites share one export/quotes/comment-aware parser and verify themselves (no password through sed); list.sh parses --porcelain; destroy requires this repo's git common dir and reports each bucket via minio-remove.sh over the provisioning endpoint; create registers the slot before the worktree is added, keeps the lock until exit, counts registry entries as claims, prunes only missing+unlocked+old entries, warns on duplicate registry owners, and prints the destroy --force hint on any failure after the worktree exists; sync-env handles escaped quotes, trailing comments, export on both sides, and dies on an unterminated quote; realpath comparisons everywhere; mv-before-rm lock reclaim; left boundary on DB-name claims. Hook: subshell/pipe/background cursor rules; bun option values consumed anywhere; word-0 normalisation and runner options; --git-dir/--work-tree/GIT_DIR/GIT_WORK_TREE redirects; comments stripped; cd options; original segment text in the deny message; here-strings; substitutions in cd targets unknown; arm only for this repo's worktrees; balanced quotes in $( ); depth cap; hashed state files; single stderr report on failure; revert/am guarded. Docs follow every change. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ites, signal-safe create, heredoc-aware guard Scripts: set_env_var re-emits with the quoting it found and verifies by sourcing semantics; destroy prints porcelain through a here-string (no SIGPIPE at 64KB), classifies a directory before printing (registered / half-created under this repo's .git/worktrees / foreign gitdir with a repair hint / independent repo / stray), normalises the typed name on a case-insensitive filesystem, and passes the same STORAGE_* as provisioning to bucket removal; create traps HUP/INT/TERM so the EXIT hook runs, consults the slot lock before the registry (dead pid → reclaim and prune, live pid → skip with the owner), prints one line per skipped slot, validates registry entries are files, treats empty entries as corrupt and relative ones as rewritable, resolves duplicate entries by the worktree's marker, reads slot evidence through env_value, and escapes PROJECT_NAME in the claim pattern; sync-env ensures a trailing newline before appending; file_mtime tolerates BusyBox stat. Hook: scripts fed to a shell (heredoc, piped echo/printf/cat, $(cat <<EOF) in -c or eval) are walked as commands; env -C/--chdir moves the segment directory; word 0 is case-folded on darwin and stripped of backslashes; a # glued to a substitution is not a comment; per-runner value option tables (sudo, caffeinate, env, xargs, timeout, nice); bun option tables switch at the subcommand so add -d is a flag. Docs follow every change. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…cket removal, pass-through pipes in the guard destroy: a .git file whose gitdir under this repo's .git/worktrees/ still exists is a moved worktree — refused with a git worktree repair hint; only a gone gitdir is removed and pruned. minio-remove: mc ls before and after rb, so "removed" means it existed and is gone. Duplicate registry resolution labels the rule that applied (marker or newest); list.sh matches. Hook: piped text is carried through tee and argument-less cat into a shell. Docs: deny sentence updated identically in all three places; scripts/db tree completed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The guard exists to catch an honest agent whose Bash cwd drifted back to the main checkout. Three adversarial rounds turned it into a ~940-line shell parser chasing evasion (piped heredocs, sudo tables, case folding) while it missed the two rules that matter: - git worktree add/remove/move/prune is denied in every session, pointing at bun run worktree:create/destroy. - once a session (or subagent, keyed by agent_id) has worked in a registered worktree of this repository, a mutating command landing in the main checkout is denied on ANY branch; REQUIRE_MAIN_BRANCH is gone, since the main checkout usually sits on a feature branch and that is exactly where a drifted commit does damage. Commands are read by words with quoted strings masked and heredoc bodies dropped; cd / git -C / bun --cwd move the landing, ( ) scopes a cd, an absolute path into main is an explicit choice, a $VAR target is unknown and allowed. State is keyed per session, subagent and main checkout. 939 -> ~290 lines; tests 1002 -> ~320 lines, 20 pass. Docs replace the long deny sentence in ENTRYPOINT, SCRIPTS and the worktrees skill. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
agreenspan
marked this pull request as ready for review
September 24, 2026 03:29
Contributor
Author
|
Added 2b8c98d — env gaps found while running worktrees in Kingdom and Tribe today:
Verified: 🤖 Generated with Claude Code |
This was referenced Oct 12, 2026
agreenspan
added a commit
that referenced
this pull request
Oct 12, 2026
…ans on worker start (#187) The cronJobSync hook passed jobId inside the repeat options, but BullMQ's removeRepeatable overwrites repeat.jobId with its third argument, so the computed key never matched and no old repeatable was ever removed: a pattern edit, rename, disable or delete left the old one firing. - Hook: removeRepeatable(handler, { pattern }, jobId). - registerCronJobs (worker start) removes every repeatable no enabled row registered, matched by the repeatJobKey each add returns; it prunes nothing if any row failed to register. - cronRepeatablesRedis.test.ts runs against real Redis/BullMQ: the old call shape removes nothing, edit/rename/delete move the repeatable, startup prunes orphans. Skips when the test REDIS_URL is unreachable (the default test env uses the in-memory mock). Port of Kingdom #116 (7c3a3737). Co-authored-by: Aron Greenspan <aron.greenspan@inixiative.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Port of userevidence/Zealot-Monorepo#2411 (worktree-provisioning), adapted to this repo: Postgres + MinIO slots,
bun runscripts that actually exist here, and the eight open review findings on the source PR fixed rather than carried over.Why
bun run worktree:createprinted a green ready while silently skipping the slot databases wheneverdockerwas off PATH or Docker was down; installed with--ignore-scriptsand then never built the gitignored artifacts (packages/db/src/generated/*,packages/sdk/src/generated/*,apps/*/app/routeTree.gen.ts,handlers.gen.ts), so every API test file died at boot in a fresh worktree; copiedapps/api/.env.localfrom root without rewriting itsPORT/API_URL, which override root's slot values underwith-env(slot 1 today hasPORT=8000inapps/api/.env.local— its API would bind main's port); and templated env files from root's copies, which lag the branch's.env.*.example(the probe run appended 11 keys root does not have). ThebashMainCheckoutGuardhook closes the other recurring failure: the Bash tool's cwd resets to the main checkout between calls, and a barebun add/git commit/git pushlands on main.What
scripts/worktree/create.shdocker(PATH →~/.docker/bin→Docker.app), requires the daemon, starts a stopped${PROJECT_NAME}_postgres, waits for Postgres to answer. Each failing is a hard error with the fix in the message.--skip-db(any position) for docs-only worktrees: no Docker gate, databases, buckets, or schema push; artifacts are still built.git rev-parse --git-common-dir, so running the script from inside a worktree still lands the new worktree under the main checkout's.worktrees/(and readsPROJECT_NAMEfrom there) instead of nesting.WORKTREE_SLOT=<N>or references${PROJECT_NAME}_wt_<N>/${PROJECT_NAME}_test_wt_<N>in any env file; the chosen slot is held under amkdirlock in.worktrees/.locks/(pid-stamped, stale locks reclaimed) until the script exits.WORKTREE_SLOTis written as the first line before anything else; root andapps/*env files are copied, thenscripts/setup/sync-env.shruns inside the worktree against the branch's own.env.*.example, then slot ports / URLs / DB names / Redis index / buckets are rewritten — includingPORTandAPI_URLinapps/api/.env.localandBETTER_AUTH_BASE_URLin root.worktree:destroy); if one does, the script refuses and names it.psqlis invoked through a bash array, never a re-split string.bun install --force --ignore-scripts, verifiesapps/api/node_modules/@template/dbresolves inside the worktree (bun's isolated linker puts workspace links under the consuming package, not the root).bun run generate:routes,bun run db:generate,bun run generate:sdk; thenbun run db:push:dev.\033[…literally; fixed.)scripts/worktree/destroy.sh— same docker resolution;DROP DATABASE … WITH (FORCE)so open connections don't block it; buckets removed via the shared minio container; Redis flushed on whichever container serveslocalhost:6379; clear warnings when Docker or a container is down.scripts/worktree/lib.sh(new) — shared helpers for all three scripts (main_checkout_root,resolve_docker,slot_resources,worktree_claims_slot,slot_claimant,ensure_env_var,rewrite_database_url,run_step,require_artifact).list.shnow uses it too..claude/hooks/bashMainCheckoutGuard.ts+tests/(new), registered in a new.claude/settings.jsonas PreToolUse on Bash + SessionStart. Two rules: (1)git worktree add/remove/move/pruneis denied in every session, pointing atbun run worktree:*; (2) once a session (or subagent, keyed byagent_id) has worked in a registered worktree of this checkout, a mutating command (git commit/push/merge/rebase/cherry-pick/revert/am,bun add/remove/update,bun install <pkg>,bun run db:migrate,prisma migrate dev) landing in the main checkout is denied on any branch, with thecd <worktree> && …form. Naming main by absolute path is allowed. Deny only, never rewrite: PreToolUse hooks run in parallel and RTK's user-level hook already rewrites viaupdatedInput. See Guard cut back below.Docs:
.claude/skills/worktrees/SKILL.md(rewritten to match the scripts and the hook exactly),AI/ENTRYPOINT.md§0.0 worktree bullet,docs/claude/SCRIPTS.md(worktree section + tree),docs/claude/DOCKER.md(bucket provisioning line).Guard cut back (supersedes the guard notes in the adversarial rounds below)
The adversarial rounds grew the guard into a ~940-line shell parser defending against evasion (scripts piped into shells,
sudo/caffeinateoption tables, case folding, symlink tricks). Its job is narrower: catch an honest agent whose Bash cwd drifted back to the main checkout, and keep worktree lifecycle on the scripts. It is now ~290 lines (tests ~320, 20 pass):REQUIRE_MAIN_BRANCHis gone — the main checkout usually sits on a feature branch, which is exactly where a drifted commit does damage.git worktree add/remove/move/pruneis denied in every session, not only after drift.git worktree addis not a command);cd/git -C/bun --cwdmove the landing,( )scopes acd, an absolute path into main (and moves relative to it) is explicit, a$VARtarget is unknown and allowed.agent_id— subagent hooks carry the parent'ssession_id) and main checkout, so parallel subagents are never pointed at each other's worktree and another repository's worktree never replaces this one's.AI/ENTRYPOINT.md,docs/claude/SCRIPTS.mdand the worktrees skill is replaced by a short description of the two rules.Verified after merging main: hook tests 20/20,
tsc --strictclean, live probes against the real checkout (main checkout on a feature branch: baregit commitdenied,cd <worktree> && git commit/git statusallowed,git worktree adddenied).bun run check: lint, typecheck, permissions 81, db 327, email 519, api 1254 green; the UI suite has the same 3 failures as main (signup verification, logout revocation, live-query subscription — also reported on #114; this PR touches no UI code). Note: on main,generate:sdknow needsAUTH_PROVIDER_SECRETS_ENCRYPTION_*in the local env, which.env.local.exampledoes not carry.Review findings from #2411 addressed
WORKTREE_SLOTis written before any destructive step; per-slotmkdirlock around allocation; orphan databases are dropped only whenslot_claimantfinds no worktree on that slot, otherwise the script dies naming the claimant..env.test.example— this repo'sscripts/setup/sync-env.shtreats a missing example as a no-op ([ ! -f "$example_file" ] && return 0), so thedieonly fires on a real sync error; a worktree that still ends up without.env.testgets a warning, not an abort.PSQL=("$DOCKER" exec "$PG_CONTAINER" psql -U postgres -v ON_ERROR_STOP=1 -qtA)expanded as"${PSQL[@]}"everywhere in create and destroy.cd /nonexistent; git pushbypass — a leadingcd/pushdis honoured only when its target exists and is inside a git checkout: a linked worktree by any path, the main checkout only by an absolute path (cd . && …is not a choice of main). Otherwise the guard applies and the deny message says which target failed. Tests: missing dir,||form, non-checkout dir,$VARtarget, relative main.GLOBAL_OPTIONSbetweengit/bunand the subcommand (git -C . commit,git --no-pager push,git -c k=v rebase,bun --cwd . add) and runners in front (bunx,bun run [--opts],npx,env K=V,sudo,time).git -C <path>is honoured like an explicitcdunder the same existence + checkout check. Tests for each.\sprefix firing on mentions — separator is^,;,&,|,(only (runners are enumerated instead);gh pr comment --body "remember to git push", anecho, and heredoc lines are asserted unmatched;rtk git pushis likewise outside (RTK sees the original command)..git/HEADrewrite without restore —onMain(head, run)helper wraps the assertion intry/finally.git worktree add/remove/move/pruneis now in the regex (this repo's rule is scripts-only lifecycle) and the skill/entrypoint list the guarded set verbatim, including that the denial fires only under the drift condition.Not ported, and why
packages/monitoringbuild step,generate:sdkviawith:local api, MySQL grants/collations,LEGACY_API_PORT/NEXT_PUBLIC_*URLs,db:clonehint — Zealot-only; template's equivalents aregenerate:routes/db:generate/generate:sdk/db:push:devand the MinIO bucket step..gloss/sidecar — template has no gloss setup (no@inixiative/glossdependency, no.gloss/); the hook's prose lives in.claude/skills/worktrees/SKILL.mdand the code carries no comments.docs/AI.mdcommand-list change — template's equivalents areAI/ENTRYPOINT.md§0.0 anddocs/claude/SCRIPTS.md.apps/*/vite.config.tshardcodeserver.port3000/3001/3002, so the slot's Web/Admin/Superadmin ports are advertised but not honoured by the dev servers. Pre-existing and app-side; out of this PR's scope, flagged here.Verified
Ran the new scripts from this worktree against the main checkout (they resolve the main checkout themselves):
main→tmp-provisioning-probe: Docker resolved at~/.docker/bin/docker, slot 3 claimed under lock, sync appendedVITE_OTEL_*to.env.localand 9 keys toapps/api/.env.local(root lags),WORKTREE_SLOT=3first line of both env files,apps/api/.env.localPORT=8300/API_URL/DATABASE_URL=…/template_wt_3/REDIS_URL=…/3,.env.testDATABASE_URL=…/template_test_wt_3+API_URL=…:8300, buckets ensured, workspace resolution check passed, all 8 required artifacts present, schema pushed, DB ping ok, green ready, probe worktree git-clean, lock released.Redis DB 3 flushed (template_redis), slot freed.template_test_wt_3with a table, re-created on slot 3:Slot 3 still has database template_test_wt_3 … dropping it, table gone afterwards, green ready; destroyed.--skip-dbcreate: no Docker gate, no slot DBs created (verified inpg_database), artifacts still built, green ready; destroyed. Final state: no*_wt_3databases, no probe worktree, probe branch deleted,.worktrees/.locks/empty.worktree_claims_slot/slot_claimantexercised directly: a marker-less env file referencingtemplate_wt_7claims slot 7, does not claim 1 or 17; slot 1's claimant resolves to the existingFEAT-003-feature-flagsworktree.bun run worktree:listfrom the worktree lists all worktrees with the main checkout as slot 0 (was aborting on the main checkout's missing marker underset -e; fixed).__TEST_REDIRECT__=1 bun test ./.claude/hooks/tests/bashMainCheckoutGuard.test.ts: 16 pass, 0 fail (103 expects).tsc --noEmit --strictclean on hook + test; biomecheck --writeproduces identical output for both (biome's own config excludes.claude/).git pushfrom the main checkout is silent because main is currently onINFRA-031, notmain— the branch condition working as designed (the sandboxed binary test covers the deny path).bun run check: exit 0 in this worktree (slot 2) — biome clean (54 pre-existing warnings), all 10 workspaces typecheck, tests: permissions 81, db 327, email 504, api 1203, shared ok, ui 255, frontend suite green, all 17 CI rules passed.Not exercised live: the Docker-daemon-down and stopped-
template_postgresbranches (would disrupt the other worktrees on the shared stack), a concurrent create racing for the same slot (lock logic exercised only sequentially), and the claimant-refusaldiepath (only reachable through that race).Adversarial round one
Fourteen confirmed findings plus the addendum, each with a fix and a probe or test. Commit
e097f0d0.Guard hook — rewritten from a single regex into a segment analyser (
segmentCommand→ strip runners →mutationInper segment, with a directory cursor). Every item below has a named test in.claude/hooks/tests/bashMainCheckoutGuard.test.ts(31 tests, 203 expects, all pass).\-newline, then split into segments on\n ; && || | & ( ) { }; each segment is trimmed and matched from its start. Tests:echo hi\ngit push,git push,\tgit push,# comment\ngit push,cd /tmp\ngit push,git \\\n push, CRLF, blank lines.VAR=value(any number),env [-i] [VAR=..],command,exec,nohup,sudo,timeout <n>,nice [-n N],time,xargs,rtk,rtk proxy,bunx/bun x/npxare stripped before matching;sh|bash|zsh|dash [-flags] -c "…"(including combined-lc) andeval "…"are analysed inside the quoted argument with the same cursor;{ … },if/then/else/elif/fi,while/until/do/done,!are separators/keywords. Every string in the finding is a test case.cd/pushdmove a directory cursor for the segments after them (relative targets resolve against the cursor;popd,cd -,$VAR, a missing path or a non-checkout set it to unknown);git -C <dir>/bun --cwd <dir>apply only to their own segment. A mutation is denied when its effective directory is the main checkout or unknown. Tests:git -C <wt> status && git push(deny),cd <wt> && cd .. && git push(deny),cd <wt>; cd <main>; git push(deny),cd . && git push(deny),cd <wt> && git push/git -C <wt> push && git status/bash -c 'cd <wt> && git push'(allow).<<EOF,<<'EOF',<<-EOF) skipped before segmentation;--dry-runanywhere in the segment,merge|rebase|cherry-pick --abort|--continue|--quit|--skip,git worktree list, andgit worktree prune --dry-runare not mutations. Tests: all six strings from the finding plusgit log --grep="git push",cat <<EOF … EOFfollowed by a realgit push(still denied).REQUIRE_MAIN_BRANCH— the branch clause is one exported constant (true) consulted throughguard(input, state, { requireMainBranch }); tests assert the constant, the inert-on-feature-branch behaviour with it on, and the deny with it off. The compared branch is nowdefaultBranch(root)read from.git/refs/remotes/origin/HEAD— the same sourcecreate.shuses (default_branchinlib.sh) — with a test flippingorigin/HEADtodevelop. Behaviour otherwise unchanged, as instructed.db:migrate\b— replaced by an exact script-name set (bun run db:migrateonly);db:migrate:anything,db:migrate-status,db:push:dev,db:deployare asserted non-mutating.SessionState.prune(maxAge)removes per-session files older than a day onSessionStart; tested at the API level and through the binary.Scripts —
bash -nclean oncreate.sh,destroy.sh,lib.sh,list.sh,sync-env.sh.<new-branch>already existing (local ororigin/), create dies with "already exists … attach it withworktree:create <branch>or pick a new name". Probe:create.sh main tmp-existing-probe→ exit 1, no worktree created. Attach mode unchanged.sync-env.shcopies the whole entry (a"that opens on the key line closes on a later line) via an awk block, and its key scan skips the continuation lines. Probe on a copy of.env.test.example: 20/20 keys, all four PEM markers,WEBHOOK_SIGNING_PRIVATE_KEYsources as 28 lines; the previous script's output fails to source (PUBLIC: command not found).create.shsources every generated env file in a subshell (env_file_sources) and records a failure as a gap.kill_port_listenersloops over each pid, pollskill -0up to 1s afterkill -9, printsKilled PID <pid>per process and a warning naming any survivor. Probe with a stublsofreturning two detachedsleeppids: both reported killed, both gone.sed 's/^[*+ ]*//'. Probe:tmp-merged-probe(==main) attached in a worktree shows as+ tmp-merged-probeingit branch --merged; the next create printsSlot 4: tmp-merged-probe (tmp-merged-probe) — bun run worktree:destroy tmp-merged-probe.destroy.shrm -rfon any name — names starting with.are rejected; the target must be a registered worktree, or (half-created fallback) contain a.gitfile. Probe:destroy.sh .locks/.slots→ exit 1;destroy.sh not-a-worktree→ exit 1, directory survives,.locksintact.WORKTREE_SLOT—env_valuestrips CR, surrounding quotes, and a leadingexport;valid_slot(^[1-9]$) is enforced in destroy (die) and in the create scan. Probe:WORKTREE_SLOT="5"\randexport WORKTREE_SLOT=5both parse to5;WORKTREE_SLOT=1; DROP DATABASE x→ destroy exits 1 with the offending value quoted, worktree untouched.claim_slot_lockreclaims a lock whose pid is dead, or that has no pid file and is older than 60s (mtime);mkdiris immediately followed by the pid write. Probe: no-pid lock aged to 2025 → reclaimed with notice; fresh no-pid lock → not reclaimed; dead pid → reclaimed; live pid → claim fails.slot_claimantexcluding the worktree being destroyed; another registered claimant → DB/bucket/Redis cleanup skipped with a message naming it, worktree still removed. Probe: squatter with every reference on the owner's slot 3 →Warning: tmp-provisioning-probe also references slot 3 — leaving template_wt_3 / template_test_wt_3, the buckets, and Redis DB 3 in place, both DBs still present, owner's registry entry intact; owner destroy then drops them.DATABASE_URL—rewrite_database_urlhandlesscheme://host/db(nouser:pw@) and returns non-zero when the URL was not rewritten; create dies on that. Probe:postgresql://localhost:5432/template→…/template_wt_9;…?schema=publicpreserved;DATABASE_URL=garbagerejected.PROJECT_NAMEfallback —project_namefalls back to the literaltemplate, matchingdocker-compose.yml, in all three scripts.Addendum
.worktrees/.slots/<N>holds the owning worktree path, written right aftergit worktree addand removed by destroy.worktree_claims_slotconsults registry → marker → DB-name references;prune_slot_registryremoves an entry whose worktree is no longer registered (with a notice) at the start of every create.worktree_slots <wt>lists every slot a worktree's registry entry, markers, and env files name. Create reserves all of them and warns; destroy refuses (references more than one slot (marker WORKTREE_SLOT='5', env files/registry name slots: 5 8)) and asks for the files to be fixed. Probe: appending atemplate_wt_8URL to the squatter'sapps/api/.env.local→ destroy exit 1, worktree untouched; reverting →worktree_slotsback to one.Docs (14) —
SKILL.md,docs/claude/SCRIPTS.md, andAI/ENTRYPOINT.mdnow carry the same deny sentence verbatim (including the branch clause), the marker-first-line guarantee (prepend_slot_markerruns after the sync, so it holds even when root has no.env.test), the merged-branch warning, stale-lock reclaim, the registry, conflict refusal, shared-slot skip, and per-pid port-kill reporting.Verification for this round
__TEST_REDIRECT__=1 bun test ./.claude/hooks/tests/bashMainCheckoutGuard.test.ts: 31 pass, 0 fail, 203 expects.tsc --noEmit --strictclean; biome on the two files: 2 warnings (the repo-widenoUndeclaredEnvVarsturbo warning), 0 errors.dockerruns for daemon down (dies before creating anything) and stopped container (template_postgres is stopped — starting it…, then the DB ping gap producedcreated WITH GAPS).bun run check: exit 0 (biome 54 pre-existing warnings / 0 errors, 10 workspaces typecheck, permissions 81 / db 327 / email 504 / api 1203 / ui 255 pass, frontend suite green, 17 CI rules passed).template_*wt_[3-9]databases,.worktrees/.slotsand.worktrees/.locksempty, notmp-*branches, worktree list back to main + slot 1 + slot 2.Still not exercised live: two creates racing for one slot (lock logic exercised sequentially), and the claimant-refusal
dieinside create (only reachable through that race, since the scan already reserves a claimed slot).Round one, addendum C and D (commit
bc0bc7b9)C — hook (
__TEST_REDIRECT__=1 bun test ./.claude/hooks/tests/bashMainCheckoutGuard.test.ts: 40 pass, 0 fail, 275 expects;tsc --strictclean; both files formatted explicitly withbiome check --writeover stdin (the repo config ignores.claude/)). The test sandbox is now a realgit initrepo with agit worktree add-ed worktree, so the registry lookup is exercised for real, including through the hook binary.bun a x,bun rm x,bun install x,bun i x,bun i -d x,bun install --force xare mutating; barebun install,bun i,bun install --force,bun i --frozen-lockfileare not.--cwdshape —bun --cwd <pkg> db:migrate,bun run --cwd <pkg> db:migrate,bun --cwd <pkg> run db:migrate, and theprisma migrate devequivalents;db:push:dev,db:generate,db:deploy,prisma generatein the same shapes are not.db:push*stays outside the set on purpose: it writes no tracked files, so it cannot land on main.realpathSyncon everycd/-C/--cwdtarget — landings carry both the realpath (directory) and the token as typed (named). Tests: a symlink to a worktree resolves to the worktree (lifts), a symlink to main resolves to main but is denied with…through <link>, which is not its own absolute path.git worktree move— inGIT_WORKTREE_MUTATING, asserted.cd <main abs>andgit -C <main abs>identical — both lift (alsobun --cwd <main abs>, quoted forms, andcd <main abs>/apps/api);cd <main abs> && cd apps/api && git pushis denied because the lastcdwas relative.cd <wt>;git push,cd <wt>&&git push,cd <wt>||git push,cd <wt>|git pushall resolve the cd token correctly (tests inmutationsInand throughguard).registeredWorktrees(mainRoot)readsgit worktree list --porcelain, realpaths each entry, drops main. A mutation is lifted only when its realpath is inside a registered worktree, or it is this main checkout named by its own absolute path. Denied with a specific reason: a non-checkout (/tmp,not a git checkout), an unregistered copy of a worktree or a different repository (neither this repository's main checkout nor one of its registered worktrees), a relative path into main (reached by a relative path), a symlink to main, an unresolvable target. Interpretation flagged: a different repository named by absolute path is denied too, per the literal rule — flipdenialForif cross-repo commits from a template session should pass.D — scripts (
bash -nclean on all five; probes below ran against the real stack on slot 3 plus stubbeddocker).WORKTREE_SLOT—copy_env_without_markerdropsWORKTREE_SLOT=/export WORKTREE_SLOT=lines from every copied root env file; probe:WORKTREE_SLOT=1 … export WORKTREE_SLOT=2in →A=1 B=2out; the real create shows exactly one marker line. Main excluded from the claimant scan —linked_worktreesrealpath-compares eachworktree listentry againstROOT_DIRand drops it;is_registered_worktree/slot_claimantcompare realpaths. Probe:linked_worktreeslists only the two linked worktrees,slot_claimant 0is empty,is_registered_worktree <main>is no.sed_replacementescapes\ & |for every value that reaches ased s|||(URLs, buckets, ports);rewrite_database_urlnow uses#as delimiter, accepts any userinfo up to the last@(so/ & |in a password survive), and still verifies the rewrite. Probe withpostgresql://user:p&ss|w/rd@localhost:5432/template→…p&ss|w/rd@localhost:5432/template_wt_9;ensure_env_var FOO 'a&b|c\\d/e'lands verbatim. (Sourcing that probe file then fails — an unquoted&is not valid sh in any env file; the real env files quote such values, which is what the sourcing gap check in create is for.)DB_EXISTS="$(psql …)" || die "could not list databases … not assuming <db> is absent". Probe with a stubdockerwhosepg_databasequery fails: create exits 1 with that message, no database created; the half-created worktree is removed bydestroy --force(destroy's half-created path).--skip-dbneeds neither credentials nor Docker — probe: root.env.testandapps/api/.env.localtemporarily stripped ofDATABASE_URL/REDIS_URL(restored undertrap,cmpbyte-identical),PATHwithout any docker directory,HOME=/nonexistent→ green ready on slot 3, the branch's.env.*.examplesupplied the URLs and the slot rewrite applied.require_env_keyruns only when databases are provisioned.dieper required value —require_env_key DATABASE_URL <files…>names the key and every file searched (probe output in the log),WORKTREE_SLOTvalidated (round one),resolve_dockertolerates an unsetHOME(env -u HOMEprobe).destroy.sh <name> [--force]; printsbranch 'X' is merged into main/is NOT merged into main (N commit(s) not in main)/detached HEAD, then refuses ongit status --porcelain --untracked-files=alloutput unless--force(paths listed either way). Probes: untracked file → exit 1, worktree kept;--force→ destroyed with the listed path; a worktree with one extra commit →NOT merged … (1 commit(s) not in main).Docs — the deny sentence (now including the alias/install forms, the
--cwdshapes, the realpath rule, and the exact lift rule) is identical inSKILL.md,docs/claude/SCRIPTS.md, andAI/ENTRYPOINT.md;destroy … [--force]and the merge-status print are documented in all three; SKILL.md also covers glued separators, symlinks, the inventory abort, the marker strip, and the--skip-dbcredential independence.Verification for this addendum — hook tests 40/40;
bash -noncreate.sh,destroy.sh,lib.sh,list.sh,sync-env.sh; probes above plus a full real create/destroy regression on slot 3 (green ready, one marker line, all four env files source, git-clean, DBs dropped, Redis flushed);bun run checkexit 0 (biome 54 pre-existing warnings / 0 errors, 10 workspaces typecheck, permissions 81 / db 327 / email 504 / api 1203 / ui 255, frontend green, 17 CI rules). Final state: notemplate_*wt_[3-9]databases,.worktrees/.slotsand.locksempty, notmp-*branches, worktree list back to main + slot 1 + slot 2, root env files unchanged.Adversarial round two
Eleven confirmed findings plus seven shared-design findings from the Zealot review, on commit
c7d1bad8. Hook tests 61 pass / 0 fail / 397 expects in a sandbox that is now a realgit initrepo withgit worktree add-ed worktrees (own and foreign);tsc --strictclean; both files formatted explicitly withbiome check --writeover stdin (the repo config ignores.claude/).bash -nclean oncreate.sh,destroy.sh,lib.sh,list.sh,sync-env.sh,minio-remove.sh. Script probes ran from a script file against a sandbox repo whose path contains a space, with stubbeddocker/lsof/bun/statonPATH, then against the real stack.Scripts
is_registered_worktreeSIGPIPE under pipefail —linked_worktrees | grep -qreplaced by[ -n "$(linked_worktrees | grep -xF -- "$dir")" ]; every other multi-line-producer grep moved to a here-string or[[ == *" $i "* ]];container_on_portno longer pipes throughhead. Probe: three worktrees, the MIDDLE one dirty →wt-b registered: yes,Error: wt-b has uncommitted or untracked changes … ?? DIRTY.txt, exit 1, directory and dirty file survive, registry still1 2 3; the following create scan lists all three worktrees and prunes nothing. Live: two probes on slots 3 and 4, slot 3 (non-last) destroyed first — DBs dropped, four buckets removed, Redis 3 flushed.stat—file_mtimebranches onstat --version(-c %Y/-f %m) likesedi. Probe with a GNU-likestatstub whose-fprints junk and exits 1: an old pid-less lock is stale, a fresh one is not, no arithmetic error.export/quotes/comments and verify — one canonicalenv_key_pattern(^(export[[:space:]]+)?KEY[[:space:]]*=) behindenv_line/env_has_key/env_value/set_env_var/copy_env_without_marker/prepend_slot_marker/require_env_key;rewrite_database_url/rewrite_redis_dbparse the value with bash regexes (no password ever passes throughsed), rewrite viaset_env_var(keeps anexportprefix, escapes the replacement), and die naming file/key/cause on empty or malformed input. Probe:export DATABASE_URL="postgresql://u:p&ss|w/rd@localhost:5432/template"→export DATABASE_URL=postgresql://u:p&ss|w/rd@localhost:5432/template_wt_9;REDIS_URL="redis://localhost:6379"→…/9;PORT=8000 # api→PORT=8900;DATABASE_URL="garbage"→… ('garbage') is not scheme://[user:password@]host[:port]/database; empty →… is empty;redis://noop:6379left alone. The first probe run caught a real regression of mine here — BSDsedrejectst;followed by another command, so every double-quoted value read back empty; the parser is now pure bash.list.shon a path with a space — parses--porcelainblocks. Probe on…/base sp/repo:0 (main) main 3000 3001 3002 8000plus the three worktree rows.scripts/db/minio-remove.shmirrorsminio-provision.sh(sameSTORAGE_*env,host.docker.internalmapping, readiness check) and reports each bucket honestly:was already absentwhen the pre-checkmc lsfinds nothing,removedonly when it existed and the post-check shows it gone,could NOT remove … (still present)otherwise; destroy printsWarning: bucket removal did not complete.on any failure. Probe with a stub failing onerb:removed template-system-wt-2,could NOT remove template-user-wt-2 (still present), warning, 6mcinvocations overhost.docker.internal.on_exittrap: any non-zero exit aftergit worktree addprintsThe worktree '<name>' was created before this failure. Remove it and free slot N with: bun run worktree:destroy <name> --force; a failure before it unregisters the slot. Probe: stubbunfails install → hint printed;destroy wt-d --forcecleans up.#after the closing quote is a comment,export KEY=matches on both sides, an unterminated quote is a hard error naming the key and line. Probe:A="x \" y"/B="v" # say "hi/export C=3/ multi-lineD/E=afteragainst an env file holdingexport A=0→ only B C D E added,Acount 1 withexportkept, file sources (D=[multi\nline]);F="never closes→sync-env: unterminated quote in .env.local.example — the value of F opened at line 1 never closes, exit 1.git worktree add) and the lock is held until the script exits; registry entries count as claims whether or not their directory exists;prune_slot_registryremoves an entry only when its directory is missing AND no lock exists for the slot AND the entry is older than 120s;unregister_slotonly when the entry's realpath equals the worktree's. Probe: a fresh entry for a missing dir reserved slot 4 (Claimed slot 5); aged to 2020 it was pruned with a notice and slot 4 reclaimed;unregister_slot 1 <other path>kept entry 1.ROOT_DIRispwd -P;linked_worktrees,is_registered_worktree,slot_claimant,unregister_slot, andlist.sh's main detection compare realpaths. Probe: scripts invoked throughlink-to-reporeport the realpath root and the(main)row..worktrees/—worktree_shares_main_gitrequiresgit rev-parse --git-common-dirto be this repo's.git. Probe: a nestedgit initis refused (no.gitfile), a.gitfile pointing at another repo is refused withdoes not belong to this repository.registry_duplicates(owner path may contain spaces);list.shwarnsSlot registry conflict: <path> is registered under slots 1 7; create warns and reserves all of them.env_value—K='v',K="v",K=v # c,K = v,K=probed:[single] [v] [v] [spaced] [](empty is missing),E="esc \" aped"→esc " aped.mv "$lock" "$lock.stale.$$"beforerm -rf(probe: two reclaimers, only A claims);worktree_claims_slothas a left boundary (mytemplate_wt_5 does not claim 5,template_wt_5 claims 5).Hook
open/closepieces for( )and records each command's terminator; the cursor is pushed on(and popped on), and acdending in|,|&, or a single&does not move it;{ }stays in-shell. Tests:(cd <wt>) && git push,(cd <wt> && bun run check) && git commit,cd <wt> | cat; git push,cd <wt> & git push→ main (denied);{ cd <wt>; } && git push,(cd <wt> && git push),(cd <wt>; (cd ..); git push)→ worktree (allowed);2>&1is not a separator.parseBunconsumes value options (--cwd --filter -F --backend --registry --config -c --target --env-file --port --shell --linker --omit …) wherever they appear;--cwdanywhere in the segment is the segment's directory. Tests:bun install --cwd <wt>,bun install --filter web,bun install --backend hardlinknot mutating;bun add x --cwd <wt>,bun add x --cwd=<wt>/apps/api,bun run db:migrate --cwd <wt>/packages/dbresolve to the worktree; docs now say "anywhere in the segment".headOfstrips a leading\, quotes ("git",gi""t,$'git'), and a path prefix (/usr/bin/git,./node_modules/.bin/bun); runners take options (command -p,exec -a x,env -u FOO,time -p,sudo -u me,nohup --,xargs -n 1/-I {},timeout -k 5 10,nice -n 5);bash -o pipefail -cand-eo pipefail -cclusters parse;$( )and backticks are analysed as substitutions (echo "$(git push)",echo `git push`,x=$(git push)).--git-dir/--work-tree/GIT_DIR/GIT_WORK_TREEredirect the landing (a.gitdir resolves to its checkout):cd <wt> && git --git-dir=<main>/.git push→targets <main> through --git-dir,GIT_DIR=<main>/.git git pushandGIT_WORK_TREE=<main> git commitdenied,git --git-dir=<wt>/.git pushallowed.command -v gitis not a run.#at a word boundary skips to end of line (echo hi # ; git pushnot mutating;git push # donestill is;$#untouched);cd -P <wt>/cd -L -- <wt>take the path, not the flag. Landings carry the original segment text with keywords stripped and runners kept (FOO="a b" git push, not the masked bytes); the remediation iscd <wt> && <that segment>per denied landing, never prefixed onto a segment that alreadycds.cat <<< "x"\ngit pushdenied,cat <<< "git push"not. Z4 acdtarget containing$( ), backticks, or$VARsets the cursor to unknown → denied (cd <wt> && cd $(pwd) && git push). Z5 the guard arms only when the remembered worktree is a registered worktree of THIS main: a foreign repo's worktree or an unregistered copy never arms. Z7 backslash-newline inside a word is removed (git pu\<nl>sh);scanParen/scanDoublebalance quotes inside$( )(echo "$(echo "a"; git push)",echo "$(echo ")")"; git push). Z8 nesting deeper than 6 is reported as a landing and denied (nested more deeply than this hook analyses); state files are named bysha256(session_id); state-file errors and any top-level throw are reported once on stderr (bashMainCheckoutGuard: …) with exit 0 — test spawns the binary with a file as the state dir and asserts exactly one report. Plausiblegit revertandgit amadded (they create commits;--abort/--continue/--skipexcluded);git pull,git reset --hard,git checkoutremain outside by design and are documented as such.Docs — the deny sentence in
SKILL.md,docs/claude/SCRIPTS.md,AI/ENTRYPOINT.mdnow carries the revert/am additions, the subshell/pipe/background rule,--cwdanywhere in the segment, the git-dir/work-tree redirects, the word-0 and wrapper list, and the by-design exclusions; SKILL.md documents the registry/lock semantics (claim before dir, 120s prune, mv-before-rm reclaim, GNU/BSDstat), the common-dir requirement on destroy, per-bucket reporting, the env-parsing rules, and the post-worktree addcleanup hint.Verification — hook tests 61/61;
bash -nall six scripts; sandbox probes above; live: two probes on slots 3/4, slot 3 destroyed while not last (DBs dropped, 4 buckets removed, Redis flushed), slot 4 destroyed,worktree:listback to main + slot 1 + slot 2;bun run checkexit 0 on the final tree (biome 54 pre-existing warnings / 0 errors, 10 workspaces typecheck, permissions 81 / db 327 / email 504 / api 1203 / ui 255, frontend green, 17 CI rules). Left behind: nothing — notemplate_*wt_[3-9]databases,.slots/.locksempty, notmp-*branches, root env files untouched. Not exercised live: two creates racing for one slot; a real Linuxstat(covered by the stub only).Adversarial round three
Fourteen confirmed findings plus the Zealot round-three mirrors, on commit
e997588f. Hook tests 72 pass / 0 fail / 470 expects;tsc --strictclean; both files formatted explicitly withbiome check --writeover stdin (the repo config ignores.claude/).bash -nclean on all six scripts;grepfinds no remainingecho "$var" | headpipe. Script probes ran from a script file against a sandboxgit initrepo withgit worktree add-ed worktrees and stubbeddocker/lsof/bun/stat, then a live create/destroy on slot 3.Scripts
set_env_varre-emits with the stylequote_style_of_lineobserved: single stays single (unless the value itself contains', then double), double stays double with\"$`escaped, unquoted stays unquoted unless the value needs quoting;env_valueunescapes\$and\`too, and the write is read back and compared. Probe: for each of$ \ & | ; space ' ?a=b&c=d× {none, single, double} the rewritten file wassourced and the value compared byte-for-byte — 24/24 ok (KEY='postgresql://u:p$ss@…'stays single-quoted; unquotedp ssgains double quotes);rewrite_database_urlonDATABASE_URL='…p$ss@…/template?schema=public&sslmode=disable'→…/template_wt_7?schema=public&sslmode=disablesingle-quoted, sourced value intact.print_first_linesissed -n '1,10{s/^/ /;p;}' <<< "$DIRTY". Probe: 1500 untracked files (75,393 bytes of porcelain),destroy big --force→ prints the first lines,Worktree 'big' destroyed, exit 0..gitFILE whosegitdir:resolves under$ROOT_DIR/.git/worktrees/→ half-created (removed + pruned);.gitfile pointing at another checkout → refused withrun 'git worktree repair' from it. Nothing was deleted.; own.gitdirectory → refused as independent repository; no.git→ refused as stray. Probe:halfremoved (exit 0),movedrefused with hint and its uncommitted file untouched,strayrefused.storage_env_from "$ROOT_DIR/.env.local"feeds the sameSTORAGE_*tominio-provision.shin create andminio-remove.shin destroy. Probe with rootSTORAGE_ENDPOINT=http://minio.example:9100and probe keys: createMC_HOST_local=http://probe-key:probe-secret@minio.example:9100, destroy identical.trap 'exit 129' HUP; 'exit 130' INT; 'exit 143' TERMso the EXIT trap sees the failure: aftergit worktree addit prints the destroy hint; before it, it unregisters the slot and removes any partial directory. The allocation loop consults the lock FIRST: dead pid →slot N: stale lock (pid P dead) — reclaimedand a directory-less registry entry is removed with a notice; live pid →slot N: being provisioned by <path> (pid P) — skipping; every other skipped slot prints one line with the reason and theworktree:destroyto free it.listprintsstale lock (pid N dead) — next create reclaimsonly for a dead pid andcreate in progress (pid N)for a live one. Probes: SIGTERM during a hanging install →created before this failure (exit 143) … bun run worktree:destroy wt-int --force; planted dead-pid lock +deadentry and a live-pid lock → list and create output exactly as above, slot 1 reclaimed, slot 2 skipped.sync-envwrites a newline first when the target lacks one:E=1+F=2→E=1|F=2|.file_mtimetriesstat -c %Y, thenstat -f %m, accepting only a numeric answer, else the current time with a warning;file_ageclamps at 0. Probe with a stub whose-cfails and-fprints a mount point: warning,age=[0], no arithmetic abort.regex_escape "$PROJECT_NAME"in the claim pattern:my.appdoes not matchmyXapp_wt_3, does matchmy.app_wt_3.validate_slot_registrydies on a non-regular entry with the repair hint before any lock is taken (probe:.slots/5directory → exit 1,.locksempty); every prune loop guards[ -f ] || continuebeforefile_age.empty (corrupt), create removes with notice; relative entry: list warns, create rewrites absolute; the repair hint says ABSOLUTE; slot evidence goes throughenv_value(WORKTREE_SLOT="4" # noteclaims 4, a commented-outDATABASE_URLdoes not); each skipped slot prints its reason; case-insensitive FS:destroy WT-C→Note: 'WT-C' is spelled 'wt-c' on disk — using that name, registry entry removed; duplicate entries resolved by the marker slot (keeping 3 (its WORKTREE_SLOT marker), removing 7), newest as tiebreak.Hook
sh|bash|zsh|dashwith no-cand no positional script walks its own heredoc, or the literal output (cat <<EOFbody,echo/printfargs) of the piece piped into it; a-corevalargument that is$(…)/backticks is replaced by that command's literal output. Deny tests:bash <<'EOF',bash -s <<'EOF',echo 'git push' | bash,cat <<'EOF' | sh,bash -c "$(cat <<'EOF' … )",eval "$(printf 'git push')",printf 'git push' | bash,sh <<EOF bun add x,printf '%s\n' 'git push' | bash,echo "git push" | bash -s. Allow tests:cat <<EOF > notes.md,echo git push | grep, heredoc with onlygit status,bash f.sh,. f.sh, a heredoc followed by an unrelatedbash. A heredoc thatcds into the worktree first lands there and passes from main.env -C/--chdir— moves the segment's directory (worktree → allowed); it does not carry the absolute-path lift for main, soenv -C /abs/main git pushandenv -C . git pushare denied (reached without cd /abs/main).GIT push,Git commit,bUn add xdeny on macOS (asserted againstprocess.platform),gi\t push/g\it pushdeny everywhere.#glued to${x},$( ), or backticks is not a comment (echo ${x}#; git pushdenied;echo x #; git pushstill clean).sudo -u -g -p -D -C -h -r -t -T -U,caffeinate -t -w,env -C --chdir -u -S,xargs -n -L -P -I -d -a -E -s,timeout -k -s,nice -n;nohup/command/rtknone) with a test per string (sudo -E|-n|-S|-k|-s git push,caffeinate -t 600 git push, …). Bun options are table-switched at the subcommand: global-d/--definetakes a value, but afteradd|install|i|a|remove|rm|update-d/-Dare flags —bun add -d --cwd <wt> pkglifts,bun -d FOO=1 add xis still mutating.Docs — the deny sentence in
SKILL.md,docs/claude/SCRIPTS.md,AI/ENTRYPOINT.mdcarries the heredoc rule,env -C, the word-0 case rule, the per-runner wrapper list; SKILL.md §5 describes the preserved quoting, the destroy row lists the.git-file fallback and the repair hint, the bucket sentence says "the sameSTORAGE_*values from the root.env.local", §11 covers signals and the kill -9 recovery, the registry paragraph covers empty/relative/directory entries and the ABSOLUTE hint, the quoted-evidence sentence, and duplicate resolution.Verification — hook tests 72/72;
bash -nall six scripts; sandbox probes above; live:create main tmp-provisioning-probeon slot 3 printed the in-use lines for slots 1 and 2,Claimed slot 3, green ready,WORKTREE_SLOT=3first in both env files, bothDATABASE_URLs on_wt_3, all four env files source, git-clean, both DBs present; destroy →merged into main, DBs dropped, four buckets removed,Redis DB 3 flushed,Slot 3 freed;worktree:listback to main + slot 1 + slot 2.bun run checkexit 0 on the final tree (biome 54 pre-existing warnings / 0 errors, 10 workspaces typecheck, permissions 81 / db 327 / email 504 / api 1203 / ui 255, frontend green, 17 CI rules). Final state: notemplate_*wt_[3-9]databases,.slots/.locksempty, notmp-*branches, main checkout untouched. Not exercised: a real Linux/BusyBoxstat(stubs only), two creates racing one slot.Final pass
Four items on commit
15feae31. Hook tests 74 pass / 0 fail / 482 expects;tsc --strictclean; both hook files formatted explicitly withbiome check --writeover stdin (the repo config ignores.claude/).bash -nclean on all six scripts.--force—destroy.shnow treats a.gitFILE whosegitdir:is under this repository's.git/worktrees/as half-created only when that gitdir is GONE; when it still exists the directory is a worktree moved by hand and destroy refuses:Re-attach it with: git worktree repair '<path>' — then destroy it normally. Nothing was deleted.Probe:git worktree add unreg-orig; mv → unreg-moved; echo precious > uncommitted.txt; destroy unreg-moved→ exit 1, file intact; aftergit worktree repairit is registered anddestroy --forceremoves it; a.gitfile pointing at a gitdir that no longer exists is still removed and pruned (exit 0).minio-remove.sh"removed" for absent buckets — each bucket ismc ls-checked first (was already absent), thenrb --force, then re-checked (removedonly when it is gone,could NOT remove … (still present)otherwise, non-zero exit). Live on slot 3:removed×4 during destroy, then a second run on the same names →was already absent×2, exit 0; stub with a bucketrbcannot delete →could NOT remove stuck (still present), exit 1.(its WORKTREE_SLOT marker)only when the kept slot came from the marker, else(newest); probes: marker 5 over {1,5} →keeping 5 (its WORKTREE_SLOT marker); marker 7 over {1,5} →keeping 5 (newest); no marker over {5,1} with 1 newest →keeping 1 (newest).list.shsays "keeps the slot its WORKTREE_SLOT marker names, else the newest entry".producedText(piece, stdin)forwards incoming piped text throughtee(any args) andcatwhose args are all options/redirections;scriptFedToShellreads that carried text instead of looking one pipe back. Deny:cat <<'EOF' | tee f | bash,cat <<EOF | cat | bash,cat <<'EOF' | tee -a log | cat | sh,echo 'git push' | tee /dev/null | bash,printf 'git push' | cat - | bash. Allow:| sed … | bash,| cat other.sh | bash,| tee fwith no shell,| tee f | grep, agit statusbody; a piped body thatcds into the worktree lands there.Docs: the deny sentence (byte-identical in
SKILL.md,docs/claude/SCRIPTS.md,AI/ENTRYPOINT.md) now says "piped into one (including throughteeor an argument-lesscat, any number of times)"; SKILL.md's destroy row carries the moved-worktree refusal and the per-bucket pre-/post-check wording, and its registry paragraph the "marker, else newest" rule;docs/claude/SCRIPTS.md'sscripts/db/tree now listspush-dev.sh,release.sh,minio-init.sh,minio-provision.sh,minio-remove.sh.bun run check: exit 0 on the final tree (biome 54 pre-existing warnings / 0 errors, 10 workspaces typecheck, permissions 81 / db 327 / email 504 / api 1203 / ui 255, frontend green, 17 CI rules). Live slot-3 cycle: create green ready, destroy dropped both DBs, removed four buckets, flushed Redis 3, freed the slot; final state has notemplate_*wt_[3-9]databases, empty.slots/.locks, notmp-*branches,worktree:list= main + slot 1 + slot 2.Not exercised (cumulative): two creates racing for one slot; a real Linux/BusyBox
stat(stubs only); the docker-not-found branch of create, which cannot be probed on a Mac with Docker Desktop becauseresolve_dockerfalls through to/Applications/Docker.app/Contents/Resources/bin/dockerregardless ofPATH.🤖 Generated with Claude Code