worktree-init
July 11, 2026 · View on GitHub
adopt and upgrade are main-checkout-only. A new git
worktree of an already-adopted tracker repo gets the framework
state by symlinking its .apache-magpie/ directory to the
main checkout's snapshot, rather than maintaining its own copy.
One snapshot on disk, one upgrade source, every worktree always
current.
This sub-action is the worktree counterpart of adopt:
adoptruns in the main checkout, fetches the snapshot, writes both lock files, and wires up symlinks.worktree-initruns in a worktree, validates the main is adopted, and points the worktree's<snapshot-dir>at the main's. Nothing is fetched; no lock files are written.
The skill is idempotent: re-running on a worktree that already has the right symlink is a no-op.
Step 0 — Pre-flight
-
Confirm we are in a git worktree, not the main checkout. Compare
git rev-parse --git-diragainstgit rev-parse --git-common-dir. They are equal in the main checkout and different in a worktree. If equal, stop:"You appear to be in the main checkout (
<path>).worktree-initonly runs in a worktree. Use/magpie-setup(or/magpie-setup upgrade) here instead." -
Resolve the main checkout's path. Take
$(cd "$(git rev-parse --git-common-dir)" && pwd); the parent of that is the main checkout's working tree. Pin the result as<main>for the rest of this flow. -
Confirm the main checkout is adopted. Check that
<main>/.apache-magpie/exists and that<main>/.apache-magpie.lockexists. If either is missing, stop:"The main checkout at
<main>is not adopted yet. From the main checkout:cd <main> && /magpie-setup. Re-runworktree-inithere once that is complete." -
Inspect the worktree's
<snapshot-dir>state. Four possibilities, each handled below:Current state Action Missing Step 1 — create the symlink. Symlink to <main>/.apache-magpie/No-op. Surface "already wired" and stop. Symlink to something else Step 1 with a move-aside warning. The skill backs the existing link up, names what it pointed at, and asks the user to confirm before replacing. Regular directory (per-worktree snapshot from before this convention) Step 1 with a move-aside warning. Back up the directory to .apache-magpie.bak.<timestamp>and create the symlink. Do notrm -rfwithout confirmation — the directory may hold uncommitted local edits the operator wants to preserve before the framework standardised on snapshot-from-main.
Step 1 — Create the snapshot symlink
ln -s <main>/.apache-magpie <worktree>/.apache-magpie
Trusted external skill sources. If the main checkout has a
.apache-magpie-sources/ directory (the adopter trusts at least
one external source), share
it the same way so this worktree's source-skill symlinks resolve
against one snapshot on disk:
# only when <main>/.apache-magpie-sources exists:
ln -s <main>/.apache-magpie-sources <worktree>/.apache-magpie-sources
Then verify the chain end-to-end:
ls -la <worktree>/.apache-magpiereturns a symlink pointing at<main>/.apache-magpie.ls <worktree>/.apache-magpie/skills/lists the same skills asls <main>/.apache-magpie/skills/.- when sources are in use,
ls -la <worktree>/.apache-magpie-sourcesis likewise a symlink to<main>/.apache-magpie-sources.
Step 1b — Wire up the worktree's per-target symlinks
The snapshot symlink in Step 1 only makes the framework's
source available to this worktree. The per-skill symlinks (the
gitignored entries the agent harness actually resolves) live in
every active target dir (agents.md —
.agents/skills/ (universal), .claude/skills/,
.github/skills/, plus any present holdout) and are
per-worktree — each working copy needs its own in every
target. A worktree branched from before adoption
landed, or branched from a state where the symlinks were
cleaned, has none on disk.
Compose the effective family set for this worktree:
- Opt-in families the project recorded — read from
<main>/.apache-magpie.lock(the committed lock; the worktree shares it via git). - Always-on families — every
family: setupskill in the snapshot exceptsetupitself, and everyfamily: utilitiesskill, perSKILL.mdGolden rule 8 (read thefamily:frontmatter key, not the name prefix). These are added unconditionally, never read from the lock.
Wiring follows the canonical-plus-relay model
(agents.md), with no per-layout variation. For each
framework skill in the effective family set:
- Canonical (
.agents/skills/) — ensure<worktree>/.agents/skills/magpie-<skill>→../../.apache-magpie/skills/<skill>/(the worktree's snapshot symlink from Step 1). Create if missing, repair if broken or pointing at the wrong path, leave alone if correct. - Relays (
.claude/skills/,.github/skills/, any present holdout) — ensure<worktree>/<target>/skills/magpie-<skill>→../../.agents/skills/magpie-<skill>. Create / repair / leave alone the same way.
All these entries are gitignored and per-worktree.
The worktree's target directories themselves — .agents/skills/,
.claude/skills/, .github/skills/, any holdout — are not
framework artefacts; they are checked into the repo as part of the
adopter's layout, so every worktree inherits them via the
ordinary git worktree add flow. worktree-init only wires the
magpie-* entries inside them; it does not touch the
directories.
Pick a framework skill symlink that should now exist in each
active target dir (e.g.
<worktree>/.agents/skills/magpie-security-issue-sync/SKILL.md
and <worktree>/.claude/skills/magpie-security-issue-sync/SKILL.md)
and confirm readlink -f resolves each into
<main>/.apache-magpie/... rather than dangling — same
sanity check as Step 1's bottom bullet, just now end-to-end
from agent-harness path through the worktree's symlink
through the snapshot symlink to the framework source, in every
target.
Step 1c — Add the worktree to its own project-local sandbox allowlists
Defensive against
issue #197 —
sandbox.filesystem.allowRead: ["."] does not in practice cover
the worktree's working dir, so reads under this worktree fail
under the sandbox until an explicit absolute path is added. See
setup-isolated-setup-install/SKILL.md → Step P
for the underlying rationale.
If ~/.claude/scripts/sandbox-add-project-root.sh is installed,
invoke it from the worktree's working directory (no
--all-worktrees flag — only this one worktree needs adding;
the helper picks up the current worktree's
git rev-parse --show-toplevel and writes the abs path to
<this-worktree>/.claude/settings.local.json):
"$HOME/.claude/scripts/sandbox-add-project-root.sh"
Invoke with dangerouslyDisableSandbox: true — the target
settings.local.json is in Claude Code's built-in sandbox
denyWithinAllow set, so a sandboxed Bash write fails with
operation not permitted. Surface the bypass proposal to the
operator before invoking; name the helper and the target file
(<worktree>/.claude/settings.local.json); confirm. Reason:
"writing project-local sandbox-allowlist entry (issue #197
fix)".
The helper writes to project-local scope, not user-scope —
each worktree carries its own .claude/settings.local.json
entry, and the per-worktree file is gitignored. The helper is
idempotent (no-op when already added) and exits 0 when
prereqs are missing (no jq, not in a git repo). Surface a
one-line recap row for the Step 2 summary:
- ✓ already covered, OR
-
- added
<worktree-path>, OR
- added
- ⚠ helper not installed —
/magpie-setup-isolated-setup-installto wire it up.
worktree-init does not fail when the helper is absent;
secure-agent isolation is independent of framework adoption.
Step 1d — Seed the worktree's agent-guard PreToolUse hook
The committed .claude/settings.json wires the deterministic
guard (tools/agent-guard)
at $CLAUDE_PROJECT_DIR/.claude/hooks/agent-guard.py — a
per-worktree path. The script + its guards.d/ are
adopter-installed local files synced into the main checkout by
adopt.md Step 12 pass 1
/ upgrade.md Step 6b
and gitignored (adopt.md Step 7).
Because they are gitignored, no worktree inherits them via git —
every worktree starts without the script and would run with the
guard silently inactive until seeded.
This is the agent-driven counterpart of the
post-checkout hook's agent-guard seeding:
the git hook covers git worktree add, this step covers worktrees
that pre-date the hook or where its best-effort copy did not run.
Seed from the main checkout's already-synced copy — a plain file
copy, the same <main> resolved in Step 0:
# Only when the main has a guard and this worktree has none — never
# overwrite a copy the worktree already carries (worktree-local guards).
if [ -f "<main>/.claude/hooks/agent-guard.py" ] &&
[ ! -f "<worktree>/.claude/hooks/agent-guard.py" ]; then
mkdir -p "<worktree>/.claude/hooks/guards.d"
cp "<main>/.claude/hooks/agent-guard.py" "<worktree>/.claude/hooks/agent-guard.py"
cp "<main>/.claude/hooks/guards.d/"*.py "<worktree>/.claude/hooks/guards.d/" 2>/dev/null || true
fi
Idempotent: a no-op when the worktree already has the script, and a no-op when the main has no agent-guard yet (an adopter who has not run the Step 12 / Step 6b sync). Surface a one-line recap row for Step 2:
- ✓ already present, OR
-
- seeded from
<main>(script + N guards), OR
- seeded from
- ⚠ main has no agent-guard yet — run
/magpie-setup(or/magpie-setup upgrade) from the main checkout to sync it.
worktree-init does not fail when the main carries no
agent-guard; the guard is an opt-in adopter-side file, and the
worktree's framework-skill symlinks are usable without it.
Step 2 — Recap
Print a short summary:
- The snapshot symlink that was just created or confirmed.
- The main checkout's resolved path.
- The framework version the main is pinned at (read from
<main>/.apache-magpie.lock). - The effective family set wired in Step 1b across every
active target dir (
.agents/skills/, the.claude//.github/pair, any present holdout), split into opt-in and always-on, with per-skill ✓ / + / ↻ counts. - The sandbox-allowlist recap row from Step 1c.
- The agent-guard recap row from Step 1d (✓ already present /
- seeded / ⚠ main has no agent-guard yet).
- A reminder:
upgradefrom the main, not from the worktree.
Inputs
| Flag | Effect |
|---|---|
--force | Replace an existing <snapshot-dir> (symlink or regular dir) without the confirmation prompt. Skips the move-aside backup. Use only when you are sure the existing state holds nothing worth keeping. |
dry-run | Show what the skill would do without writing anything. |
Adopter overrides
This sub-action does not touch .apache-magpie-overrides/.
That directory is committed in the tracker repo and is
worktree-local by design — different branches may carry
different overrides. Symlinking it would conflate branches.
What this sub-action is NOT for
- Fetching the framework. Use
adoptin the main checkout first. - Upgrading the framework version. Use
upgradein the main checkout; the symlink means every worktree sees the refreshed snapshot immediately. - Auto-running on
git worktree add. Adopters who want automatic worktree initialisation can wrapgit worktree addwith a script that calls/magpie-setup worktree-init— the framework does not install that wrapper.
Failure modes
| Symptom | Likely cause | Fix |
|---|---|---|
| Step 0 step 3 stops with "main checkout not adopted" | The main has never run adopt. | cd <main> && /magpie-setup, then re-run worktree-init here. |
worktree-init runs but skills still fail to resolve | The per-target magpie-<skill> symlinks (in .agents/skills/, the .claude//.github/ pair, or a holdout) are missing from this worktree's commit (the worktree was branched from before adopt ran on main). | Re-run worktree-init from main's adopt flow afterwards, or git merge / git rebase the branch carrying the symlink commits. |
<snapshot-dir> is a regular directory and --force is not passed | A previous worktree snapshot is still on disk. | Re-run the skill, accept the move-aside prompt, then optionally inspect .apache-magpie.bak.<timestamp> for any non-snapshot content before deleting. |