From d176b636b7915de5f7004f2316234975637d9850 Mon Sep 17 00:00:00 2001 From: Matt Menefee Date: Wed, 5 Aug 2026 15:56:21 -0600 Subject: [PATCH 1/2] Keep wt numbering stable when agent worktrees exist `git worktree list` sorts linked worktrees by directory basename, and Claude Code creates throwaway checkouts for its subagents under `.claude/worktrees/agent-`. 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. --- home/.zshrc | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/home/.zshrc b/home/.zshrc index 7ea0a14..ba988d1 100644 --- a/home/.zshrc +++ b/home/.zshrc @@ -50,9 +50,22 @@ alias yo="yarn upgrade-interactive" # Git worktrees - navigate by number (e.g., wt 1, wt 2) # Run from any worktree to switch between them +# +# `git worktree list` sorts linked worktrees by directory name, which would interleave the throwaway +# worktrees Claude Code creates for its subagents (`.claude/worktrees/agent-`) with the +# permanent ones and renumber them mid-session. Listing the temporary ones last keeps `wt 2` +# pointing at the same checkout whether or not agents are running. +wtlist() { + git worktree list 2>/dev/null | awk ' + index($1, "/.claude/worktrees/") { temporary[++count] = $0; next } + { print } + END { for (i = 1; i <= count; i++) print temporary[i] } + ' +} + wt() { local worktree_path - worktree_path=$(git worktree list 2>/dev/null | sed -n "${1}p" | awk '{print $1}') + worktree_path=$(wtlist | sed -n "${1}p" | awk '{print $1}') if [[ -n "$worktree_path" ]]; then cd "$worktree_path" || return else @@ -61,7 +74,7 @@ wt() { } # List all worktrees with numbers -alias wtl='git worktree list | nl' +alias wtl='wtlist | nl' # Docker alias dc='docker compose' From a50beebe6b435d90f0f312a45bae8afc857fefae Mon Sep 17 00:00:00 2001 From: Matt Menefee Date: Wed, 5 Aug 2026 16:31:16 -0600 Subject: [PATCH 2/2] Fix wt path resolution and guard its argument MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- home/.zshrc | 46 +++++++++++++++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/home/.zshrc b/home/.zshrc index ba988d1..6a93b0d 100644 --- a/home/.zshrc +++ b/home/.zshrc @@ -51,25 +51,57 @@ alias yo="yarn upgrade-interactive" # Git worktrees - navigate by number (e.g., wt 1, wt 2) # Run from any worktree to switch between them # -# `git worktree list` sorts linked worktrees by directory name, which would interleave the throwaway -# worktrees Claude Code creates for its subagents (`.claude/worktrees/agent-`) with the -# permanent ones and renumber them mid-session. Listing the temporary ones last keeps `wt 2` +# `git worktree list` sorts linked worktrees by directory basename, which would interleave the +# throwaway worktrees Claude Code creates for its subagents (`.claude/worktrees/agent-`) with +# the permanent ones and renumber them mid-session. Listing the temporary ones last keeps `wt 2` # pointing at the same checkout whether or not agents are running. +# +# `wtl` reorders git's own listing so it keeps the commit and branch columns, while `wt` reads +# `--porcelain -z`, the only format that leaves a path containing spaces or newlines intact. Both +# walk git's ordering and hold back the same entries, so the numbers you see and the directories +# you land in stay in step. wtlist() { - git worktree list 2>/dev/null | awk ' - index($1, "/.claude/worktrees/") { temporary[++count] = $0; next } + git worktree list | awk ' + index($0, "/.claude/worktrees/") { temporary[++count] = $0; next } { print } END { for (i = 1; i <= count; i++) print temporary[i] } ' } +# Worktree paths in the same order `wtl` prints them, returned in $reply so that no delimiter has +# to survive the trip back +_wt_paths() { + local -a temporary + # `dir`, never `path`: zsh ties `path` to `PATH`, so a local of that name empties it and `git` + # stops resolving for the rest of the function + local record dir + reply=() + for record in ${(0)"$(git worktree list --porcelain -z 2>/dev/null)"}; do + [[ $record == 'worktree '* ]] || continue + dir=${record#worktree } + if [[ $dir == */.claude/worktrees/* ]]; then + temporary+=("$dir") + else + reply+=("$dir") + fi + done + reply+=("${temporary[@]}") +} + wt() { + if [[ $1 != <-> ]]; then + echo "Usage: wt " >&2 + return 1 + fi + local -a reply local worktree_path - worktree_path=$(wtlist | sed -n "${1}p" | awk '{print $1}') + _wt_paths + worktree_path=${reply[$1]} if [[ -n "$worktree_path" ]]; then cd "$worktree_path" || return else - echo "Worktree $1 not found (are you in a git repo with worktrees?)" + echo "Worktree $1 not found (are you in a git repo with worktrees?)" >&2 + return 1 fi }