Skip to content

Keep wt numbering stable when agent worktrees exist - #96

Open
mattmenefee wants to merge 2 commits into
mainfrom
stabilize-wt-worktree-numbering
Open

Keep wt numbering stable when agent worktrees exist#96
mattmenefee wants to merge 2 commits into
mainfrom
stabilize-wt-worktree-numbering

Conversation

@mattmenefee

@mattmenefee mattmenefee commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Summary

  • git worktree list sorts linked worktrees by directory basename, so the throwaway checkouts Claude Code creates for its subagents under .claude/worktrees/agent-<id> sorted in among the permanent worktrees rather than after them. A running agent renumbered the list mid-session, and wt 2 stopped resolving to the same checkout it meant a moment earlier
  • Adds a wtlist helper that holds the temporary entries back and prints them after everything else. Agent worktrees stay visible and reachable by number without displacing the permanent ones ahead of them
  • wt resolves paths from git worktree list --porcelain -z, read into an array. NUL-delimited records are the only format that keeps a path containing a space or a newline intact — plain --porcelain leaves a newline-bearing path split across two lines, which would invent an entry and pull the numbering out of step. wtl keeps reordering git's human-readable listing so it retains the commit and branch columns, matching against the whole line, which is equivalent to matching the path because git forbids a refname component beginning with a dot
  • Indexing an array also retires sed -n "${1}p", which was a sed program rather than a subscript: wt '1w /some/file' truncated an arbitrary file, wt '1r /some/file' read one back, and a bare wt printed every line into cd. A numeric guard now turns away anything that is not a plain number
  • Both refusals report on stderr and return non-zero. A missing worktree used to be announced on stdout while wt still exited successfully, so wt 9 && bin/dev went on to run the command from wherever the shell already stood
  • The loop variable in _wt_paths is named dir rather than the obvious path because zsh ties path to PATH — a local by that name empties it and git stops resolving inside the function. There is a comment on the declaration so a future tidy-up does not reintroduce it
  • Split out of Isolate review subagents in throwaway git worktrees #91, which needs this fix but is otherwise about isolating review subagents. Isolate review subagents in throwaway git worktrees #91 will be rebased to drop its copy of this change

Requires source ~/.zshrc to pick up the new functions; shells already running keep the old wt.

Test plan

  • zsh -n home/.zshrc parses
  • Ordering verified in a scratch repo holding a permanent worktree, one under a path containing a space, one under a path containing a newline, and an agent worktree nested beneath My Code/.claude/worktrees/wtl lists the agent entry last, and the earlier index($1, ...) form left it interleaved because $1 stops at the first space
  • wt N lands in the directory wtl numbers N for every entry, including the space-containing and newline-containing paths that the previous awk '{print $1}' truncated
  • Confirmed against git 2.55.0 that git worktree list --porcelain without -z splits a newline-bearing path across two lines while -z keeps the record whole, and that the human-readable listing C-quotes it onto one line — the reason the two consumers read different formats
  • wt abc, wt -1, wt '', wt '1p;2', and wt '1w /tmp/CLOBBER_ME' are all refused with Usage: wt <number> and exit 1, and the w attempt created no file
  • wt 0 and wt 99 report not found on stderr and exit 1
  • wt 2 && … runs the following command and wt 99 && … does not, with wt 99 || … firing as expected
  • wtl surfaces fatal: not a git repository again outside a repository, where the first revision of this branch printed nothing at all
  • Numbering and path resolution checked against the real dotfiles worktrees
  • No line exceeds 100 characters

@mattmenefee mattmenefee self-assigned this Aug 5, 2026
`git worktree list` sorts linked worktrees by directory basename, and
Claude Code creates throwaway checkouts for its subagents under
`.claude/worktrees/agent-<id>`. Those sort in among the permanent
worktrees rather than after them, so a running agent renumbered the
list mid-session and `wt 2` stopped meaning the same checkout it meant
a moment earlier.

Route both `wt` and `wtl` through a `wtlist` helper that holds the
temporary entries back and prints them after everything else. They stay
visible and reachable by number, but they no longer displace the
permanent worktrees ahead of them.
@mattmenefee
mattmenefee force-pushed the stabilize-wt-worktree-numbering branch from c942390 to d176b63 Compare August 5, 2026 22:00
The reordering added in the previous commit tested `$1` from
`git worktree list`, which stops at the first space. A worktree beneath
a path containing a space therefore never matched the
`.claude/worktrees/` marker, so agent checkouts stayed interleaved and
went on renumbering the list — the exact problem the change set out to
prevent. The same truncation reached `cd`, which would land silently in
a prefix directory rather than the worktree whenever that prefix
happened to exist.

`wt` now reads `git worktree list --porcelain -z` into an array.
NUL-delimited records are the only form that survives a path holding a
space or a newline; plain `--porcelain` leaves a newline-bearing path
split across two lines, which would invent an entry and pull the
numbering out of step. `wtl` still reorders git's human-readable
listing so it keeps the commit and branch columns, matching against the
whole line — equivalent to matching the path, because git forbids a
refname component beginning with a dot.

Indexing an array also retires `sed -n "${1}p"`, which was a sed
program rather than a subscript: `wt '1w /some/file'` truncated an
arbitrary file, `wt '1r /some/file'` read one back, and a bare `wt`
printed every line into `cd`. A numeric guard now turns away anything
that is not a plain number, closing those off for good.

Both refusals report on stderr and return non-zero. A missing worktree
used to be announced on stdout while `wt` still exited successfully, so
`wt 9 && bin/dev` went on to run the command from wherever the shell
already stood.

Error suppression moves down into the path helper as well. Sharing it
through the listing function had silenced `wtl` outside a git
repository, where it printed nothing at all in place of git's
explanation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant