One Checkout, One Owner: Native CLI Worktrees vs Your Lane Runner
Codex, Claude Code and Cursor now create git worktrees for AI agents. Give each checkout one owner, lock what your runner owns and prove it with a decoy.
Go deeper. Build your own.
A lane runner’s teardown goes to remove the checkout it created and finds a second one nested inside it: a .claude/worktrees/ directory on branch worktree-fix-auth, because the lane’s launch line still carried claude --worktree fix-auth from the week someone tried the native flag. Two creators, one lane. Each did what it was built to do, and the teardown now has to guess which checkout is its own.
Git worktree management for AI agents used to have one owner per repo: whoever typed git worktree add. In September 2026 three AI coding agents create checkouts on their own. Claude Code does it with --worktree, Cursor does it from its Agents Window, and since Sep 9 Codex CLI does it with --worktree and /worktree. Each binds sessions to checkouts, resumes into them and sweeps them on its own clock, which is also a fair description of your runner.
This is the runbook for giving every checkout exactly one owner. By Tuesday you have an ownership table per repo, one owner per root directory, one creator per lane, a lock carrying your reason on every checkout your runner made, and a decoy test that proves no native sweep touches a checkout it did not create.
Agents act, and they act inside checkouts. One removed under a running agent takes its uncommitted work with it; a session resumed into the wrong one commits a lane’s change on another lane’s base. Neither arrives as an error.
Sep 9: Codex CLI becomes the third native worktree creator
Codex CLI 0.154.0 shipped on Sep 9, 2026, and one line in its release notes changes who creates checkouts in your repos: “Experimental worktree support lets you create isolated checkouts for new or forked sessions using --worktree or /worktree, then browse and resume them.”
Screenshot: GitHub, “Release 0.154.0 · openai/codex · GitHub” (Sep 9, 2026), captured Sep 21, 2026.
The notes stop there, and the pull requests say more. PR #42196 creates detached, Desktop-compatible worktrees from HEAD or an explicit base. PR #42652 adds an experimental worktrees feature and the --worktree flag to codex exec, binds each checkout to its new thread, and does one more thing: “Share the configured worktree pool with Desktop while leaving automatic cleanup disabled for CLI allocations.” PR #43069 brings the flag to interactive sessions and codex fork --worktree, binds ownership before the first turn, and keeps unbound checkouts after a failed startup with recovery instructions for a human.
PR #43279 is the one to read twice. With worktrees enabled, Codex will “include corresponding directories across linked checkouts in resume and fork pickers and --last lookup”. A resume can land in a checkout other than the one you are standing in. Codex 0.155.0 followed on Sep 17 with “worktree ownership details and confirmed deletion of clean managed worktrees” in the agents overview.
The pool itself is described in the Codex app’s worktree docs. Checkouts live in $CODEX_HOME/worktrees unless someone changes the “Worktree root” setting, sit in detached HEAD, and follow the conversation: “Each chat keeps the same associated worktree over time.” The app keeps the 15 most recent, snapshots before it deletes, and deletes when you archive the chat. CLI allocations are exempt from that sweep, so nothing removes them on a timer.
Claude Code has been a creator for longer. Boris Cherny’s February post introduced claude --worktree, and the worktree docs have grown a rulebook since: a marker in the git metadata of every worktree Claude creates, a lock while an agent runs, and name-reuse rules that changed in July.
Cursor’s worktree docs, undated, with a cleanup section for 3.5 and later, sweep its root every 6 hours down to 25 worktrees per machine, with every workspace counted against the same limit. They say plainly that worktrees “created outside the manager (for example, worktrees created by /worktree skills or git worktree add) are eligible for deletion”. They do not name the root’s path, or say whether dirty or locked checkouts are spared.
One thing in these rules is reassuring: each native sweep is scoped by its own root or its own marker, and no doc describes a vendor reaching into a directory it doesn’t manage. The danger is narrower and more ordinary: a checkout placed inside a vendor’s root, or created twice because two creators each believed the lane was theirs.
The live guides assume one manager per repo, and inside that assumption they hold. Interruptible coordinators gives each worker a worktree under a single coordinator, subagent orchestration uses worktrees for parallelism, agentic CI/CD removes them after merge and puts TTLs on branches, and the Claude Code power guide runs one per task. Claude Code vs Cursor goes further and says the two tools’ worktrees coexist with ordinary branch discipline. That is true of branch names and not of placement, because Cursor counts every worktree under its root as a candidate for deletion.
Step 1: Build a git worktree ownership table for each repo
One table per repo, one column per native manager you actually run, plus your runner. Fill it from the docs first, then from your machines. Every cell that reads “not documented” or “check” becomes a test in step 5.
| Question | Claude Code | Codex CLI 0.154+ | Cursor 3.5+ | Your runner |
|---|---|---|---|---|
| What creates a checkout | --worktree / -w; isolation: "worktree" subagents |
--worktree, /worktree, fork --worktree (experimental worktrees feature) |
Agents Window; /worktree and /best-of-n skills in the IDE |
runner create, or its hook |
| Root path | .claude/worktrees/<name>/ at the repo root |
$CODEX_HOME/worktrees (app setting “Worktree root”) |
not documented; read it off your machine | yours: outside every root to the left, outside any repo |
| Branch scheme | worktree-<name> |
detached HEAD | check on your machine | lane/<lane>/<run-id> |
| Reuse by name | on the default fresh base, resets to the default branch only if clean, still on its branch and without commits of its own (or PR merged and remote branch deleted); otherwise reopens at the old tip | checkout bound to one owner thread | check on your machine | never; a new run gets a new checkout |
| Resume discovery | resuming returns the session to its worktree | with worktrees on, linked checkouts join the pickers and --last |
check on your machine | lane registry |
| Lock while running | git worktree lock while an agent runs; a -p lock stays until a later stale-lock sweep |
not documented | not documented | runner:<lane>:<run-id>, released in teardown |
| Cleanup trigger | sweep of subagent and background worktrees older than cleanupPeriodDays |
none for CLI allocations; app sweeps on archive or past 15; clean-only delete in the 0.155 overview | every 6 h, down to 25 per machine | teardown only |
| Spared by cleanup | changed or untracked files, unpushed commits, --worktree sessions you haven’t backgrounded |
app: pinned or in-progress chats, permanent worktrees; snapshot first | dirty or locked: not documented | anything not stopped, exported and clean |
| Touches checkouts it didn’t create | no: keeps any worktree without its marker (since 2.1.246) | not documented | yes, inside its root: “eligible for deletion” | never |
Three things fall out of a filled table. Two native sweeps count rather than age, so whether your checkout survives depends on what else was created that day. Claude’s sweep is the only one that documents keeping checkouts it didn’t make, and it learned that the hard way: the 2.1.246 changelog (Aug 25, 2026) fixed a background sweep that removed worktrees users had created themselves under .claude/worktrees/ when an old background-session record pointed at them. And Codex CLI checkouts are removed by nobody on a schedule, so they pile up unless your runner or a human in the 0.155 overview removes them.
How long anything should be kept is the cleanup audit’s job, not this one’s.
Two sweeps count and two don’t. A counted root evicts by everyone’s activity, including the checkouts you parked there.
Step 2: Give every root directory exactly one owner
A root is the directory a manager creates checkouts in and sweeps. The rule is short: your runner’s root sits outside .claude/worktrees/, outside $CODEX_HOME/worktrees, outside Cursor’s root, and outside any git repository. The last part is Claude’s requirement as much as yours: its docs refuse a hook-created directory inside a repository and tell you to “have the hook create its directories outside any repository”.
Cursor’s root is the awkward one, because the docs never name it. On each machine, create one worktree from the Agents Window, read its path from git worktree list, and write it into the table. Check Codex’s “Worktree root” setting on the same pass, because anyone can move it. If it ever points at your runner’s directory, your checkouts are sitting in Codex’s pool, and the app’s docs describe its rules for Codex-managed worktrees only.
Put the check in the runner rather than in a wiki page. An unknown root fails closed.
# Illustrative runner preflight: refuse a root that overlaps any native manager's root
RUNNER_ROOT=/srv/lanes # outside any repository
FORBIDDEN=(
"$REPO/.claude/worktrees"
"$CODEX_HOME/worktrees" # CODEX_HOME as your machines set it
"$CURSOR_WORKTREE_ROOT" # read off the machine in step 2
)
for f in "${FORBIDDEN[@]}"; do
[ -n "$f" ] || { echo "native root unknown; fill the table first" >&2; exit 1; }
case "$RUNNER_ROOT/" in "$f"/*) echo "runner root is inside $f" >&2; exit 1;; esac
case "$f/" in "$RUNNER_ROOT"/*) echo "$f is inside the runner root" >&2; exit 1;; esac
done
if git -C "$RUNNER_ROOT" rev-parse --git-dir >/dev/null 2>&1; then
echo "runner root is inside a git repository" >&2; exit 1
fi
Both directions matter. A runner root inside a vendor root is the obvious mistake, and a vendor root moved inside yours is the one nobody notices until a sweep runs.
Each sweep stays inside its own box. The runner’s job is to never put a checkout in someone else’s.
Step 3: Allow one creator per lane, the runner or a hook that calls it
Pick one of two patterns per lane, write it into the lane manifest, and never run both.
Pattern A: the runner creates, the CLI runs plainly inside. The runner makes the checkout, locks it (step 4), changes into it and launches the agent with no worktree flag at all.
# Illustrative lane launch, pattern A
wt="$RUNNER_ROOT/$LANE/$RUN_ID"
git -C "$REPO" worktree add -b "lane/$LANE/$RUN_ID" "$wt" "$BASE"
git -C "$REPO" worktree lock --reason "runner:$LANE:$RUN_ID" "$wt"
cd "$wt"
claude -p "$TASK" # never -w or --worktree on a runner lane
# codex exec "$TASK" # never --worktree; worktrees feature stays off
Keep Codex’s experimental worktrees feature disabled in the config your runner lanes load. PR #42652 rejects --worktree when the feature is off, so a stray flag stops the launch instead of creating a second checkout, and PR #43279’s cross-checkout --last lookup only applies with the feature on.
Then lint every lane before launch. Fail on --worktree or -w in a Claude or Codex command line, on /worktree in a prompt file, and on isolation: "worktree" in any subagent definition the lane loads. That last one is easy to miss: Claude’s hooks reference lists a finishing isolation: "worktree" subagent among the worktree-removal triggers, so those subagents get checkouts of their own. Without the hook below, those checkouts are Claude’s, under Claude’s sweep.
Pattern B: the vendor flag delegates to the runner. When a lane needs Claude’s own --worktree flow, for a person working interactively say, hand creation to the runner through Claude’s hook pair. The worktree docs say a WorktreeCreate hook replaces “the default git worktree logic entirely”. The hooks reference sets the contract: the hook “must return the path” (a command hook prints it on stdout, an HTTP hook returns hookSpecificOutput.worktreePath), and “any non-zero exit code from WorktreeCreate aborts worktree creation”. WorktreeRemove fires when someone exits a --worktree session and chooses removal, when an isolation: "worktree" subagent finishes, and when a background session whose worktree the hook created is deleted.
Illustrative .claude/settings.json fragment for a pattern-B lane (check the hooks reference for matcher and input fields):
{
"hooks": {
"WorktreeCreate": [{ "hooks": [{ "type": "command", "command": "/srv/lanes/bin/runner-hook create" }] }],
"WorktreeRemove": [{ "hooks": [{ "type": "command", "command": "/srv/lanes/bin/runner-hook remove" }] }]
}
}
The runner’s hook does what pattern A does: create outside any repo, lock with the runner’s reason, register the lane, print the path. On any failure it exits non-zero, which aborts creation rather than falling back to Claude’s default root. That is the fail-closed direction you want. The hook-created checkout carries no Claude marker, so Claude’s sweep keeps it, and cleanup stays in the runner’s teardown.
The hook is a guardrail with a known edge. It governs Claude only; Codex’s worktree PRs and app docs describe no creation hook, so Codex lanes run pattern A. And it only works if the lane loads the settings file that carries it; skip that file and Claude’s default logic creates the checkout in its own root. Prove a headless lane loads what you declared before you rely on it, and keep step 2’s placement rule and step 5’s decoys as the wall behind the hook.
Step 4: Lock what your runner owns, and unlock it only in teardown
Lock right after create, with a reason that names the owner: git worktree lock --reason "runner:<lane>:<run-id>" <path>. Claude’s docs back this directly: “The sweep never releases a lock you set yourself with git worktree lock.” Git itself keeps a locked worktree out of git worktree prune and makes a plain git worktree remove refuse it. Neither Codex nor Cursor documents what its cleanup does with a locked checkout, so for those two the lock is a request, and step 5 finds out whether it is honored.
Screenshot: Claude Code Docs, “Run parallel sessions with worktrees - Claude Code Docs” (undated), captured Sep 21, 2026.
The teardown is the only place a lock comes off, and it has one more job. If a lane ever runs claude -p --worktree, Claude’s docs say “Non-interactive runs with -p have no exit prompt, so Claude doesn’t clean up their worktrees, and Claude Code leaves the lock it took on each one at creation in place until a later session’s stale-lock sweep releases it.” On a headless box that later session may never come. Clear the lock yourself, on your own lane’s checkout, once the lane’s Claude process has exited.
# Illustrative teardown; the order is the point
kill -0 "$AGENT_PID" 2>/dev/null && { echo "agent still running; not tearing down" >&2; exit 1; }
export_ignored "$wt" # logs and transcripts first; see the cleanup audit
lock=$(git -C "$REPO" worktree list --porcelain |
awk -v p="$wt" '$1=="worktree"{cur=substr($0,10)} cur==p && $1=="locked"{print "L:" substr($0,8)}')
case "$lock" in
"L:runner:$LANE:$RUN_ID") git -C "$REPO" worktree unlock "$wt" ;;
L:*) [ "$LANE_RAN_CLAUDE_P_WORKTREE" = yes ] || { echo "foreign lock on $wt: $lock" >&2; exit 1; }
git -C "$REPO" worktree unlock "$wt" ;; # Claude's leftover -p lock, process gone
"") echo "warning: $wt was not locked" >&2 ;;
esac
git -C "$REPO" worktree remove "$wt" # never --force
A lock is a guardrail, not a wall. Anyone who passes --force twice to git worktree remove gets past it, and two of the three vendors don’t say whether their cleanup checks. The walls are placement (step 2) and export before delete, which the cleanup audit owns.
Step 5: Run the collision checks and the decoy test
Run these on a test machine with the same CLI versions as your lanes.
Check 1: --last resumes the right checkout. On the test machine only, enable Codex’s worktrees feature, start one Codex session in checkout A and another in checkout B, then run codex resume --last from a runner worktree. Pass: it opens the session tied to the checkout you are in, or none. Fail: it opens a session bound to a different checkout. On runner lanes the fix is the feature staying off, which the step 3 lint enforces.
Check 2: Claude’s name reuse, both ways. Run claude --worktree probe, exit with the tree clean and no commits, keep the worktree when asked, then reuse the name and confirm it reset to the default branch. Commit once in it without merging, exit, reuse the name again, and confirm it reopened at the old tip. Both behaviors are documented; the reset arrived in 2.1.208 on Jul 14, 2026, and before that every reuse reopened the old tip. A runner that reuses lane names is silently betting on one of them.
Check 3: the decoys. One question: does any sweep touch a checkout outside its own root, or one without its own marker? Plant clean decoys and leave them through at least one full cycle of every sweep: Cursor’s 6-hour pass, a Codex app archive, and Claude’s retention period.
| Decoy | Where | What the docs predict | Pass |
|---|---|---|---|
| Two plain decoys | runner root | no native sweep is scoped there | both present |
Foreign decoy (git worktree add) |
.claude/worktrees/ |
kept: no Claude marker, since 2.1.246 | present |
| Foreign decoy | $CODEX_HOME/worktrees |
docs cover Codex-managed worktrees only | present, or record what removed it |
| Foreign decoy | Cursor’s root | eligible for deletion | record; this is the documented risk |
| Locked foreign decoy | Cursor’s root | not documented | record whether the lock spared it |
Keep decoys clean. An untracked sentinel file makes a checkout dirty, and Claude keeps dirty checkouts for that reason alone, which would hide the marker result. Record decoys in a register outside the tree instead.
# Illustrative decoy plant and check; the register lives outside every root
plant_decoy() { # plant_decoy <path> <label> [lock]
git -C "$REPO" worktree add --detach "$1" HEAD || return 1
[ "$3" = lock ] && git -C "$REPO" worktree lock --reason "decoy:$2" "$1"
printf '%s\t%s\t%s\n' "$1" "$2" "$(date -u +%FT%TZ)" >> "$DECOY_REGISTER"
}
check_decoys() {
while IFS=$'\t' read -r path label planted; do
if [ -d "$path" ]; then echo "present $label"; else echo "GONE $label (planted $planted)"; fi
done < "$DECOY_REGISTER"
}
The Cursor rows only mean something on a machine that actually passes 25 worktrees, so check the count there before you read the result. A missing decoy anywhere else is the signal this whole piece is about. Wire the check into the upgrade canary, because these rules moved four times between Jul 14 and Sep 17, and run it weekly between upgrades.
Worktree collisions between native CLIs and your runner, and the signal for each
A second checkout nested in a lane. Signal: git worktree list shows a path under a runner checkout, or a worktree-<name> branch appears on a runner lane. Cause: a pattern-A lane that also passed a native flag. Fix: the step 3 lint, failing the launch.
A resume in the wrong checkout. Signal: the session’s working directory differs from the checkout the lane registry assigned, or commits land on a detached HEAD that belongs to another lane. Cause: --last looking across linked checkouts with worktrees on. Fix: feature off on runner lanes; resume by explicit session.
An old tip where you expected a fresh base. Signal: a new run starts with commits it didn’t make. Cause: Claude name reuse on a checkout holding unmerged commits, which reopens at its old tip by design. Fix: never reuse names across runs; put the run ID in the name.
A checkout evicted by someone else’s busy day. Signal: a decoy or a real checkout inside Cursor’s root disappears after the machine passes 25 worktrees. Cause: placement inside a counted root. Fix: step 2.
A lock nobody will release. Signal: git worktree list --porcelain shows locked on a checkout whose process is gone, with a reason that isn’t yours. Cause: a -p run’s lock waiting for a stale-lock sweep that no later session will run. Fix: the teardown clears it after checking the process.
Leftovers with no owner. Signal: a checkout in the Codex pool with no owner in the 0.155 overview, or a detached checkout holding commits that no branch reaches. Cause: a startup failure (PR #43069 keeps unbound checkouts and prints recovery steps), or work finished on a detached HEAD. Fix: recover it by hand and name an owner before the next sweep decides for you.
Worktree ownership belongs to the layer that runs the fleet
No vendor here is wrong about its own checkouts. Each knows its root, its marker and its sweep, and none of them knows your runner exists. The ownership table, the root preflight, the launch lint, the locks and the decoy register can’t live in any one vendor’s settings. They live one layer up, where a multi-agent command center keeps every CLI’s sessions and checkouts on one board.
One checkout, one owner is a small rule. It only holds if you can name the owner of every checkout on the machine, and the decoy is how you find out whether you can.
FAQ
Does Cursor delete git worktrees it didn’t create?
Yes, inside its own root. Cursor’s docs say worktrees created outside the manager, including ones made with git worktree add, are eligible for deletion. The sweep runs every 6 hours down to 25 per machine, and the docs don’t say whether dirty or locked checkouts survive. Keep your runner’s checkouts outside Cursor’s root.
Can I use Claude Code’s --worktree flag with my own lane runner?
Only through a hook, because each lane needs one creator. If your runner creates the checkout, launch Claude with no worktree flag, or you get a second checkout nested in the lane. When a lane needs --worktree, a WorktreeCreate hook that calls your runner replaces Claude’s default logic, and any non-zero exit aborts creation.
Does git worktree lock protect a worktree from AI agent cleanup?
Partly. Git keeps a locked worktree out of git worktree prune, a plain git worktree remove refuses it, and Claude’s sweep never releases a lock you set yourself. Passing --force twice still gets past it, and Codex and Cursor don’t document how their cleanup treats locks, so confirm with a decoy.
Sources
- Codex 0.154.0 release notes — Sep 9, 2026; experimental worktree support
- Codex PR #42652 — Sep 4, 2026;
worktreesfeature, shared pool, CLI allocations exempt from cleanup - Codex PR #43069 — Sep 5, 2026; interactive sessions, forks, unbound checkouts kept
- Codex PR #43279 — Sep 6, 2026; linked checkouts in pickers and
--last - Codex 0.155.0 release notes — Sep 17, 2026; ownership details, clean-only delete
- Worktrees, ChatGPT Learn — Codex app pool, keeps 15, snapshot first
- Claude Code worktrees docs — marker, lock, reuse rules,
WorktreeCreate - Claude Code hooks reference —
WorktreeCreateandWorktreeRemovecontract - Claude Code changelog — 2.1.208 (Jul 14, 2026); 2.1.246 (Aug 25, 2026)
- Cursor worktrees docs — 6-hour cleanup, 25 per machine, foreign worktrees eligible
