487 KiB
Changelog
Unreleased
Improved
-
wt config showgives onewt config shell installfor the whole shell section: an outdated wrapper, fish integration still at the deprecatedconf.dpath, and a missing fish completions file each carried their own per-shell hint, so a machine with all three printed three commands where the bare one fixes everything. The rows still say what's wrong; the section's single trailing hint says what to run. The zshcompinitsnippet is syntax-highlighted like every other command block. -
Interactive prompts no longer open with a blank line:
wt config shell install,wt config plugins claude install, and the commit-message setup offer at the top ofwt mergeall began with one. A prompt that follows other output keeps the blank line separating it. -
wt config update --output <path>confirms the write: it now prints✓ Wrote user config migration @ ~/migrated.toml, so a command whose only effect is the file it wrote now says where that file is. Writing to stdout with--output=-stays silent, since the artifact is right there. (#4053)
Fixed
-
wt config plugins opencode installno longer writes the plugin into the directory it was run from: an exported-but-emptyOPENCODE_CONFIG_DIRwas taken at face value, so the install target collapsed to the relative pathplugins/worktrunk.ts— typically somewhere inside the user's repository.is_plugin_installed()then read the plugin back from that same relative path, so the install reported success while OpenCode never saw it. An empty value now reads as unset, as it already did for$CLAUDE_CONFIG_DIRand$PI_CONFIG_DIR; the documented precedence$OPENCODE_CONFIG_DIR>$XDG_CONFIG_HOME/opencode>~/.config/opencodeis unchanged. (#4084) -
wt step pruneno longer deletes a branch created minutes ago from an older commit: the min-age guard aged a branch with no worktree by the committer date of the commit it points at, sogit branch <name> mainon a default branch whose last commit was over a day old was pruned on the next run — the case--min-ageexists to prevent. The branch's age now comes from its oldest reflog entry, written when the branch was created. -
wt mergeno longer deletes a worktree locked withgit worktree lock: its cleanup step removed the worktree anyway and reported success. The merge now completes and printsWorktree preserved (locked), and every Worktrunk removal path refuses a lock even under--force— whatgit worktree removedoes, minus git's-f -foverride. Unlock withgit worktree unlock <path>first. (#4073, thanks @Duang777) -
wt switchrecovers from a removed worktree in a bare repository: recovery walks up from$PWDlooking for a.gitdirectory, and a bare repo has none — not at the bare directory and not at any ancestor — so the walk ran to the filesystem root and found nothing. Bare worktrees typically sit inside the bare directory, leaving it nowhere else to land, sowt switchand the picker surfaced git's raw "cannot resolve CWD" error instead of recovering and the removed-directory message dropped itswt switch ^suggestion. A directory that is itself a bare repository now counts as one to recover from; the deleted path still has to appear in that repository's worktree list before recovery accepts it. (#4067) -
wt mergeno longer treats the feature's own files as uncommitted changes on the target: whenwtruns with an inheritedGIT_DIRpinned to the invoking worktree — a!wtgit alias from a linked worktree is one source; git also exports discovery vars to the hooks it spawns — worktree-localstatusandread-treeforwarded it, so the target conflict check compared the invoking index to the target worktree and refused a merge that should have been clean. Those calls now discover the worktree from the directorywtchose; repo-level plumbing keeps the inherited context so the alias still finds the repository. -
wt removeno longer deletes a dirty worktree when an inheritedGIT_DIRmasks its changes: the same forwardedGIT_DIRmadeensure_cleancompare the target's working tree against the invoking worktree's index, so when the two agreed on a path a genuinely dirty target read as clean and removal proceeded with no--forceand no warning. The dirty gate now reads the worktree being removed. -
wt config updatemigrates[ci] platforminto a[forge]section that only setshostname: the migration stood down whenever a[forge]section existed at all, so the config a GitHub Enterprise or self-hosted GitLab user ends up with —[ci] platformfrom before the rename,[forge] hostnameadded later for an SSH host alias — kept the deprecated key with no warning and nothingwt config updatewould do about it. The platform still resolved, so nothing broke; the deprecation notice that precedes[ci]'s eventual removal simply never arrived. A[forge]that already setsplatform, or aforgethat isn't a section, still stands down. -
wt config shell installmigrates a fish wrapper at the deprecatedconf.dpath even when~/.config/fish/functionsdoesn't exist yet: fish was skipped for want of a config location, so the bare command left the deprecated wrapper running and onlywt config shell install fishmigrated it. A worktrunk wrapper at the old path now counts as fish being configured, just at the old path.wt config showreports that wrapper too, where before it showed nothing at all for fish unless fish was onPATH. -
wt config shell installno longer tells an already-wrapped shell to restart: reinstalling from inside a shell that has the wrapper loaded — after a version bump, or when the fish extension moves fromconf.dtofunctions— printed↳ Restart shell to activate shell integrationright after the wrapper had intercepted the command. The hint now only appears when integration isn't active. -
An alias that binds
dry_runcan take--dry-run <value>:wt <alias> --dry-run <value>answered the retired-flag error even when the alias's own template referenced{{ dry_run }}, while--dry-run=<value>bound normally — so one flag behaved two ways. Both value-taking spellings now bind for an alias that references the name, matching how--helpand--yesalready yield to a template that binds them. A bare--dry-runwith no value still errors, since forwarding it into{{ args }}would leavedry_rununset and run the live command; the message names--dry-run=1. (#4058) -
wt mergeno longer runspost-commitin a worktree the user never chose: the merge removes the worktree that hook is anchored on before the hook starts, so the pipeline was spawned into an emptied path. Where the worktree sat inside the repository, git discovery walked up from there and the hook's commands ran in the primary worktree; anywhere else they didn't run at all, and the only record was a log file nothing reads. The merge now prints▲ Skipped post-commit: …naming that worktree, and points atpre-removefor work that must finish there.--no-remove, merging on the target branch,wt step commit, andwt step squashstill run it. (#4049) -
wt config plugins claude uninstall/codex uninstallno longer report success over a marketplace that is still there: to tell an already-removed marketplace from a removal that failed, uninstall used to read the harness's own config file. A file or key the harness renamed reads as an absence there, so every failed removal would have printedPlugin & marketplace removedand exited 0. It now asksclaude/codex plugin marketplace list --json, and an answer it cannot read leaves the harness's error standing. (#4048) -
wt config showreports Claude Code's plugin and Gemini CLI's extension from those tools' own listings: it readinstalled_plugins.jsonand the~/.gemini/extensions/directory, so a store either tool relocated read as an absence and the section said "not installed" over an installed plugin. Both answers now come fromclaude plugin list --jsonandgemini extensions list -o json.wt config plugins claude uninstallfollows: it runs the plugin removal and the marketplace removal every time, each tolerating only the absence Claude Code itself reports, so it no longer exits early withPlugin not installedon a file read. (#4054)
0.77.0
Improved
-
wt listsizes and aligns its columns to the terminal width: one long branch name no longer sizes the Branch column for every row — it caps at 32 characters and elides, so a narrow terminal keeps its other columns instead of degenerating into a branch list.Remote⇅stops holding blank space open in a repo with no remote, and alignment now follows the value type.--format=jsonstill carries the whole name. (#3998) -
wt listnames what it hid and marks detached worktrees: the summary readshidden: Path, Commitinstead of counting columns, a detached worktree shows⊘rather than borrowing⚑, and a prunable row leaves its cells blank instead of showing loading dots that never resolve. (#3998) -
wt list --format=jsongains amarkerfield and stops reportingdetachedtwice: the branch marker set bywt config state markeris now readable without parsing it out ofsymbols, andstateno longer reports a detached worktree asbranch_worktree_mismatch— the siblingdetachedboolean carries it. (#3998) -
wt list --format=jsonnow defaults to schema 2: callers get the envelope with repository metadata and orthogonal per-item facts without configuring[list] json-schema.wt list statusline --format=jsonfollows the same key. Setjson-schema = 1to retain the original bare-array format. (Breaking.) (#4038) -
wt config showfails on a broken config: it exits 1 on an unreadable or invalid config, an invalid[list] columns, or an invalidapprovals.toml. Unknown keys and deprecations stay warnings and still exit 0. A newAPPROVALSsection counts project commands awaiting approval. (Breaking: it always exited 0 before.) (#3999) -
Config migration output names its destination:
wt config update --output <path>writes the migration artifact to that file instead of applying it in place, and--output=-writes it to stdout. Output mode includes project config when run from a linked worktree. (Breaking:--printwas removed.) (#4021) -
Pi joins the agent integrations:
wt config plugins pi installwrites an activity hook to~/.omp/agent/hooks/pre/worktrunk.ts, so Pi sessions show 🤖/💬 markers inwt listlike Claude Code, Codex, OpenCode, and Gemini.$PI_CONFIG_DIR,$PI_CODING_AGENT_DIR, and named$OMP_PROFILE/$PI_PROFILEprofiles are honored. Docs (#3594, thanks @adity982, and @ortonomy for the request) -
wt config plugins codex installinstalls the plugin too: it previously registered the marketplace and left you to run/pluginsin Codex yourself. Uninstall follows for both Codex and Claude Code, removing the plugin and then the marketplace, so the two commands are inverses. (Breaking: Codex uninstall previously left an installed plugin alone; Claude uninstall now also removes the marketplace.) (#4019) -
wt step copy-ignoredsays whether it reflinked or copied: the summary now reads(reflinked, no extra disk),(full copy), or a partial count, so the same29.5 GBline distinguishes a free copy from one that actually wrote the bytes.--format=jsongainsreflinkedandwritten. (#4025) -
wt config updateremoves keys its destination cannot hold: migrating[select]or[commit-generation]drops keys the new section has no field for, reporting each one. Otherwise the key landed at a path the user never typed and warned on every command, with no way to clear it. (#3994)
Fixed
-
A failed
wt switch pr:<n>against a fork no longer deletes a branch it didn't create: the rollback force-deleted the branch on any setup error, including the one case where the branch wasn't its own — apre-switchhook or a concurrent session claiming the name in the window after the forge answered. Branch and worktree are now created in onegit worktree add -b, which writes nothing when the name is taken. (#3984) -
Codex on Windows: the activity hooks no longer fail on every event: each hook led with a bare
bash, whichcmd.exeresolves to the WSL launcher rather than Git Bash, so every event raised aHook failedbanner. Hooks now runwt.shthrough a shim that finds Git Bash by path. (#4008, fixes #4007, thanks @McNultyyy for reporting and diagnosing) -
Templates leave
branchunset in a detached worktree: aliases and hooks rendered{{ branch }}as the literalHEAD, which git resolves, so an unguarded template ran against the wrong ref — the reported case ended ingit push origin --delete HEAD, which git refused.baseandtargetfollow. (Breaking: an unguarded{{ branch }}now errors.) (#4010, fixes #4009) -
The OpenCode plugin loads under OpenCode 2: OpenCode 2's loader dropped the old bare-function export silently, so activity markers stopped appearing. The plugin now exports one object carrying both the v2
setupand v1serverentry points, and spawnswtthroughnode:child_processrather than the Bun shell. Re-runwt config plugins opencode installto pick it up; the one file covers OpenCode 2 and OpenCode 1.16 or later. (#4018, thanks @pragmaticivan for the request) -
wt step commit --branchbuilds its prompt from the worktree being committed: prompt-building git plumbing ran in the invoking worktree, so with nothing staged there the generated message described none of the changes it committed.wt step relocate --commithad the same bug, and--dry-run/--show-promptnow preview the worktree--branchnames. (#3996) -
LLM commit messages keep diffs for quoted paths: a file whose name git quotes in the
diff --githeader — any non-ASCII name under the defaultcore.quotePath— had its whole diff section dropped from the prompt once the diff exceeded the 100 KB budget. (#4041) -
An unparsable user config is reported, not a panic:
wt config show --format=jsonand--full, pluswt step prune,relocate,eval, andfor-each, panicked on a debug build and printed a bare✗ Command failedon a release one. They now report✗ Failed to load configwith the parser's caret diagram. (#4002) -
wt switch --executenames the directory the program actually starts in: theExecuting (--execute) @ …header rendered the worktree the background hooks run in, not the program's own directory — which differs under--no-cdor a switch from a subdirectory. The path is omitted when the shell already stands there. (#4043, fixes #4042, thanks @yajo for reporting) -
wt config plugins claude|codex uninstallis safe to re-run: removing an already-absent marketplace reported a failure, and a Claude uninstall that removed the plugin and then failed left the marketplace with no way to clear it. Both now finish a half-done uninstall and succeed when there is nothing left to remove. (#4033, #4034) -
wt config state markerset and clear no-op outside a git repository: they exited 1 with a git error, so an agent plugin running outside a repository printed one every turn (the hook's|| truekept the session going). (#3981, thanks @mahirhir) -
wt listproduces output in a bare repo with no worktrees: it printed nothing at all and exited 0. It now reports○ No worktreeswith a hint,--brancheslists the branches, and--format jsonemits the envelope with an emptyitems. (#3992) -
wt listwarns when it can't parse the project config rather than ignoring it silently; the listing still succeeds. (#3993) -
Relayed command output no longer prints ahead of the delayed-stream progress line, and delayed-stream output routes through anstream so color is stripped consistently when redirected. (#4037, #3995)
-
A hook filter that matches nothing names the unconfigured source, rather than reporting no match without saying which source was empty. (#3997)
Documentation
-
wt hooksays thatpost-*hooks from the two sources run concurrently, so dependent commands belong in one source;wt mergerecords thatpost-commitcan't run when the merge removes the worktree it anchors on. (#4020, #4026) -
wt stephelp pages describe what the commands actually do:wt step prunesays it removes branches as well as worktrees,wt step copy-ignoreddocuments--from/--to,wt step commitdocuments--branch, and both it andsquashdocument their hooks and no-LLM fallback messages. (#3997) -
The FAQ lists the files Worktrunk writes outside its own config: OpenCode's plugin, Pi's hook, and the Claude Code
settings.jsonstatusline entry, alongside the existing inventory of what Worktrunk creates and deletes. The Claude and Codex plugin installs write nothing themselves. (#4036) -
wt step copy-ignoreddocuments its disk-space benefit per filesystem, and the bare-repoworktree-pathexample names the bare repo's own location. (#4022, #4006) -
Terminal examples on the docs site fit a desktop window rather than overflowing. (#4039)
0.76.0
Improved
-
wt switch --executetakes a program, not a shell string:-xnames one program, with everything after--passed as literal argv. Worktrunk spawns it as a child rather than running it in your interactive shell, so shell functions and itscdno longer reach you.-x sh -- -c '…'recovers shell syntax, not functions. (Breaking: existing-xstrings, plusWORKTRUNK_DIRECTIVE_EXEC_FILEandWORKTRUNK_SHELL.) (#3977, closes #2860, fixes #3944, thanks @omgreenfield for testing the migration path) -
Worktrunk decides tracking for the branches it creates: a new branch gets an upstream only when its name matches the remote branch it starts from, whatever
branch.autoSetupMergesays:--create release --base origin/releasetracks,--create feature --base origin/releasedoes not. UnderautoSetupMerge = falsethis previously exited 128 after creating the worktree. (#3913, #3950, fixes #3937, thanks @mjakl for reporting and diagnosing) -
Retired config keys are no longer read:
commit.generation.template-fileandsquash-template-fileare gone — put the file's contents intemplateorsquash-template. There is no migration: the key stays, warns as unknown on every load, and the prompt reverts to the built-in default. Setting bothtemplateandtemplate-fileused to fail the load; it now loadstemplate.switch.picker.timeout-mslikewise warns rather than being stripped. (Breaking.) (#3949) -
Project aliases and hooks can use
wt switch --execute: their bodies previously refused it. Worktrunk now starts the program itself instead of handing shell text to your shell, and the command-approval gate remains the control on project-defined commands. (#3977) -
Established automation and LLM customization interfaces are now stable:
wt step eval,wt step for-each,wt step prune, LLM branch summaries,wt config state vars, andcommit.generation.template-appendare no longer marked experimental. (#3949) -
wt step prunestages removals concurrently again: only thegit worktree removeteardowns serialize, so dirty checks, fsmonitor shutdown, and the trash rename overlap. Theprune_e2e/livebenchmark median went from 377 ms to 222 ms. (#3954) -
wt config approvals addreads as a review rather than a warning: it opens with a cyanReview 1 command for repo:instead of a yellow▲ … needs approval to execute. The execution-time gate is unchanged. (#3953)
Fixed
-
The
wt switchpicker survives a preview longer than 65,535 lines: skim keeps the pane's line count in au16, so a larger diff abortedwtwith exit 101 seconds after it painted — on screen the picker looked like it closed by itself. Panes now cap at 60,000 lines and say so. (#3959, fixes #3958, thanks @sandertammesoo for reporting and diagnosing) -
The nushell wrapper propagates exit codes without a POSIX shell: it reported a failing
wtby spawningsh, so on Windows every failing command printed aCommand `sh` not foundtrace and exit 1 whatever the real code was. Nushell's wrapper is a static file, so rerunwt config shell installto pick this up. (#3945) -
The picker no longer shows one detached worktree's diff for another: previews were keyed by branch name, so every detached row shared the key
(detached). Rows now key by canonical identity, which also letswt remove feature ~/repo.featureplan that worktree once instead of failing on the second. (#3926) -
wt switch pr:<n>givespre-switchhooks the real branch:branchandtargetheld the literalpr:3933, andtarget_worktree_pathwas unset even when that branch had a worktree.pr_numberandpr_urlare now set for same-repo PRs, not only forks. (#3941, fixes #3934, thanks @robsonpeixoto for reporting) -
wt config shell installno longer replaces an rc file created while it was checking: the missing-file path refuses to clobber and fails with a rerun hint. A dangling rc symlink is preserved and rejected rather than overwritten. (#3929) -
wt switch --create X --base pr:<n>works when the PR branch has no local branch: the fetch writes only the remote-tracking ref, and git won't expand a bare name to one, so the command failed withNo branch, tag, or commit named …. The base is now named under its remote. (#3951) -
wt switch --createstreamsgit worktree addinstead of stalling: where a sandbox denied the timed wait's pipe orsigaction, the command waited out git's entire run and then reportedFailed to wait for commandfor work that had already succeeded. (#3881) -
Claude Code activity markers stay on the session's launch worktree: the plugin's marker hooks pass
-C "$CLAUDE_PROJECT_DIR", so acdmid-session no longer marks another repository. A session launched outside a repository gets no marker. (#3956)
Documentation
-
--executedocuments how to get a variable into a shell body: each position is substituted verbatim, so a variable spliced intosh -ctext is re-parsed by that shell and splits on spaces. The help now shows passing it as a separate argument and referencing it positionally. (#3977) -
The clone-local default-branch override is documented:
wt config state default-branch setwritesworktrunk.default-branchto the clone's local git config — machine-local, never committed, and shared by every linked worktree.setwarns when the branch isn't checked out locally, andwt listskips its comparisons until it is. (#3947, #3948, fixes #3946, thanks @danielo515 for the request) -
The docs site renders code with the CLI's own colors: terminal examples reproduce each snapshot's exact ANSI roles, while command references and the homepage command comparison get clap and shell syntax color from paired light/dark themes. File excerpts gain a path tab, and code wraps instead of scrolling on mobile. (#3936, #3938, #3939, #3942)
-
The
pre-startupstream example uses--base: it showedwt switch --create feature origin/feature, whichwt switchrejects. (#3955)
Internal
-
Library API rework (Breaking library API):
cargo-semver-checksfails ten lints —RemoteRefProviderand its provider structs give way toForgeKinddispatch, the--executerework retires the fish and PowerShellShellEscapeModeescapes andWORKTRUNK_SHELL_ENV_VAR,BranchRefswapsfull_ref/worktree_pathforid, and accessors includingBranch::unset_upstreamandProjectConfig::ci_platformare gone. (#3913, #3926, #3949, #3963, #3977) -
Hook pipelines and
wt listcollection lost two indirection layers: the background pipeline carriesPreparedStepdirectly, and 15 marker task types collapse into one exhaustive dispatch. The synthesized---forcesubmodule path keeps its dirty re-check and destructive command under one lock, so prune's narrower locking doesn't widen that window. (#3964, #3954)
0.75.0
Improved
-
The
wt switchpicker opens on one unified diff: local rows combine committed, staged, unstaged, and untracked changes. Tab skips empty subsidiary views, whileAlt-1throughAlt-8retain direct access. Docs (#3865) -
Git 2.43 is now the minimum supported version: older Git exits with an upgrade message before Git-dependent commands run;
wt config shellremains available so shell startup continues. (Breaking.) (#3895)
Fixed
-
wt listno longer grows.git/objectsduring advisory conflict checks: untracked content stays visible but is excluded from synthetic trees, and all probe-only objects use temporary storage. (#3906, fixes #3883, thanks @srobroek for reporting and the original fix) -
HEAD±counts untracked files without inflating moves:wt list, the picker, and statusline include untracked lines. Tracked deletions paired with untracked destinations count as renames, so pure moves are line-neutral and edited moves show only their edits.HEAD±now always detects renames, regardless ofdiff.renames. (#3925) -
Timed child processes no longer abort
wtin restricted sandboxes:wt switch --create, picker pagers, and other bounded commands survive denied signal-handler wakes. TERM→KILL cleanup also returns promptly once the process group is gone. (#3857, #3887, fixes #3856, thanks @tomascamargo for reporting) -
JSON list output ignores display-column gates:
[list] columnsno longer makeswt list --format jsoncontact a forge or generate summaries. CI requires--full; summaries also require[list] summary = trueand a configured generator. (Breaking: schema 1 loses config-drivenciandsummaryfields.) (#3812, thanks @emeren for the request) -
Long Windows paths compare consistently: paths beyond 260 characters could retain a
\\?\prefix and appear to be on another drive.copy-ignoredthen refused them, while switch, remove, merge, and relocate landed at the worktree root instead of the original subdirectory. (#3899, fixes #3898, thanks @Persedes for reporting and verifying the fix) -
Shell configuration rechecks before it writes: overlapping installs lock and reread rc files; Fish completion installs preserve files created after preview; uninstall applies only previewed rc removals and rejects changed Worktrunk-owned files. (#3853, #3924)
-
-vvprofiles exclude their own collector commands: command counts and cache summaries no longer include duplicate-looking work performed only to assemble the diagnostic report; raw traces still retain it. (#3900) -
Fenced HTML comments survive picker Markdown rendering: PR descriptions and comments now preserve fenced
<!-- … -->lines; fenced<!-- wt list … -->markers also no longer affect the following block. (#3908)
Documentation
-
Agent CLIs without a plugin can publish activity markers: the integration guide now specifies the session-start, turn-end, and session-end calls, working-directory requirement, error guard, and cleanup contract. Docs (#3848, thanks @AsafMah for requesting generic-agent guidance and @ortonomy for the related Pi use case)
-
Agent guidance explains worktree selection: commands that name a branch already select its worktree;
-Cchanges repository context and is needed only for commands without a worktree selector or callers outside the repository. (#3890) -
The
wt uprecipe safely updates dirty worktrees: it fetches all remotes, fast-forwards dirty branches without autostash, rebases clean branches, and continues past an ordinary refusal or a failed remote. Docs (#3882) -
The docs site has a new responsive design: rebuilt on Astro and Starlight while preserving public routes, anchors, and crawler URLs; generated reference pages remain synchronized. (#3866)
Internal
-
Library API rework (Breaking library API):
BranchDiffSpecgainedworking_base, while remote-URL, shell-path, branch-push, approval, temporary-index, and repository helpers were removed. (#3833, #3853, #3865, #3866, #3875) -
Codex loads repository maintainer skills:
.agents/skillsnow exposes the canonical.claude/skillstree; checkouts without symlink support keep the existing limitation. (#3903)
0.74.0
Improved
-
A JSON bool or null nested in a
varsvalue renders asTrue,False, orNone: minijinja 2.22 adopted Jinja2's spelling for all three, so{{ vars.<key>.<field> }}into an object set bywt config state vars setprints those words. A var whose whole value istruestays a string. (Breaking: the old spellings weretrue,false, andnone.) (#3795) -
wt config approvals add --yesrecords approvals without a terminal: the command refused every non-interactive run, so a container or CI job could not pre-approve a project it had just cloned.--yesnow lists what it trusts and writes it, and a save it cannot make fails the command rather than warning and exiting 0. (#3819) -
remote_reponames the repository as the remote spells it:repois the directory on disk, so a renamed clone reports the new name.{{ remote_repo }}takes it from the primary remote's URL, available everywhereowneris and unset when no remote parses. (#3745, thanks @canac)
Fixed
-
wt remove --forcedeleted a live worktree moved onto another's registered path: the ownership gate asked which repository the occupant belonged to, which a sibling worktree of this repo satisfies, so the directory went to trash with its uncommitted work. The directory and the registration must now name each other, asgit worktree removerequires. Also reacheswt merge,wt step prune, and picker removal, and refuses a bare repository's path that--forcepreviously deleted. (#3808) -
wt step copy-ignoredskips a source file that vanishes mid-copy: the walk collects leaves, then copies them in parallel, so a file removed in between — a concurrent build rewritingtarget/, say — aborted the batch, under--forceafter deleting the destination. The source is stat'd before anything is removed now. (#3744, fixes #3743, thanks @dataders for reporting and fixing) -
wt step promoterefuses to delete a source it did not fully copy: across filesystems the move copies then deletes, and an entry with no copy — a socket or FIFO, or a file a concurrent build rewrote mid-walk — was deleted anyway. The copy now reports what it skipped, and a non-zero count fails the move. (#3820) -
An
ssh://remote's host comes from the authority, not a later path segment: the search for the userinfo@covered the whole remainder, sossh://git@attacker.example/owner/repo@github.com/org/repo.gitresolved togithub.comand borrowed that repository's approvals while git dialed the attacker. Every URL form now splits the authority first, so@in a namespace parses instead of being rejected. (#3813) -
WORKTRUNK_*env vars and--config-setoutrank a[projects."…"]entry: layer and specificity resolved separately, so a project entry answered for the global key whichever layer set it —WORKTRUNK_WORKTREE_PATHcould not override a project'sworktree-path. Both invocation layers now apply at either scope, and the help text gained a precedence section. (#3790, fixes #3788, thanks @jeremy0dell for reporting) -
Shell completions register under the
--cmdname:wt config shell init zsh --cmd wotrenamed the wrapper, but clap emitted the registration underwt. Bash and zsh completed nothing and regenerated the script on every TAB, PowerShell registered the wrong command, and zsh'scompdefhanded worktrunk's completer to the otherwt. (#3817, fixes #3816, thanks @sbarre for reporting)
Documentation
- The picker's
Alt-xnever forces: the keybinding table readRemove selected worktree/branch, which sent a reader looking for a force-remove that isn't there. (#3811, raised in #3809, thanks @josh-burton)
Internal
- Library API rework (Breaking library API):
GitError::WorktreePathNotOursgained anoccupant_registered_atfield, andWorkingTree::ensure_belongs_to_repois nowensure_holds_this_worktree. (#3808)
0.73.0
Fixed
-
wt removerefuses a registered path that now holds a different repository: A clone made at a stale registration's path was removed whole, uncommitted work included — and wt routed the user there, since the dirty gate read the occupant's files as this worktree's and offered--forceas the cure. Removal now compares the directory's git dir against this repository's;--forcewaives uncommitted changes, not the check for whose directory it is. (#3785) -
The Nix flake names three systems, dropping Intel macOS: nixpkgs drops
x86_64-darwinin 26.11, so the flake would stop evaluating there onceflake.lockadvances past it. (Breaking:nix build,nix run, and the home-manager module no longer resolve on Intel macOS. Release binaries are unaffected and still ship for it.) (#3776) -
NO_COLORand a redirected stream reach every surface: Escapes still landed in progressivewt list,-vdiagnostics, clap's error tips, and parts of stderr — the deprecation warning, thewt config updatepreview, the[y/N]prompt — so a redirected log carried escapes on one line and not the next. Color now resolves in one place. (#3777, #3771) -
A trailing separator no longer hides the branch: Git's ref format forbids a name ending in
/, and shell completion produces exactly that spelling whenever adocsdirectory sits beside the branch, sowt switch docs/never had a candidate. Selectors are normalized before resolution now —wt remove, merge targets,--base, and thepre-switchhook'stargetalike. (#3785) -
A path holding no worktree is reported as a path:
wt remove ../repo.ghostansweredNo branch named …and pointed at a listing it could never appear in;wt switchandwt mergeoffered--create <path>, which git rejects; andwt config state marker set --branch <path>silently stored state keyed by a path. (#3773, thanks @judewang for reporting) -
A failed GitLab project lookup carries
glab's own verdict:wt switch mr:<n>on a fork MR answered a 401, a 404, and a network failure with the sameFailed to fetch project 456, swallowing the output every otherremote_reffailure path forwards. A non-project response body now says so too. (#3799) -
A deleted-and-recreated worktree directory is reported, not leaked as git's exit 128: An
rm -rffollowed by amkdirpasses thePath::exists()probe, sowt switch,wt merge,wt step push, andwt removewalked into a raw git failure. All four now name the missing worktree and thegit worktree prunethat clears it. (#3785)
Internal
-
Library API rework (Breaking library API):
cargo-semver-checksfails four lints —GitErrorgainedWorktreeNotFoundAtPathandWorktreePathNotOurs, shifting seven later discriminants, itsDetachedHeadvariant gained aworktreefield,ResolvedWorktreegainedNoWorktreeAtPath, andRepository::resolve_worktree_namewas removed. (#3785, #3773) -
Tests and benches spawn a pinned
wtbinary: a concurrentcargoremoves and recreatestarget/debug/wtas it uplifts, so spawns hit a one-offNotFound; every spawn now routes through a hardlinked pin. (#3784, #3792) -
The nix devShell and
task setup-webinstall what the test suite drives: both were missingnushell,pwsh, andjq, which--features shell-integration-testsshells out to. (#3768, #3776) -
Benchmark fixtures reduced to two provenance-based bases:
Generatedbuilds a repository locally andImportedcopies the pinnedrust-lang/rustcorpus, with worktree, branch, and remote-ref populations as parameters. (#3761)
0.72.0
Improved
-
A host carrying a forge's name anywhere resolves to that forge again: 0.71.0 required
github,gitlab, orgiteaas a whole dot-separated label, which read as an ownership check but wasn't one — an attacker controls their own DNS — while shutting out self-hosters with hyphenated names.github-enterprise.acme.com,mygithub.com, and thegithub-personalSSH alias classify again, so CI status,wt switch --prs, andrepo.providerwork with no config. (#3673) -
One
[projects."…"]entry can cover every repository on a host, and can set the forge: A key containing*matches any run of characters,/included, so[projects."git.company.example/*"]covers a whole host; every matching entry applies, least- to most-specific. The table also gainedforge.platformandforge.hostname, so a self-hosted host needs one entry here rather than a block in every repo. Docs (#3701, thanks @chrishas35 for the request and @witt-bit for the workspace-scoped case it partly serves) -
wt mergeandwt step pushleave the target worktree's uncommitted changes in place: The autostash that held a dirty target's changes restored them as unstaged; both now advance the target with a compare-and-swapupdate-refandread-tree -m -u, which leaves uncommitted work untouched. (Breaking: the fast-forward path no longer runsgit push, sopre-pushand the receive-side hooks no longer fire, and a failed sync errors with the ref rolled back.) (#3703, #3684, #3693, thanks @gubasso for reporting) -
Approval state and branch-removal outcomes are machine-readable:
wt config approvals list --format=jsonreports whether a non-interactive run would stop for approval.wt removeandwt step prunereplacebranch_deletedwithbranch_outcome:deleted,deferred,not_attempted,retained_unmerged,retained_checked_out,retained_raced,retained_failed. (Breaking.) (#3710, thanks @NathanaelRea for the requests) -
Every commit hash worktrunk prints follows
core.abbrev: Thewt listtable,wt switch --prs'slogtab, andwt config state's CI cache table sliced to 8 characters while--format=jsoncarried git's%h. All now ask git how wide it abbreviates in this repo. (#3676, #3677) -
wt list --format=jsonschema 2 has a published JSON Schema: worktrunk.dev/schema/list-v2.json holds the contract, andwt list --print-schemaprints the same document. Four fields that were bare strings are now enumerated vocabularies; the emitted JSON is unchanged. (#3747) -
A detached worktree is named by its commit, not
-: The Branch cell hardcoded-, which reads as missing data rather than a state; it now carries the row's abbreviated HEAD, and the picker and statusline name the worktree the same way. (#3675)
Fixed
-
Piped output is plain and no longer panics:
wt list | head -3exited 101 withfailed printing to stdout: Broken pipe, andwt listwrote ANSI to a pipe unconditionally. Every stdout surface now exits cleanly, and the human-read ones are plain when piped unlessCLICOLOR_FORCE=1. (#3746, #3766) -
A CI check that hasn't finished no longer reads as passed: Each forge's status parser missed documented values, so a GitHub PR parked on an approval gate showed green, and GitLab's
cancelingand Azure DevOps'spostponedread as no CI at all. (#3741, #3740) -
The shell wrappers survive an
rmalias and a failing--execute: Aliases bake into the wrapper at parse time, soalias rm='rm -v'reached its cleanup — noise on zsh and bash, and on nushell an abort that leaked three temp files, as a failing--executebody also did. (#3714, thanks @Ar4l), (#3732, #3734) -
An alias or hook wrapping
wt switchorwt removekeeps your subdirectory: The user's position came from thewtprocess's cwd, which inside an alias body is the worktree root, so an aliasedwt removefromfeature/apps/gatewaylanded at the primary worktree's root. Fixes #3723. (#3724, thanks @vivienm for reporting) -
wt mergeandwt step pushrefuse a target worktree parked mid-operation: The two-tree sync refuses an unmerged index but not a stopped cherry-pick or rebase whose conflict was already staged, so the push range could land in a paused target and be committed by--continue. (#3759) -
The Claude plugin's worktree-remove hook resolves against the worktree path: The hook anchored at
CLAUDE_PROJECT_DIR, which theclaude agentsview routinely leaves outside any repository, sowt removedied withnot a git repositoryand the session became undeletable. Its guard now also requires a.gitentry. (#3754, #3767, thanks @judewang for the fix and the report) -
A Gitea API error is reported as one, not as a parse failure:
tea apiexits 0 whatever the HTTP status, so both call sites guessed from the body's shape and blamed an API change for an API error.--includesurfaces the status instead. (#3713, #3600) -
--print-schemaand the doc-generation help flags name the right command: All three found the subcommand by scanningargvfor a/wtsuffix, which never matcheswt.exeunder a backslash path, so on Windows they read the binary's own path as the command. (#3762) -
A multibyte shell name no longer panics:
extract_filename_from_pathsliced atlen() - 4to test for.exewith no char-boundary check:SHELL=/bin/日本語 wt config showpanicked, and on macOS every process name goes through it during shell detection. (#3727) -
wtinstalled under a dotted name generates shell integration for that name:binary_nameusedfile_stem, which cuts at the last dot, sowt config shell init bashunderwt.oldemitted a wrapper forwt. It now strips only the executable suffix. (#3719) -
Concurrent
wt step pruneremovals no longer race the worktree registry:git worktree removereads every sibling under.git/worktrees/, so two overlapping removals could have one read a sibling mid-teardown. Registry-mutating removals now serialize behind a second lock. (#3692) -
The
wt switchfirst-run offer previews the legacy files it removes: Accepting "Install shell integration?" could delete a deprecated worktrunk-managed wrapper the prompt never named. What gets removed is unchanged. (#3656) -
wt config create --projectwrites a resolvable link: The comment it writes into.config/wt.tomlcarried a raw Zola target, because the link-conversion regex stopped at the first]— here the one closing a nested code span. (#3731) -
wt list --branchescounts a local branch containing/as local: The summary tally classified branch-only rows bybranch.contains('/'), so a localfeature/logincounted under "N remote branches". (#3687) -
wt step relocate's human summary counts template-error branches as skipped:--format=jsonalready folded them intoskipped; the human tally undercounted by the number of branches whoseworktree-pathtemplate failed to expand. (#3688)
Documentation
-
SignPath attribution appears with the artifacts it describes: The code-signing notice and a route to the policy now sit in the install section's Windows block on the README and the docs landing page, as SignPath Foundation's OSS program requires. (#3709)
-
wt step copy-ignored's--require-includeexample renders as a terminal block: It was the onlyconsoleblock in the command's long help missing the$prefix. (#3706)
Internal
-
Library API rework (Breaking library API):
cargo-semver-checksfails five lints —LegacyForgeAliasandRepository::legacy_forge_aliasremoved with the forge-classification revert,Repository::forge_platform_overrideremoved for one shared resolver,stage_worktree_removalgained two parameters,UserProjectOverridesgained aforgefield, and thereal-repo-benchesfeature was removed. (#3673, #3694, #3701, #3721) -
One function owns the pre-removal gate:
stage_worktree_removalnow performs the dirty-worktree gate, the fsmonitor stop, and the rename into trash for both removal paths. (#3694) -
The test mock stub is the
wtbinary itself:cargo test --test integrationnever rebuilt the separatemock-stubpackage;wtis now linked under each mock's name instead. (#3712) -
-vvtraces report a normal run's fork count:prewarm_atfast-pathed on a cache key that-vv's own logging setup had populated, so every trace overstated a normal run's config forks. (#3705) -
Benchmarks are organized by repository shape: Groups select semantic
FixtureRecipes and share table-driven cases; separate fixtures remain only for a controlled contrast, a destructive precondition, or disproportionate setup cost. (#3721) -
azandteaspans group undernetworkinwt-perf timeline: The trace exporter'snetworkbucket recognized onlyghandglab. (#3689)
0.71.0
Improved
-
Forge hosts are classified by exact DNS label: Host detection matched the bare substring, so
githubanywhere in a hostname selected the GitHub provider —evil-github.comanddev.azure.com.attacker.exampleincluded. Classification now requiresgithub,gitlab, orgiteaas a whole dot-separated label (Azure DevOps matchesdev.azure.com/visualstudio.comas a domain suffix), and userinfo is split at the final@, sohttps://github.com@attacker.example/owner/reporesolves toattacker.examplerather than GitHub. Ports and a trailing DNS root dot are normalized away first, so both transports classify identically.github.mycompany.comstill resolves, but a host that carries the forge name only as part of a label no longer does —gitlab-internal.company.com,github-mirror.example.com, andmygithub.comall become unknown, alongside the lookalikes the change targets. What is lost is forge-derived detail: CI status inwt list --full,wt switch --prs, andrepo.providerinwt list --format=json, which readsunknown. Settingforge.platformin.config/wt.tomlrestores all of it, andwt switch pr:{N}keeps working either way.Only one case announces itself — a single-label SSH alias of the
github-personalform, the shape used for multi-account setups:SSH host alias github-personal is not auto-detected as a forge; enable CI status and wt switch --prs with forge.platform = "github" @ .config/wt.toml. A self-hosted instance at a hyphenated hostname gets no such message; it simply stops reporting CI. Going the other way, a recognized Gitea or Azure DevOps remote now knows its own change-request vocabulary, sowt switch 123against a missing branch suggestswt switch pr:123instead of offering bothpr:andmr:forms. (Breaking: a remote whose host merely contains a forge name is no longer detected. Setforge.platformin project config.) (#3662) -
The retired single-file shell directive is no longer honored: Wrappers written before 0.37 passed one
WORKTRUNK_DIRECTIVE_FILE, mixing a trusted directory path with arbitrary shell for the parent to source. The splitWORKTRUNK_DIRECTIVE_CD_FILE/..._EXEC_FILEpair replaced it in 0.37, the fallback has warned since 0.54, and it is now gone. A shell still running the old wrapper fails closed on both halves:Cannot change directory — shell wrapper is out of date, with the repair lineTo update the shell wrapper, run wt config shell install, and--execute disabled because the shell wrapper is out of datein place of running the payload. That makes a stale wrapper stricter than no wrapper at all, where--executestill runs the command directly — deliberate, since a retired wrapper is the one case where worktrunk cannot tell what the parent will do with what it writes. Child processes can no longer inherit the retired capability. (Breaking: a live shell on a pre-0.37 wrapper stops changing directory and stops running--executeuntilwt config shell installand a shell restart; nushell needs the reinstall because its wrapper is a static file.) (#3662) -
Every hyperlink is underlined: The statusline underlined its PR reference but not the dev-server port, so nothing marked the port as clickable. Both now route through one helper that emits the OSC 8 sequence and the underline together, closing with
[24mrather than a full reset so a wrapping color or a dim survives the link. Text that is not a link stays plain: the picker cannot carry OSC 8 through skim, so it prints the same short text with no underline instead of underlining something unclickable, and on a terminal without OSC 8 support a dev-server URL still prints in full and stays copyable. (#3643, #3651) -
Shell completion answers a Tab press in roughly half the time: Completion forks a whole
wtprocess on every Tab press, with the user's finger still on the key, and it has nowhere to hide work — nothing paints progressively, no cache survives the process, and no shell caches a dynamic completion. It was doing every git read in sequence. The reads now overlap, the repository caches prewarm on this path,refs/remotes/is not scanned where a remote-only branch can never be offered (wt removeand worktree-only arguments), and the handler returns beforemainbuilds a rayon pool it never reaches.wt switch <Tab>goes from 45.7 ms to 27.4 ms on a fixture of 80 worktrees and 1400 remote-tracking refs; on worktrunk's own checkout, 47.9 ms to 29.0 ms, andwt <Tab>from 22.2 ms to 12.8 ms. (#3664) -
wt step pruneprunes stale metadata alongside its other removals: Stale-detached and not-yet-planned prunable candidates were held on the scan lock's write side because the repo-widegit worktree prunethey used to call raced sibling prunes. Each call now names its own entry, so both shapes join the read-side fan-out; the write side keeps only the candidates that run arbitrary commands or own the terminal — hook-bearing ones,--foregroundones, and the deferred current worktree. (#3655)
Fixed
-
Removing one worktree no longer unregisters another: Every stale-metadata cleanup in the removal chain ran a bare
git worktree prune, which takes no path filter — it walks every entry in.git/worktrees/and unregisters each one whose directory it cannot find at that instant. A worktree that is merely absent right now (an unmounted volume, a dropped network mount, a half-finishedmv) is indistinguishable from a deleted one, so removing worktree A also unregistered bystander B. B's commits and files survive, but its admin directory does not: the index,ORIG_HEAD, the per-worktree reflog,refs/worktree/*andrefs/bisect/*, and any in-progress rebase or merge go with it — andgit worktree repaircan relink an admin directory but cannot recreate one. Each cleanup now names the entry it means to remove, so a removal's blast radius equals its intent. (#3650) -
A locked worktree whose directory is missing is no longer removed: The missing-directory fallback was tested before the lock guard, so a worktree that was both locked and currently absent skipped the lock entirely — its registration was pruned and it fell through to branch deletion, which is precisely the case
git worktree lockexists for (removable media, a network mount, a dropped VPN). The lock is now honored whether or not the directory is present, and--forcestill does not override it, matchinggit worktree remove. Fixes #3645. (#3647) -
A branch that gains a checkout just before deletion is retained: Branch deletion read worktree topology from cache, so a worktree registered after that read — by a hook, or a concurrent
wt switch— was invisible, and the ref was deleted out from under it, leaving that worktree unable to resolveHEAD. A fresh topology read now sits immediately before the compare-and-swap delete, and a branch found checked out (live or locked) is kept and reported. Git exposes no transaction spanning worktree registration and ref updates, so the window cannot be closed entirely; it is now as narrow as two adjacent commands allow, and either one failing leaves the branch intact. (#3662) -
Planning a removal no longer prunes metadata as a side effect:
prepare_worktree_removalpruned stale worktree metadata while planning the missing-directory fallback, so every "validate first" path quietly mutated — a refusedwt removeof the default branch still pruned its entry,wt removepruned before its approval prompt, and the picker pruned on skim's event loop during row validation. Planning is now a pure read, and the prune happens at execution, ahead of the branch deletion that may still be declined. Two related contracts move with it: a failing metadata prune surfaces as a removal failure and abandons the rest ofwt step prune's queue instead of being skipped silently, and--dry-runplans stale entries through the same gate as every other candidate, so a dirty prunable-but-present entry is correctly left out of the preview. (#3658) -
wt config shell installpreviews and confirms the legacy files it reclaims: Install reclaims its own legacy wrapper paths — the fishconf.d/{cmd}.fishand stranded nushell autoload copies — but the deletion never appeared in anything the user saw before it happened.--dry-runreturned an empty cleanup list, the?preview showed only the files to add, and when everything was already configured the cleanup ran and returned without prompting at all. Both previews now renderWill remove <old> (deprecated; now using <new>), and the already-configured branch prompts before removing and preserves the file on decline.--yesstill skips the prompt, so no non-interactive flow changes. Fixes #3644. (#3648) -
Completion offers remote branches in a stable order: They were grouped through a
HashMapand sorted by timestamp with a stable sort, so on tied timestamps the order was whatever that process's hashing produced — three consecutive completions could print three different orders. Ties now break on name. (#3664) -
The Codex plugin clears its activity marker when the session ends: The marker returned to idle only at turn end, so one set by a session's last turn outlived the session. Codex now exposes
SessionEnd, and a main-session exit hook clears the marker there, within Codex's three-second maximum hook timeout. (#3660)
Documentation
wt switch --executedocuments what{{ base }}resolves to without--create: The help text said{{ base }}and{{ base_worktree_path }}require--create; they describe the source either way — the selected base with--create, or the invoking worktree when switching to an existing one. The picker'scommentspreview tab was likewise documented as--prs-only when it loads for any row whose branch has a PR/MR. Both are help-text corrections; neither behavior changed. (#3663)
Internal
-
Library API rework (Breaking library API):
cargo-semver-checksfails ten lints, all from the forge, shell, and removal consolidation — removed traitworktrunk::git::RefContext, enumworktrunk::git::CiPlatform(superseded byForgeKind), functionworktrunk::shell::detect_zsh_compinit, constDIRECTIVE_FILE_ENV_VAR(renamedRETIRED_DIRECTIVE_FILE_ENV_VAR), methodsRepository::prune_worktrees,Cmd::directive_legacy_file, andGitRepoProvider::from_platform, and fieldRemoteRefInfo::ref_type;RemoteRefProvider::platform_labelreplaced byforge_kind;Repository::branches_for_completiongained a parameter; andBranchDeletionOutcomegained aRetainedCheckedOutvariant. (#3662, #3650, #3664) -
The integration suite stopped leaking a temp directory per test:
isolated_test_cwd()held itsTempDirin aLazyLock; statics do not run destructors at process exit, and nextest runs one process per test, so every test leaked an empty directory into the system temp root — 704 per integration-suite run, and one machine had accumulated 454,907 entries. Stale entries are cheap to ignore but expensive to enumerate, andgit::recover::recover_from_pathreads every ancestor of a deleted CWD: one recovery test took 14.2 s against that temp root versus 0.27 s against an empty one. One fixed directory replaces the leak, fixtures root under$TMPDIR/wt/rather than the shared temp root, andtask profile-testsmakes the measurement repeatable. (#3604, #3666) -
The integration suite converged on distinct contracts: Two tranches removed duplicated and false-confidence coverage — overlapping list-layout suites, command-by-shell wrapper cross-products, and setup variants that never reached the behavior they named — and cut the switch picker from 42 PTYs to 19. Snapshots go from 1,259 to 1,131, a warm all-feature run from 84.7 s to roughly 73 s, and comparable production-line coverage is essentially unchanged at 97.3% to 97.2%. (#3657, #3663)
-
Benchmark fixtures and harness are shared: Each benchmark target had reimplemented temporary-repository ownership, linked-worktree paths, subprocess isolation, and warm/cold loops; a shared
FixtureRepowith canonical command and cache helpers replaces that, leaving each scenario's workload and destructive lifecycle explicit. The completion benchmark now fails when its measured subprocess fails instead of silently timing it, and its two fixture ids collapse into onemixedthat reaches the completer's 100-candidate threshold. (#3669, #3664)
0.70.0
Improved
-
wt step pruneremoves worktrees far faster: Each removal ran a serial chain of ~17 git subprocesses under the scan write lock, re-preparing a plan the scan had already computed and re-stating the worktree right after the fsmonitor daemon stop. The chain is now one check per guarantee, reusing the scan-time plan, and removals run concurrently on the scan lock's read side — the write side is kept for the candidates that need it (hook-bearing,--foreground, metadata-pruning, and the current worktree). The documented rust-scale live prune of 24 candidates goes from ~12 s to ~0.6 s wall, and theprune_e2e/livebenchmark from ~620 ms to ~400 ms. (#3617, #3631) -
wt step prune --format=jsonis ordered, and a failed removal aborts the rest: Live JSON output is now sorted by scan index, matching--dry-run, with the current worktree last. The first failing removal drains the remaining queue unexecuted, matching the serial loop it replaced; in-flight removals complete. (#3631) -
A worktree can be named by its path wherever a branch is accepted: Every argument that takes a branch now also accepts the worktree's own path, resolved after the branch so a directory never shadows a branch sharing its name. A path names what a branch cannot — a detached worktree, or one of two checkouts of the same branch. Relative paths resolve against
-Cand a leading~against the home directory, so a path worktrunk printed can be pasted back. Docs (#3607) -
wt listflags a branch checked out in more than one worktree: Such a branch resolves to whichever worktree git lists first, so every worktree on it now carries⚑—worktree.state"duplicate_branch"in schema 1, aworktree.duplicate_branchboolean in schema 2. The flag makes the ambiguity visible in the listing; resolving such a branch from any command warns separately and names a duplicate to drop. (#3480, #3606) -
wt switch --executecomputes only the template variables its command names: The switch path built every variable the template context could hold before rendering; it now resolves just the ones the command references. On a clone with noorigin/HEADand no cached default branch, that removes agit ls-remotethe command never asked for — 13 subprocesses and one remote query down to 8 and none. (#3628)
Fixed
-
A branch checked out in a second worktree is retained on removal,
-Dincluded:wt removeandwt step prunenow act on the worktree named rather than the branch's first checkout, and all three ofwt remove,wt step prune, andwt mergekeep the branch while another worktree still has it out — deleting the ref would leave that worktree unable to resolveHEAD, which is whygit branch -drefuses the same delete. The retention is reported and names the surviving checkout rather than passing silently. (#3533) -
Removal reports what it took, not what it selected: A removal's summary and JSON described the plan, so a worktree candidate whose branch was retained still counted as
✓ Pruned 1 branch, andwt remove --format=jsonreported"branch_deleted": truebeside a stderr line saying the branch was kept. Execution now returns the branch's fate;wt step prunecounts executed outcomes (--dry-runincluded), both JSON payloads gainedbranch_deleted, and a declined orphan deletion drops out of the removed list rather than being reported as removed. (#3633, #3637) -
Hook previews expand every variable except
vars.*: Onevars.token disabled expansion for the whole command, sowt hook show --expandedandwt hook <type> --dry-runprinted{{ branch }}and{{ repo }}raw in a listing whose job is to show the expansion. A preview now substitutes a stand-in that renders eachvars.*reference back as itself, nested access included, while every other variable expands — and no longer spawns the git read that resolvingvarsrequired. The listing is also derived from the execution path itself, so a context key added there reaches the preview with no second edit.wt config alias dry-runshares the renderer, so its help text — which still described the all-or-nothing behavior — was corrected to match. (#3635, #3638, #3639) -
wt hook showno longer prints a bare heading for an empty command list: A hook type declared aspost-switch = []has a config entry but no commands, and the section decided it had printed something from the entry rather than from the rows — so it emitted its heading and stopped, and the(none configured)line never appeared. Both the user and project sections carried the bug, since the loop and the fallback were duplicated; they now share one renderer that reports whether it wrote any rows. The execution path was already correct: an empty list announces nothing and is omitted from JSON. (#3641) -
wt config shell installreclaims its own legacy wrapper paths: Fish sourcesconf.dat startup, so a staleconf.d/{cmd}.fishwas already loaded by the time fish would autoload thefunctions/{cmd}.fishthe install had just written — the old definition won and the new wrapper never loaded. Install decided ownership by reading the file, and left anything unrecognized in place. Ownership now comes from the path:conf.d/{cmd}.fishand the stranded nushell{cmd}.nucandidates are paths worktrunk computes for the command being installed, so it takes them back whole, unread. Only that exact filename is touched — a neighbour under another name is not worktrunk's — and each removal is reported.wt config shell uninstallstill reads the header, because it takes no--cmdand so cannot know the name; it prompts and previews every file first. (Breaking: install now removes a file at those exact paths regardless of its contents.) (#3602) -
Command timeouts actually bound wall-clock, and a default branch guessed while the remote was unreachable isn't cached: A timeout killed only the direct child, so a surviving grandchild held the output pipe open and the call ran on regardless — a 3 s bound measured at 120 s. A timed command now runs in its own process group and the whole tree is torn down on expiry, which fixes every existing bound including the fsmonitor and reap probes. On top of that, nothing in git bounds
git ls-remote(an unreachable host costs ~127 s per address on Linux), so default-branch detection abandons the query after 10 s and falls back to local inference — without caching the result, so an outage can't make an inferred default branch permanent. (Breaking: because a timed command gets its own process group, Ctrl-C no longer reaches it; the command waits out the remaining bound.) (#3603) -
wt step relocateno longer strands a worktree in its staging directory: When worktree A's target was held by worktree B, and B was itself blocked by a non-worktree path without--clobber, the dependency loop read the stall as a cycle, temp-moved A into.git/wt/staging/relocate/, then failed moving it into the still-occupied target — leaving A at neither its original nor its expected path. A worktree blocked by an immovable occupant is now skipped. (#3530) -
Forge CLI failures are classified by response shape, not by the tool's prose:
tea apicopies the response body to stdout and exits 0, so an HTTP error never tripped the exit-code gate — a GiteaAPIErrorbody deserialized into{state: "", total_count: 0}, indistinguishable from a commit with no CI statuses, while the PR-list path blamed an API change for what was an API error. Failures fromgh,glab, andteaare now keyed on the response envelope, and a non-zero exit keeps meaning the tool itself failed; the CLI's own error text is forwarded rather than reworded, so a bad token surfaces asgh: Bad credentials (HTTP 401)instead of a suggestion to re-authenticate.wt config show --fullreports the Azure DevOps CLI extension alongside the other forge tools. (#3595, #3597, #3605) -
wt config plugins claude install-statuslineno longer mistakes another tool's statusline for its own: The check for an existing worktrunk statusline matched the bare substringwt, which an unrelated command likenewt statussatisfies — sowt config showreported a foreign statusline as worktrunk's, and the installer early-returned "already configured" and refused to install. It now matches the adjacentlist statuslinetoken pair, so it works whether the binary iswt,git-wt, or an absolute path. (#3595) -
[list] task-timeout-msis removed: The per-command bound is gone;[list] timeout-msbounds the whole collect phase. A config that still sets it warns, andwt config updatestrips the key — in the top-level, project-scoped, and inline-table forms. (Breaking: the setting no longer has any effect. It was also, until this release, ignored for a command that carried its own bound.) (#3613, #3615)
Documentation
-
wt removedocuments what retention protects: The branch-cleanup section's six conditions all ask whether deleting loses work; a branch checked out in a second worktree fails a different test, and the page now says so, alongside a new "Naming a worktree" section onwt switch. The FAQ's advice togit worktree locka worktree holding precious ignored data now says what the lock actually buys — it blocks removal, and nothing else;wt mergeandwt step pushwill still overwrite an ignored file the incoming commits track. (#3533, #3601, #3607) -
Default-branch detection documents the remote-query bound: The detection ladder records that the
git ls-remotestep is abandoned after 10 s, that local inference covers the abandoned case, and that this is the one result not cached. (#3603) -
wt step pushrecords that destination-worktree safety matches git: Pushing overwrites an ignored file in the destination whose path the incoming commits track, exactly as agit mergerun there would; the spec now says the match is deliberate. (#3614)
Internal
-
skim is back on a current release: The picker was held at skim 5.1.0 because 5.3.1 regressed the legacy Windows console — every picker PTY test failed on the keyboard progressive-enhancement path. 5.5.0 guards it, so the pin moves forward. (#3626)
-
Coverage uploads on every
maincommit: The coverage job shared one concurrency group with a sampled job that held it for ~2 h, so a queued coverage run was usually cancelled by the next merge — 29 of the last 40maincommits uploaded no report, which left later PRs comparingcodecov/patchagainst a stale base. CI is now split by cadence into three workflows (cievery commit,coverageevery commit keyed per-sha,affectedsampled), so a merge train can't cancel a queued upload. (#3608) -
/wt-switch-createcreates the worktree by name and cleans up an untouched one: The shipped Claude skill now creates throughEnterWorktree({name})for the common case, falling back to a path, which avoids a confirmation prompt on recent Claude Code versions. A worktree created that way and never touched — no changed files, no commits — is removed when the session ends, branch included; anything written into it keeps it, and a worktree entered by path always stays. (#3636)
0.69.2
Improved
wt removeresolves every fsmonitor daemon in onelsofcall: The end-of-command sweep forked onelsofpergit fsmonitor--daemonon the machine — withcore.fsmonitorenabled globally that is one daemon per repo ever touched, routinely over a hundred — and the spawn cost compounds under load rather than staying fixed per call. All daemons now resolve in a single call: 108 spawns to 1 on a live machine. (#3581)
Fixed
-
Shell integration no longer deletes user data that merely quotes the init command: Three places decided whether text was worktrunk's by testing a blob for substrings, and two of them deleted on a wrong guess.
wt config shell uninstallremoved an rc line likealias setup='echo "run: wt config shell init fish | source"';wt config shell install's legacy cleanup deleted a user's own~/.config/fish/conf.d/wt.fishoutright when the file happened to mention the init command. Both now require the command name in command position, not merely somewhere in the text. (#3589) -
Forge detection matches host labels, not substrings:
wt switch pr:<n>and friends picked their provider withhost.contains("github")/contains("dev.azure.com"), so a host that only spells the name inside a longer one —github-mirror.example,dev.azure.com.attacker.example— resolved as that provider. Matching is now label-wise, accepting the domain and its subdomains only; a self-hosted deployment this misses can still name its provider explicitly. (#3589) -
A crash mid-write no longer empties the file being written:
wt config shell uninstallended its rc rewrite by truncating in place, so a crash, a full disk, or a lost power cable between the truncate and the write left~/.bashrc,~/.zshrc, or a PowerShell profile empty or half-written, taking every line the user had ever added. Every user-file write now goes through one writer that writes a temp file beside the target and renames it into place. (#3585, #3591) -
wt step pushrefuses to run out of a half-finished operation: Mid-rebase the detached HEAD looks like a linear extension of the target, sowt step push mainreported✓ Pushed to main (1 commit)while moving the target branch onto a half-replayed history and leaving the rebase open; it now runs the same operation gate aswt step rebaseandwt merge. The same change fixes an annotated-tag target always reporting as needing a rebase — the tag object's SHA was compared against amerge-basethat peels it — and makes both push paths refuse a target worktree whose directory is gone, rather than--no-ffmoving the ref over the stale registration. (#3578) -
Conflict markers can't reach a commit, and the refusal names the command you ran:
wt step relocate --commitstaged withgit add -Aand committed straight through an unresolved merge, reaching neither gate added for the other staging commands; every staging path is now gated on an unmerged index.wt mergealready refused, but in a sub-step's name. (#3588, #3587) -
OpenCode activity markers land in the worktree they belong to: The plugin issued its marker commands through the process-global Bun shell without scoping them, so each ran in whatever the process-wide cwd happened to be — under concurrent parallel-agent sessions, a marker write could land in another session's worktree. (#3554, thanks @4i3n6)
Documentation
-
wt step rebaseandwt step pushrender on the docs site: the only two of twelve step operations whose help was terminal-only. Both bodies are rewritten, correcting (among others) the claim that conflicts abort immediately — nothing aborts; the worktree is left mid-rebase with git's markers — and awt step squashnote promising a backup ref unconditionally, when a clean-tree squash writes none. (#3578) -
Troubleshooting no longer suggests disabling
core.fsmonitorglobally: the guidance for a wedged daemon keeps to the targeted fixes — kill the daemon serving that worktree, or let the nextwt listrespawn the live ones. (#3581)
Internal
- The Windows zip ships the signed binary: SignPath names its download after the GitHub artifact, so the signed zip landed beside the unsigned build as
worktrunk-x86_64-pc-windows-msvc.zip.zipwhile the checksum step and the release upload both kept reading the original — so v0.69.1 shipped an unsigned binary under a green run and a completed signing request. (v0.69.0's unsigned binary was the separate upload bug fixed in #3566, where the request failed outright.) The signed file now replaces the built one only after it verifies, and a final step reads the shipped zip and reports any executable without a certificate. Signing remains non-blocking on a self-signed test certificate pending SignPath's OSS review, so Windows will not show a trusted publisher yet. (#3590)
0.69.1
Improved
wt switchstatusline dims the dev-server URL until its port answers: The Claude Code statusline's dev-server URL now dims until something answers on its port, matching thewt listcell it already copied in every other respect. (#3561)
Fixed
-
wt step rebaseandwt mergerefuse to run mid-operation: With a git operation already in progress (a conflicted rebase stop, or a killedgit),wt step rebase <target>reportedAlready up to dateand exited 0 over a conflicted, mid-replay tree — it asked "already rebased?" before consulting the worktree's operation state — andwt mergefailed with a detached-HEAD error whose suggestedgit switchwould have discarded the in-progress rebase. Both commit-replaying commands now detect an open operation up front and refuse with a clear message. (#3558) -
Shell-integration install/uninstall correctness:
wt config shellnow validates the integration command name and rejects malformed values (empty, leading-, or shell-unsafe characters) with a clear error rather than writing a broken rc line; recognizes manually-added or older-form integration lines oninstall, reporting already-configured instead of appending a duplicate; and, onuninstall, scans for worktrunk-managed wrapper files and rc lines by content marker, so integration installed under an alternate binary name (git-wt, …) is cleaned up regardless of the name it was installed under — while a user's own file that merely mentionswt config shell initis left untouched. (#2864) -
wt step squashandwt step commitrefuse to commit unresolved conflicts;wt listmarks every in-progress operation: Invoked directly on a conflicted tree,wt step squashgenerated a commit message for and committed the unresolved conflict markers — clearingMERGE_HEAD, so the broken merge read as complete. The commit-writing step commands now refuse when the index has unmerged paths. The same broadened operation detection giveswt lista single↻gutter symbol for any in-progress git operation (rebase, merge, cherry-pick, revert, bisect), where it previously recognized only rebase and merge. Follow-up to #3558. (#3579) -
wt switchpicker stops its background preview work on exit: Accepting or cancelling the picker abandoned its backgroundgitpreview processes rather than stopping them, leaving them running orphaned — computing diffs into a cache that no longer existed, churning disk on a repo the user had already left. The picker now cancels pending background commands and SIGTERMs running ones when it exits. (#3560) -
wt switchpicker reflects a mid-session removal after a deleted-CWD recovery: When the picker recovered from a deleted working directory, accepting a row reused the startup-time repository snapshot — so an in-pickeralt-xremoval of a worktree or branch during that recovered session wasn't observed on accept. The accept path now rebuilds the repository. (#3557) -
wt -Cis honoured bywt list statusline:wt -C <path> list statuslinereported the statusline for the process's current directory instead of the-Cworktree, and printed nothing when run from outside a repository; both the text and JSON formats now resolve their worktree through the discovery path. (#3567)
Internal
-
Windows code-signing upload no longer double-zips: The unsigned Windows artifact was uploaded wrapped in an artifact-storage zip around the already-zipped binary, so SignPath couldn't locate
wt.exeinside it and the (non-blocking) signing request failed. The upload now setsarchive: false, submitting the real zip to SignPath. (#3566) -
crates.io publishing via trusted publishing (OIDC): The release workflow mints a short-lived crates.io credential per run via
rust-lang/crates-io-auth-actioninstead of a storedCARGO_REGISTRY_TOKEN. (#3564)
0.69.0
Improved
-
wt switchstatusline links in Claude Code: The Claude Code statusline suppressed OSC 8 hyperlinks, so its CI segment printed colored but inert and its dev-server URL printed in full. Claude Code renders OSC 8, so both segments now link, matchingwt list. (#3550) -
wt merge --no-rebaseaccepts merge-shaped histories:--no-rebasepreviously required a strictly linear rebased history and rejected a branch carrying a merge commit, even when the target could already fast-forward to its tip. It now accepts any history the target can fast-forward to, sowt merge --no-commit --no-rebasepreserves an exact commit graph — merge commits and all. (#3509, thanks @reneleonhardt) -
-vvariable blocks name their template and render consistently: The four-vtemplate-variable listings (foreground/background hooks, aliases,wt step eval) now label each block with the template it belongs to, andevalrenders through the shared formatter — curated help-table order rather than its own alphabetical layout. (#3495, #3536)
Fixed
-
wt mergemeasures the squash/rebase span against the target's upstream: When the primary checkout's local default branch was behind its upstream (e.g. localmainbehindorigin/main) and the branch descended from the newer upstream tip (created with--base origin/main),wt merge— andwt step squash/wt step rebase— measured the commit span against the stale local ref and swept in commits already upstream, folding them into the squash and corrupting the local default branch (duplicating upstream content under new SHAs if later pushed). The span is now measured against the target's upstream (a local-only check, no fetch), and a target that has genuinely diverged from its upstream is refused up front. Fixes #3519. (#3549, thanks @starlightromero for reporting) -
Ctrl-C during a rebase surfaces as an interrupt, not a conflict: A
git rebasekilled by a signal (SIGINT/SIGTERM) mid-operation left the worktree inREBASINGstate, whichwtclassified as a merge conflict — printing conflict-resolution guidance and the wrong exit code. A signal-killed rebase now exits cleanly with the signal's conventional code (130 for SIGINT, 143 for SIGTERM). (#3539) -
wt switchpicker responsiveness: Accepting a row could stall for ~10s on a large repo (indefinitely under sustained background traffic) while the switch queued behind per-row preview diffs; the foreground thread now bypasses the command-concurrency semaphore. Separately, an idle picker with pending background work — a slow CI fetch, or an LLM branch summary ([list] summary = true) — spun 100% of a CPU core; the reader now exits once the last row batch lands. (#3544, #3534) -
Clear error when a new branch name collides with an existing branch namespace: Creating
featwhilefeat/xexists (or the reverse) failed with git's raw ref-lock error;wt switch --createnow explains the namespace conflict. (#3528) -
wt switchpicker no longer shows another row's branch name in an empty diff preview: The branch-diff and upstream-diff caches are keyed by SHA, so branches parked at the same commit (common after merged branches reset to the default branch's tip) shared one entry — and the cached pane had the first row's branch name baked into its "no file changes" headline. The cached value is now branch-agnostic and the headline renders per row. (#3481) -
WorktreeRemoveplugin hook no longer strands a completed session: Claude Code fires the hook on session teardown for the recorded worktree path, which may already be gone (removed bywt mergeorwt remove). In that case the hook exited non-zero, which Claude Code read as a failed removal, leaving the completed session row undeletable; it now exits 0 when the worktree is already gone (#3493, closes #3488). Separately, the hook now anchors at the project directory rather than inheriting the session's working directory (#3489). Thanks @judewang for reporting #3488 and for #3489. -
WorktreeCreateplugin hook surfaceswtfailures: The hook pipedwt switch --create … --format=jsonintojqwithoutset -o pipefail, so a failedwt(e.g. a branch collision after a partial creation) tookjq's exit status — 0 on empty input — and Claude Code saw a successful hook that returned no path. The hook now setspipefail. (#3546, closes #3545, thanks @avdi for reporting) -
Picker no longer crashes on the legacy Windows console: skim 5.3.1 drives keyboard-enhancement handling the legacy Windows console API doesn't support, crashing the picker at startup; skim is held at 5.1.0 until the upstream regression is resolved. (#3538)
-
Non-ASCII and non-UTF-8 content handled throughout: Diagnostics no longer panic slicing a config or log at a non-UTF-8 byte boundary, the shell-integration config scan no longer truncates at a non-UTF-8 line, and
wt step copy-ignoredhandles non-ASCII filenames (git'squotePathescaping). (#3514, #3499, #3487) -
-vvoutput cleanup: The end-of-run block names onlydiagnostic.md, dropping the redundanttrace.jsonl/subprocess.loggutter lines the report body already links; and the startup pointer uses@before the log directory, matching the rest ofwt's path output. (#3521, #3543)
Internal
-
Windows release binaries are submitted to SignPath for code signing: Submitted for signing under a test certificate for now, while the project's OSS-program application is under review, and non-blocking so a signing failure can't hold up publishing to crates.io, Homebrew, winget, or AUR. (#3553, #3556)
-
wt listruns its merge analysis in a read-only object database: When the git object store is read-only,wt listandwt list statuslineredirect their object-writing merge/conflict probes into a temporary object database layered over the real one, so the full analysis still runs. Mutating commands keep the persistent store and fail loudly on a read-only one. (#3535) -
wt step relocatepreserves your subdirectory position: Routed through the shared subdir-resolution helper, sorelocatefollows the cwd into the moved worktree likeswitch,remove, andmergealready do. (#3346)
0.68.0
Improved
-
wt switchpreview tabs fill on demand: Navigating to a preview tab (e.g. alt-3, the branch diff) could show "Loading…" for ~10 seconds in a large repo while the tab waited its turn in the background precompute queue. A local-git tab (working tree, log, branch diff, upstream) now computes as soon as it's viewed: a cold tab costs its own git command, and a previously computed one fills from the disk cache in milliseconds. Analt-rrefresh also discards in-flight results from before the refresh rather than letting them repopulate the cleared cache. (#3439) -
wt config stateflags a stale default-branch cache: The detected default branch is cached and not re-validated on every command, so a later change toorigin/HEAD(a rename followed bygit remote set-head origin -a) went unnoticed.wt config statenow flags the cache when it differs from the remote's local HEAD;setadopts the new branch andclearre-detects. (#3478, closes #3471, thanks @starlightromero for reporting) -
Branch-worktree path mismatch demoted to a dim note:
wt listflagged a worktree whose path doesn't match its templated location in red, alarming for the agent-harness worktrees that routinely park at non-standard paths. The mismatch now renders as a dim informational note, and only inwt list: the inline notices onwt switch,wt remove,wt merge, andwt step pruneare removed. (#3419, for #3389, thanks @dmsmidt for raising it) -
wt config updatewrites the upcoming[list] json-schema = 2: With the key unset,updatepreviously pinned the current default (= 1), entrenching the schema being retired; it now writes= 2, moving JSON consumers forward. The default is unchanged: schema 1 applies until the key is set. (#3436) -
wt step promote --format text|json: Parity with the otherstepsubcommands (rebase,push, …). (#3424, thanks @fuleinist)
Fixed
-
Bare-repo project config survives the default branch being checked out nowhere: When no worktree had the default branch checked out (e.g. a linked worktree parked on another branch, common in agent-driven workflows),
wtfound no on-disk.config/wt.tomland silently ran with no project config and no project hooks. It now reads the default branch's committed config from the object store, andwt config showreports that source. (#3462, #3466, #3477, closes #3461, thanks @shuntaka9576 for reporting) -
Relative
WORKTRUNK_PROJECT_CONFIG_PATHanchors to the worktree root: A relative override resolved against the process's current directory, so runningwtfrom a subdirectory silently disabled it. It now resolves from the worktree root, like the default.config/wt.toml. (#3460, thanks @indexzero for reporting in #3454) -
Misplaced nested config keys name the file they belong in: A key valid only in the other config file (e.g.
list.columns, user-config only, set in the project config) was flagged only as an unknown field; the warning now says which file it belongs in. (#3473, thanks @KieranP for reporting in #3469) -
Shell integration detects the shell that invoked
wt:wt switchcould tell a fish user "shell integration requires a restart" because$SHELLnames the login shell, not the shell in use. Detection now walks the process tree to find the enclosing shell, falling back to$SHELLwhen the walk can't resolve one. (#3455) -
Claude plugin's
WorktreeCreatehook runs from the project directory: The hook inherited the session shell's working directory, so a session that hadcd'd outside the repository failed every worktree creation with "not a git repository". (#3453, thanks @silvanshade) -
Codex plugin installs ship skills:
codex plugin addcopies the plugin via a mechanism that doesn't follow symlinks, so theskillssymlink shipped no skills to Codex installs; Windows checkouts, where symlinks materialize as plain text files, had the same gap. The plugin now carries a generated real-file mirror of the skills. (#3440) -
LLM prompt size bounded for long diffs: Commit-message, squash, and branch-summary generation could exceed the model's context window on a large diff. Diffs and squash commit lists now truncate within a fixed budget. (#3438)
-
wt listtask-failure warnings show git's full message: A failure was truncated to its first line, dropping git's own recovery guidance (e.g. theindex.lockadvice). Warnings now render the whole message, and the summary footer no longer restates the warning header's failure count in different vocabulary. In buffered (piped) output the○ Showing …summary line also moves to stderr, so stdout ends after the last row. (#3435, #3442, #3467) -
Watchdog status lines name the running operation: A long wait read "Waiting for the commit message (4s)", as if blocked on user input; it now reads "Waiting for the commit generation command (4s)", and likewise for version checks and PR/MR lookups. (#3443)
-
Carriage returns normalized in captured stdout: A failing command with progress output on stdout (e.g. git's
Receiving objects: 42%\r…) could return the cursor to column 0 and corrupt the rendered error block. stdout is now normalized as stderr already was, and the failure sites that bypassed the standard error renderer (git plumbing, plugin CLI calls, LLM commands) now route through it, so every failed command shows the canonical✗ <command> failed (exit N)header and output gutter. (#3459, #3474, #3483) -
PowerShell wrapper no longer leaks a stray exit-code line to stdout. (#3425)
Internal
-
MSRV bumped to 1.96. (#3428)
-
Dropped the ragged-table panic workaround; the upstream
termimadfix ships in 0.35.1. (#3422)
0.67.0
Improved
-
Experimental
--reapflag forwt remove:wt remove --reapterminates processes still running in the worktree (apost-startdev server, a file watcher, a language server) before removing it, freeing the ports and file handles they hold. Processes are discovered by working directory and terminated withSIGTERM, thenSIGKILLfor survivors; the list prints before any signal is sent. A process holding a controlling terminal (an interactive shell, a terminal editor with unsaved buffers) is never touched. Unix only. Docs (#3396) -
wt switch -xwithout a branch opens the picker:--executeno longer requires a branch argument —wt switch -x claudeopens the interactive picker and runs the command against the selected worktree. It composes with every picker mode (--branches,--remotes,--prs), and the picker path shares the same pipeline as the argument path, so hooks, approval, and template expansion behave identically. (#3394, closes #3370, thanks @gbcreation for the request) -
wt config state logs profilegroups subprocess time by worktree: The report gained a BY CONTEXT table (and aby_contextarray in--format=json) — subprocess time per context, typically the worktree name — so a slow parallel phase can be attributed to the worktree causing it without exporting the trace to an external tool. (#3403) -
-vvdiagnostics surface pager and terminal environment: The diagnostic report (.git/wt/logs/diagnostic.md, written on every-vvrun) gained an "Environment variables" section listing a curated, non-secret allowlist of the pager / terminal / locale knobs (PAGER,GIT_PAGER,TERM,COLUMNS,NO_COLOR,LANG, …) plus git's resolvedcore.pager. These are the inputs that most often explain a rendering bug — like a pager interaction suspendingwt config show(#3322) — and they were previously invisible in the report. The list is a strict allowlist, never a blanketenvdump, so no credential-bearing variable can leak into an uploaded report. -
wt config updatepins[list] json-schemawhile unset: The schema hint from 0.66.0 now comes with the standard one-command fix —wt config updatepinsjson-schema = 1(the behavior-preserving choice) when the key is unset, and the hint offers the command only when running it would actually write the pin. (#3411)
Fixed
-
Claude plugin hooks load again: The Claude activity (🤖/💬) and
WorktreeCreate/WorktreeRemovehooks stopped loading after the hooks file was renamed toclaude-hooks.jsonin #3382. Claude Code discovers plugin hooks by convention athooks/hooks.jsonand does not honorplugin.json's string-pathhooksoverride for plugin loads, so the renamed file was never read —/hooksshowed no worktrunk handlers and the markers silently stopped updating. The file is restored to the conventionalhooks/hooks.json; the #3362 Codex collision the rename guarded against stays closed because the Codex manifest defines its hooks inline, which overrides Codex's convention discovery independently of the filename. (#3417, thanks @avdi for reporting) -
wt switchno longer crashes on ragged tables in PR comments: A PR/MR comment table with more cells in a data row than in its header could panic termimad's column fitter at narrow widths, and because the comments preview renders on a background worker, the panic aborted the whole picker. The table render is now contained; the preview falls back to the table's plain text. (#3408, closes #3407, thanks @ortonomy for reporting) -
wt config showno longer suspends on the zsh completion probe: The interactive zsh probe that detects a missingcompinitclaims the terminal foreground when job control is on; a slow or prompting zsh startup could hit the probe's kill-on-timeout before the foreground was restored, leavingwtin a background process group and its pager suspended withsuspended (tty output). Both interactive probes now run with job control disabled (zsh +m), so a timed-out probe can't strand the terminal. (#3327, closes #3322, thanks @karmeleon for reporting) -
wt step for-eachand the--executefallback no longer inheritwt'sGIT_*discovery vars: The command that for-each runs in each worktree — and the--executepayload, whenwtexecutes it directly because shell integration isn't active — now discovers its repository from the worktreewtplaced it in, rather than an inheritedGIT_DIR/GIT_WORK_TREE. Previously such a command'sgitcalls resolved against the one inherited repo (e.g. the invoking worktree, whenwtruns as a!wtgit alias from a linked worktree) while the per-worktree headers claimed otherwise. This extends the hook-spawn scrub from #3374 to the remaining spawn sites that relocate a user command into awt-chosen worktree; aliases andcommit.generationcommands run in the user's own context and keep the inherited environment. (#3400, #3373) -
wt listholds steady when the shell prompt returns: The progressive table reserves two blank rows beneath its footer, so the multi-line prompt printed at exit renders into pre-scrolled rows instead of scrolling the settled table up. (#3409) -
JSON schema 2: no self-relation on a remote-only default-branch row:
wt list --format=jsonwith schema 2 gave a remote-only row of the default branch (origin/main) a self-referential relation; the default-branch check now compares the remote-stripped branch name. (#3383)
Documentation
-
Codex commit-generation model bumped to
gpt-5.6-luna: The recommended[commit.generation]command for Codex — shown bywt config createand in the LLM-commits docs — now usesgpt-5.6-luna(wasgpt-5.4-mini). OpenAI's models page movedgpt-5.4-miniinto the previous-generation "Other models" tier;gpt-5.6-lunais the fast/low-cost variant of the current recommended (5.6) family. (#3430) -
Code Signing Policy page: A new page documents Worktrunk's code-signing policy for the Windows binaries under the SignPath Foundation open-source program: certificate provenance, the signing pipeline, and per-release approval. Signing addresses Microsoft Defender's false positives on unsigned native binaries. Docs (#3366, thanks @bemnlam for reporting #3355)
Internal
-
skim 5.0: The picker's fuzzy-finder library moves from 4.10 to 5.0, dropping roughly 1,000 lines of transitive dependencies from the lockfile. (#3378)
-
Prune benchmarks: A rust-repo-scale prune fixture, Criterion benches, and trace spans over the removal path. (#3401)
-
Nix flake test sandbox provides
lsof, and flake changes now trigger the nix-flake CI job. (#3410)
0.66.0
Improved
-
Opt-in JSON schema 2 for
wt list:wt list --format=jsonandwt list statusline --format=jsoncan now emit a v2 schema, selected via the[list] json-schemaconfig key. Schema 2 wraps the rows in an envelope (schema,repo.default_branch,repo.forge,collected) and separates "nothing to report" (field absent) from "requested but undetermined" (fieldnull— a probe still pending, timed out, or failed). Schema 1 — the existing bare array — remains the default, byte-for-byte; with the key unset, a once-per-process stderr hint shows both values (= 1to pin,= 2to opt in; suppressed on the statusline surface). Docs (#3357) -
wt config approvals listandclear --stale:listshows every command the project config declares — hooks in lifecycle order, aliases, commit-message guidance — grouped into approved and unapproved, with approvals recorded for commands no longer in the config flagged as stale.clear --staleremoves only those left-behind approvals, echoing each one; valid approvals survive, and a full wipe remains plainclear. Docs (#3380) -
Codex sessions show activity markers: The Codex plugin now ships Codex-native activity hooks, so
wt listshows 🤖 (working) / 💬 (waiting) for Codex sessions as it does for Claude Code, OpenCode, and Gemini — and Claude-branded events no longer surface inside Codex sessions. Codex has no session-exit event, so the marker rests at 💬 after a session ends until the next session orwt config state marker clear. (#3364, closes #3362, thanks @ofek for reporting)
Fixed
-
User hooks no longer inherit
wt'sGIT_*discovery vars: A user hook that shells out togitnow discovers its repository from the worktreewtsets as its cwd, rather than an inheritedGIT_DIR/GIT_WORK_TREE(e.g. fromwtrun as a!wtgit alias, or nested under another tool's git hook).wtnow scrubs theGIT_*discovery vars at every hook spawn site — foreground, background, and concurrent. Previously the inherited context leaked into hooks: with bothGIT_DIRandGIT_WORK_TREEpresent, a hook that rangit initwould writecore.worktreeinto the inherited repo's config, silently redirecting later plain git commands there.wt's own internal git plumbing keeps the inherited context (the absolutize-and-forward behavior from #1914); aliases keep it too, since a top-levelwt <alias>is the user's own command. (#3374, closes #3373, thanks @silvanshade for reporting) -
SCP-style SSH remotes with custom usernames: Remote URLs like
org-12345678@github.com:owner/repo.git(GitHub account/org aliases) now parse canonically, sowt switch pr:<n>matches the local remote instead of failing withNo remote found. Malformed and local-path forms that merely resemble SCP syntax are still rejected. (#3371, thanks @fcoury-oai)
Documentation
-
Windows app-alias guidance matches current Settings: The instructions for disabling Windows Terminal's
wtapp-execution alias now point at the current Settings path (Apps → Advanced app settings → App execution aliases). (#3372, thanks @ofek) -
/wt-switch-createalways creates a worktree: The plugin skill's wording let a session judge that a research or read-only task didn't need isolation and skip the worktree; invoking the command now counts as the explicit request, so the worktree is created unconditionally. (#3356)
0.65.0
Improved
-
Picker
alt-xflashes why a worktree wasn't removed: Whenalt-xin thewt switchpicker keeps a row instead of removing it, the reason now flashes in the picker header for a beat, rather than only draining to stderr after you quit — so the "why" is visible while the row is still in front of you. It covers both the by-design keeps (the current worktree; an unmerged branch-only row shows○ Kept <branch> — branch is unmerged) and genuine removal failures (a dirty, locked, or main worktree, shown as an error). The full diagnostic still drains on exit. The--prsloading marker now also matches the picker's other in-flight placeholders (↳ Loading open PRs…). (#3336, #3350) -
-vvdiagnostics consolidate ondiagnostic.md, led by the performance profile: A-vvrun now opens with a one-line pointer to the log directory (○ Verbose logging to .git/wt/logs/) and closes by naming what it captured —○ Logs, performance profile, and diagnostics saved @ diagnostic.md— with the rawtrace.jsonl/subprocess.logcompanions listed beneath and thegh gist createbug-report hint.diagnostic.mdnow leads with the performance profile, expanded by default and promoted above the environment / worktree / config dumps; that profile reports the 20 slowest calls (was 8) and 10 same-context redundant-command offenders (was 3). The profile lives in thediagnostic.mdbundle, andwt config state logs profilere-renders it live fromtrace.jsonl. (#3329)
Fixed
wt removepreserves your subdirectory position: Removing a worktree from a subdirectory (e.g.apps/gateway/) now lands you in the equivalent subdirectory of the destination worktree rather than at its root — matching howwt switchalready behaves, and falling back to the root when that subdirectory doesn't exist there.wt mergelands through the same handler, so it gains the same behavior. (#3344, closes #3343, thanks @caillou for reporting)
0.64.0
Improved
-
{{ git.branch.* }}template namespace for custom columns:wt listcustom columns can now read a branch's own git config via{{ git.branch.<key> }}— both convention keys you set yourself (branch.<name>.jira) and the git-nativebranch.<name>.description— without re-storing the values throughwt config state vars set. It complements{{ vars.* }}, which reads only worktrunk's own state namespace. (#3319, thanks @cazador481 for the request) -
Faster warm-cache
wt listre-runs: A repeatwt list(with.git/wt/cache/already populated) is ~36% faster, by removing redundant per-row git subprocesses — priming the commit→tree cache from the%ctbatch already issued, persisting merge-base results to a content-addressed on-disk cache, and seeding worktree roots and git-dirs from the singlegit worktree listinstead of re-forkinggit rev-parseper worktree. (#3334)
Fixed
-
Refs that look like flags can't inject git options:
wt'sgit diffpreviews and diffstats (picker diff previews,show_diffstat, push diffstat) now fence user-controlled refs behind--end-of-options, and branch removal (git branch -D) passes them after--, so a branch literally named like a flag (-x,--foo) reaches git as a positional ref instead of being misparsed as an option. (#3317) -
Statusline renders untruncated when
COLUMNS=0:wt list statuslinetreatedCOLUMNS=0as a zero-width budget and dropped every segment, rendering an empty line. A zero or unparsableCOLUMNSis now treated as no detectable width, so the line renders everything untruncated — as the statusline sizing already documented for a missing width. (#3318) -
Watchdog "still waiting" line uses the hint symbol: The transient
Waiting for the commit message (Ns)line shown during a slowwt step commitLLM call now uses the hint prefix (↳) instead of the info symbol (○), matching the convention for fully-dim status lines. (#3330)
0.63.0
Improved
-
[list] columnscan force a column on past--full: Listing a column now overrides the--full/[list] summarypresets —columns = ["branch", "ci"]shows the CI column without--full. Hard data-source prerequisites still apply: a listedsummarywith no[commit.generation]command, orurlwith no template, stays hidden, since listing can't conjure data that isn't configured. (#3295) -
Alt-rin the picker refreshes the preview panes, not just the rows:Alt-rre-collected the rows but kept serving cached preview content, so editing a tracked file and refreshing still showed the pre-edit diff. It now clears the preview cache too, recomputing the working-tree / log / branch-diff / upstream / summary tabs and re-fetchingpr/comments. Unchanged branches re-read from the content-keyed on-disk cache, so only genuinely changed content pays a recompute. (#3293) -
A narrowed
wt listis now actually faster: A narrowed[list] columnsselection (e.g.["branch", "path"]) now runs only the git work its columns need. It previously ran every per-worktreegit status, diff, and ahead/behind walk regardless, then discarded the unselected results — so a trimmed view was no faster than the full table, and on a repo with many dirty worktrees that discarded work was the bulk of the wall-clock cost. (#3274, thanks @jtaby for reporting) -
Fewer duplicate git calls in
wt list --full: The two integration probes per row (the conflict bit formain-stateand the clean-merge tree for the integration column) issued a byte-identicalgit merge-tree, and the shared default-branch tip was peeled to its tree once per row. Both are now deduplicated through the in-memory cache, so each resolves once per run instead of once per worktree. (#3288, #3289) -
Picker
commentstab avoids redundant forge fetches: The picker'scommentspreview tab gained an on-disk cache keyed by the PR'supdatedAt(which rides for free on the CI fetch the picker already makes), so a repeatwt switchskips the per-rowgh pr view --json commentsfetch when the thread is unchanged and paints the tab instantly instead of showing "Loading comments…". The cache is also primed from thegh pr listcall the picker already makes, so the tab skips its own fetch even on a session's first open (including PRs with no comments). GitHub only. (#3294, #3299) -
Statusline width comes from
COLUMNS, not a parent-process walk:wt list statuslineused to spawn up to 10pscalls plussttyper render to recover a terminal width, because Claude Code piped the subprocess with no inherited TTY. Claude Code now setsCOLUMNS/LINESto the terminal dimensions before running the script (since v2.1.153), so the width comes straight from there — less a fixed 5-column margin for Claude Code's own UI — and theps/sttywalk is gone. On an older Claude Code that doesn't setCOLUMNS, the line renders untruncated rather than walking the process tree. (#3286, closes #2950)
Fixed
-
wt listdiff and ahead/behind columns use the upstream default tip: Themain↕(ahead/behind) andmain…±(diff) columns measured every branch against the local default-branch tip, so in a fork whose localmainlagged its upstream they reported inflated counts — one fork branch showed↑44and+∞ / -5Kwhen it was ~2 commits past the real upstream tip. They now diff against the same upstream-aware base the integration column already uses. (#3280) -
Integrated branches no longer flash
✗inwt list: A squash-merged branch whose default branch later re-edited the same lines showed✗(would-conflict), even thoughwt step pruneclassified it as⊂(fully integrated) and removed it. The list now ranks the integration verdict above the downstream conflict, matching prune; a genuinely un-integrated conflict still shows✗. (#3278) -
wt listdivergence-overflow marker uses one emphasis level: When an ahead/behind count overflows its digit budget, themain↕column's compactC/K/∞marker rendered the "behind" subcolumn as dim + bold (reading as bold red) instead of a clean one-level step. It now steps exactly one level — dim red → normal red — matching the "ahead" subcolumn, acrosswt listand the picker. (#3303) -
Branch deletion on removal is atomic:
wt removeand prune now delete a branch with a compare-and-swap (git update-ref -d <ref> <expected-sha>) against the SHA the integration check already read, closing the window where a branch whose tip moved in between (e.g. a hook landing a commit) could be deleted. Such a branch is now retained with a clear message and awt remove -D <branch>recovery hint. This also unifies the previously divergent safe-delete paths; explicit force-delete still usesgit branch -D. (#2903) -
wt listand picker Age/Message columns paint as soon as the commit batch lands: These columns carry no async task — their data arrives with the initialgit logbatch — but stayed on the·placeholder until some slower task happened to redraw the row, so the commit message lagged behind a cache-warm Summary preview. They now paint the moment the batch lands. (#3287) -
Picker
Alt-xremoval: no cursor flash,--prsrows preserved: Removing a row withAlt-xflashed the>pointer to the top of the list for a frame, and in--prsmode made the streamed PR/MR rows vanish until the nextAlt-r. Removal is now a synchronous in-place pool resync: the cursor lands on the row that slid up with no flash, and the PR/MR rows survive. (#3268, #3275) -
Removable rows stay gray when selected in the picker: A safe-to-delete worktree (integrated, or clean and even with the default branch) renders its row gray, but the gray vanished under the selection highlight — exactly when you're about to act on it. The gray now survives selection (selected row only;
wt listand unselected rows are unchanged). (#3267) -
Picker preview keeps its scroll when CI status arrives: Scrolling down a diff and waiting a couple of seconds snapped it back to the top when the live CI fetch landed and re-rendered the pane. The re-render is now precise — a tab re-runs only when its own content would actually change — so a CI update no longer throws away the scroll position of an unrelated tab. (#3292)
-
Picker summary tab dims when there's nothing to summarize: The summary preview tab (
5) stayed lit on a clean branch with no commits ahead, unlike the diff tabs (1/3/4), which dim once their diff is known empty. It now dims in concert with them once both the branch diff and working tree are known empty. (#3291) -
Picker default view keeps collect order: With no query typed, the
wt switchpicker reordered rows by where each name's last/falls, so slash-bearing branches (feature/…,perf/…) sank toward the bottom and intermixed with other row kinds. The default view now preserves collect order. (#3301) -
Picker branch-diff preview and summary use the upstream-aware base: Like the
wt listcolumns above, the picker's branch-diff preview pane and the LLM branch summary diffed against the raw local default branch, so a fork whose local default lagged upstream made them describe dozens of already-merged commits. They now use the same upstream-aware comparison base. (#3305) -
Picker comment previews render fenced code blocks cleanly: A fenced code block inside a PR/MR comment rendered as a garbled double gutter in the
wt switchcomments preview — alternating bar/no-bar lines with broken alignment. The code block now renders without the nested gutter. (#3306) -
First-run hints show the config path wt actually loads from: The picker's disabled-summary tab and the commit-generation setup prompt hardcoded
~/.config/worktrunk/config.toml, so a user with--config,WORKTRUNK_CONFIG_PATH, or a non-default$XDG_CONFIG_HOMEwas told to edit a file wt never reads. Both now show the resolved path. (#3290, #3298)
Internal
-vvtrace and profiler accuracy:trace.logis now purely human-readable, with the machine-parseable[wt-trace]fields living only intrace.jsonl; and the performance profile's cache analysis no longer reports stdin-driven commands (LLMclaude -pcalls,git patch-id) as duplicate re-runs, since their real input isn't captured in the command string. (#3296, #3297)
0.62.0
Improved
-
Browse open PRs/MRs in the
wt switchpicker, with live CI: The picker now streams a live CI/review-status column per row — priming from the local cache, then fetching live (it previously showed only cached numbers with no network). A new--prsflag adds the repository's open PRs (GitHub) / MRs (GitLab) as rows alongside your worktrees, each withpr/comments/logpreview tabs loaded from the forge in the background; selecting one fetches its branch and switches. Rows whose branch is already shown aren't duplicated, so--prsdiffers from plainwt switchonly by the extra rows. Docs (#3128, #3169, #3189, #3252) -
main…±diff column shows by default inwt list: Themain…±column (lines changed since the merge-base with the default branch) now renders in the defaultwt listand picker, served from a persistent cache rather than a history walk.--fullnow adds only the columns that reach off-machine — CI status and LLM branch summaries — andwt list --format=jsonpopulatesmain.difffor every item, not just under--full. (#3236) -
[list] columnsselects and orders the columnswt listshows: A new[list] columnsuser-config key (also settable via--config-set) takes an ordered array of column names — built-in (branch,status,ci,path, …) and[list.custom-columns]headers alike — and renders exactly those, in that order. Where v0.61.0's custom columns add columns, this selects and reorders the whole set; omit the key for the default layout. (WORKTRUNK__LIST__COLUMNSisn't supported yet and warns if set.) (#3141) -
Picker PR preview shows full PR/MR detail: The
prtab now renders the PR/MR title, markdown description, author, draft state, comment count, bold branch name, and underlined URL for any row whose branch has an open PR — not just--prsrows — and thecommentstab fetches the real thread. The title and description ride the CI fetch the picker already makes; the comment thread is fetched lazily in the background, once per row. (#3167, #3197, #3223, #3195, #3231) -
Interactive picker runs on Windows: The
wt switchpicker was Unix-only because its preview-tab keys shelled out through a per-process state filecmd.exelacks; tab state moved to an in-memory atomic with native skim callbacks, lifting the platform gate. (#3217) -
Picker keyboard shortcuts:
Alt-ycopies the selected branch name,Alt-oopens the row's PR/MR in the browser, andAlt-rrefreshes the list (picking up worktrees created or removed elsewhere). (Breaking: in the picker, remove moved fromAlt-rtoAlt-x) (#3233) -
Picker uses the full terminal: List height scales with the terminal (a roughly even split with the preview, minimum 3 rows) instead of a fixed 12-row cap, the table lays out at full width so toggling the preview off with
Alt-preveals the freed columns with no reflow, and a scrollbar appears when the list overflows. (#3205, #3214, #3198) -
Picker filters on more of each row: Typing a gutter sigil filters by row kind (
+for linked worktrees,@for the current one), the fuzzy matcher ranks on the distinguishing path segment rather than the shared parent prefix, and rows with a PR/MR also match on its number, title, and author. (#3143, #3208, #3252) -
Picker visual polish: The legend recolored to dim-cyan and reordered so navigation leads, and preview loading placeholders moved to the dim-hint style (
↳transient,○settled,▲failed). (#3237, #3253) -
"Still waiting" status for slow commit-message generation: A configured
commit.generationcommand captures stdout, so a slow or hung LLM previously showed nothing whilewt step commit/squashwaited. After a 2s delay worktrunk now shows a dim, in-place○ Waiting for the commit message (Ns)status, escalating at 10s to reveal the exact shell-escaped invocation in a gutter beneath it; the block clears on completion, mirroringwt list's stall footer. (#3178) -
"Still waiting" status extended to more slow commands: The same waiting status now covers three more foreground commands that were silent while a captured subprocess ran — the
wt config show --fullcommit-generation self-test, thewt switch pr:/mr:host lookup, and thewt config show --fullversion check. The version check no longer caps its fetch at an aggressive 5s, instead showing the status while a slow-but-working request completes (with a generous ceiling so a non-interactive run can't hang). (#3183) -
wt config state logs profileperformance profiler: A new subcommand turns a-vv[wt-trace]capture (a path argument,-for stdin, or the defaulttrace.log) into a performance profile: subprocess time by command shape, the slowest calls, parallelism and peak concurrency, redundant cache-miss re-runs, and — forwt list/picker captures — derived latencies and a collect timeline.--format=jsonemits the same data, and every-vvbug-report bundle now inlines a rendered profile. (#3184, #3186) -
WORKTRUNK_VERBOSEenv var:WORKTRUNK_VERBOSE=0|1|2mirrors-v/-vv, combined with any flag viamax(the env sets a floor the flag can raise). Unlike the flags it's honored on the shell-completion path, which returns before flag parsing — so a slow tab-completion can be profiled for the first time. (#3166) -
Aliases inherit the wrapped command's completion: An alias that forwards
{{ args }}to a singlewtcommand (co = "wt switch {{ args }}") now completes that command's arguments and flags —wt co <Tab>completes branches likewt switch <Tab>— instead of a generic stub. Bare dispatchers and multi-{{ args }}aliases keep the stub. (#3172, thanks @yzx9) -
wt step copy-ignored --require-include: A new--require-includeflag makes the copy a no-op unless a.worktreeincludefile exists in the source worktree (matching Claude Code desktop, where that file is required), reporting why it skipped as a hint in text mode and areasonfield in--format=json. (#3196, thanks @yzx9) -
wt step tetherhonors-C: The global-C <path>flag now sets the tethered command's working directory (wt step tether -C frontend -- npm run dev); teardown still watches the worktree root, so the command is reaped when the worktree is removed. (#3207) -
Statusline rate-limit pace color grades by severity: The pace segment's color now deepens (dim → dim-yellow → yellow) with the projected throttling severity, so an early-window burst stays muted while a costly projected lockout stands out. The displayed pace number is unchanged. (#3229)
Fixed
-
Picker
Alt-xremoval updates the row in place: Removing a worktree no longer re-collects the whole list (a flicker that reset the cursor to the top): an unmerged worktree's row morphs into a branch-only row while the worktree is removed in the background, and a merged worktree's row drops with the cursor landing on the row that slid up. A removal that can't safely happen — the current or main worktree, or a dirty or locked one — is declined with the same diagnosticwt removeprints, rather than a dead keypress or a disruptivecdhome. The post-removal cursor lands by row identity, so it stays correct even under an active filter. (#3262, #3199, #3211, #3225) -
Picker preview refreshes when its background fetch lands: A diff/log/PR-view fetch that completed after its triggering keystroke used to sit unshown until the next keypress; the pane now updates on its own when the fetch lands. (#3247)
-
Picker rows stay aligned while filtering: Typing a filter no longer slides the matched row left and drops its leading sigil, and
Alt-l/Alt-hno longer scroll the list off its gutter. (#3213, #3226) -
fish completions no longer recurse to the call-stack limit: With worktrunk's fish lazy-load wrapper installed, completion could re-enter the wrapper and recurse until fish hit its call-stack limit. The package-manager registration now resolves the real binary via
type -Pinstead of calling the barewt, and the wrapper stub short-circuits in completion mode the way the bash and zsh wrappers already did. (#3241, #3250; fixes #3240, thanks @maciej-lech for reporting) -
wt switchchanges directory under fish withzoxide.fish: The fish integration used a barecd, which thekidonng/zoxide.fishplugin intercepts as a fuzzy query, so a switch reported success but failed withzoxide: no match found. The wrapper now usesbuiltin cd, bypassing any usercdoverride. (#3160, fixes #3159, thanks @anon-legion for reporting) -
wt switch pr:Nresolves Azure DevOps projects with encoded path segments: Azure returns decoded project names while git remotes store them URL-encoded, so a project likeproject with spacesfailed to match. Path segments are now canonicalized before comparing remotes and building Azure URLs. (#3204, fixes #3203, thanks @jonasherfort) -
Recommended Claude Code commit command preserves
apiKeyHelperauth: The suggested[commit.generation]command for Claude Code used--setting-sources='', which dropped user settings and broke authentication for setups that get their key viaapiKeyHelper. It now uses--safe-mode --setting-sources='user': the run stays hermetic (no hooks, plugins, MCP, skills, or CLAUDE.md) but loads user settings soapiKeyHelperworks (requires Claude Code ≥ 2.1.169). Existing user configs are not rewritten. (#3170) -
Claude Code paths honor
CLAUDE_CONFIG_DIR: worktrunk hardcoded~/.claudefor every Claude Code path, so on a machine that relocates the config tree viaCLAUDE_CONFIG_DIR,wt config showwrongly reported the plugin and statusline as not installed andinstall-statuslinewrote to a stray file Claude Code never reads. All three call sites now resolve throughCLAUDE_CONFIG_DIR, falling back to~/.claude. (#3215, thanks @tftio) -
Deprecated config keys migrate in
--config-set, env vars, and inline tables: Deprecated keys were rewritten to canonical form only in config files; passed any other way they fell through as unknown fields and were silently dropped.wt --config-set 'merge.no-ff=true',WORKTRUNK__MERGE__NO_FF=true, and the inlinemerge = { no-ff = true }form now all migrate and take effect. (#3152, #3158) -
wt config showcan't hang on the zsh compinit probe: The interactivezsh -icprobe that checks whether compinit is configured could hang indefinitely on compinit's insecure-directories prompt; it now has a 2s kill-on-timeout, declining to warn on timeout. (#3165) -
Empty branch-name arguments rejected at the parse boundary: An empty value (e.g.
wt step diff --branch=) was accepted and produced a garbled downstream diagnostic; all branch-name arguments now reject empty or whitespace-only values at the CLI edge with a standard usage error. A real missing branch still gets its normal "no worktree" diagnostic. (#3179)
Documentation
wt listJSON and help-text accuracy: Thewt listhelp text and web docs now note which--format=jsonobjects (ci,summary) require--full, and the status-symbol reference tables name each JSON field by type — correcting a wrong "only the first matching symbol is shown" note and an unreachableworktree.statevalue. (#3139, #3220, #3224)
Internal
- Richer
-vvdiagnostics:-vvnow also writes a machine-readabletrace.jsonl(one JSON object per event) alongsidetrace.log, segmentssubprocess.loginto per-command blocks joined to the trace by aseqfield, and lists each log path on its own line; worktrunk's own log sites moved to nativetracing. (#3232, #3182, #3163)
0.61.0
Improved
-
wt listcustom columns: Each[list.custom-columns.<Header>]entry in user config adds a column towt list(and thewt switchpicker), rendered per row as a minijinja template overbranch,worktree_path,worktree_name, andvars.*, with optionalwidthand drop priority. Values expand from in-memory data only — no subprocess runs per cell — and a column that is empty on every row is dropped.wt list --format=jsongains acolumnsmap per item. The feature is experimental, so the config shape may still change. (#3073, thanks @Faria22, whose #3065 prototyped configurablewt listcolumn visibility and motivated this area) -
--config-setfor inline config overrides: A global, repeatable--config-set <toml>flag overrides any user-config key for a single invocation, layered above config files andWORKTRUNK_*env vars. The value is a real TOML fragment, so arrays and tables work natively (wt --config-set list.full=true list); nested tables deep-merge, and a malformed or invalid override drops the whole--config-setlayer with an attributed warning rather than failing the command. (#3138) -
Picker shows cached PR/MR numbers: The
wt switchpicker skips the networked CI-status fetch, so it previously had no CI column. It now fills PR/MR numbers from the local.git/wt/cache/ci-status/cache populated by earlierwt list --fullor statusline runs, with zero network access. A stale entry (TTL passed or branch head moved) keeps its number dimmed; expired entries without a number are dropped. (#3073) -
Faster file copies on macOS: After a reflink (
clonefileon APFS, which already preserves mode bits), worktrunk now skips the redundant follow-upchmodon macOS, saving one syscall per file inwt step copy-ignoredand every other copy path. Linux (btrfs/XFS) still sets permissions, sinceFICLONEclones data extents only and drops the execute bit. (#3149)
Internal
-
Picker migrated to skim 4.8 (ratatui/crossterm): The
wt switchpicker moved off skim 0.20.5 (tuikit) to skim 4.8.0, dropping the vendoredvendor/skim-tuikit/patch tree (both patches it carried are now native or upstream). Two cosmetic picker changes come with it: the match counter no longer overlaps the preview-tab header, and the HEAD column shows the full short-SHA. (#3137) -
All command spawns route through one trace chokepoint:
CommandTraceis now the sole emitter of[wt-trace]command records, so a spawn path can't silently skip tracing —git worktree add, previously an unattributed gap, now shows up as a labeled slice inwt-perf timeline. (#3134)
Documentation
- Hook-approval skill guidance: The bundled worktrunk skill now frames hook approvals as user consent and no longer advocates
--yesto bypass prompts. (#3146)
0.60.0
Improved
-
Package installs complete branch and worktree names: A plain
brew install worktrunk(or other package install) now tab-completes branch and worktree names, not just subcommands and flags.wt config shell completions <shell>emits a dynamic registration that calls the binary at completion time (the maintainedclap_complete::envpath), matching whatgh,rustup, andkubectlship. (#3105, thanks @bendrucker) -
wt step relocatemoves dirty linked worktrees: Relocating a linked worktree with uncommitted or untracked changes no longer skips it.git worktree movecarries those files along, so the dirty-skip was a worktrunk policy rather than a git limitation. The main worktree still skips when dirty (without--commit), since its relocation falls back togit checkout, which refuses to switch over a dirty tree. (#3104, thanks @lunaynx for reporting) -
--dry-runpreviews print to stdout:wt hook <type>,wt step relocate,wt step prune,wt step copy-ignored, andwt config shell install/uninstallnow send their--dry-runpreview to stdout (the command's answer) while narration stays on stderr. This matcheswt list,git clean -n, andterraform plan, and keeps previews pageable and pipeable. (Breaking: scripts reading these previews from stderr should now read stdout) (#3085) -
Picker gutter distinguishes local and remote branches: With
--branchesand--remotes, thewt switchpicker marks each row's kind in the gutter:/for a local branch without a worktree and|for a remote branch, alongside the existing@/^/+worktree glyphs. (#3115) -
wt list --format=jsongains structuredrepometadata: JSON output addsrepo(the local checkout's primary remote) andci.repo(the repositoryci.urltargets, which differs for fork PRs) objects carryinghost/owner/name/provider; the existingrepo_urlandci.repo_urlstrings remain. The provider honors the configured[forge].platformon hosts that can't be auto-detected. (#3021, thanks @jeremy0dell) -
wt step eval --format=json:wt step evalgains a structured{name, template, result}JSON lane on stdout, the machine-readable analog of its-vview. Text mode is unchanged. (#3106) -
--format=claude-coderejected where it never applied:wt listandwt config state getaccepted--format=claude-codesilently and treated it astable; the value only ever meant anything onwt list statusline. Both now fail fast withinvalid value 'claude-code'. (Breaking:wt list --format=claude-codeandwt config state get --format=claude-codenow error) (#3116)
Fixed
-
cargo install worktrunkbuilds from crates.io: Installing from crates.io failed to compile withenvironment variable VERGEN_GIT_DESCRIBE not defined at compile time, because the package archive has no.gitfor the build script to read. The version lookup now falls back to the cargo package version when git-describe is unavailable. (#3124, fixes #3123, thanks @kerrickstaley for reporting) -
Tab-completion works in repos with no commits:
wt switch <TAB>andwt remove <TAB>returned nothing on a freshgit initrepo, because the unborn default branch has no entry underrefs/heads/. Completion now also draws on worktree branches, so the unbornmain(and anywt switch --createbranch on an empty repo) completes. (#3097, closes #3094) -
wt config state get ci-status --format=jsonhonors[forge].platform: On a self-hosted host the parser can't recognize (Gitea, Azure DevOps, GitHub Enterprise on a generic domain), this path reportedprovider: "unknown"whilewt list --format=jsonreported the configured provider. All structured-output paths now route the[forge].platformoverride through one accessor, so they stay consistent. (#3120)
Documentation
-
wt list --helpexample tables fit the terminal: The capturedwt listexample tables in--helpchop to terminal width with a dimmed ellipsis (matching realwt list) instead of word-wrapping and shearing their columns; hand-authored command examples still wrap. (#3125) -
/wt-switch-createcross-repo handling reworked: The Claude Code plugin command's skill reworks how it creates and enters a worktree in another repo, built around what the harness actually does (EnterWorktreere-roots within the current repo; acdreaches another repo when it's inadditionalDirectories). The procedure dropped from five steps to three. (#3118) -
Help-text and docs refinements:
--formathelp renders its values inline ([possible values: table, json]) instead of an expanded block;wt switchandwt step pushhelp got smaller clarifications; and web-doc terminal blocks no longer wrap command-only lines. (#3096, #3108, #3110, #3112, #3126)
0.59.0
Improved
-
Picker frees digit keys for filtering; preview tabs move to
Alt: In thewt switchpicker, plain digits now go to the filter, so a branch name with a number in it can be typed directly. Preview tabs jump withAlt-1–Alt-5or cycle withTab/Shift-Tab. (Breaking:1–5no longer switch preview tabs) (#3079) -
Squash templates use
commit_detailsinstead ofcommits: The squash commit-message template's{{ commits }}variable (commit subjects) is deprecated in favor of{{ commit_details }}, which renders as the bare subject when printed directly and also exposes.subjectand.body.wt config updatemigratescommitstocommit_detailsas a plain rename. (#2985) -
wt step eval -vlists template variables:wt step eval -vnow prints the available template variables on stderr in the gutter style, above the template-expansion view. That expansion (also shown bywt switch -v, hooks, aliases, andwt -v list) renders the template and its result as separate labeledsource/resultblocks rather than a shared gutter. The result still goes to stdout, so$(wt step eval …)is unchanged. Sinceevalmutates nothing and is experimental, its--dry-runflag (which dumped the raw variable context) is removed rather than deprecated. (#3078, #3099)
Fixed
-
Picker no longer freezes on the first keystroke: With many worktrees, typing the first character in the
wt switchpicker locked the UI for several seconds. The fuzzy matcher shares rayon's global thread pool with worktrunk's git collection, which floods it with blocking subprocess calls, so the matcher queued behind them. Collection and preview work now run on a dedicated pool, keeping the matcher responsive. (#3087, thanks @bendrucker; fixes #2926, thanks @mahume for reporting) -
Tab-completion covers all hook types:
wt hook <type> <Tab>completed configured command names for only seven of the ten hook types;pre-switch,post-switch, andpost-removereturned nothing. Completion now derives the type from the canonical hook list, so every type completes its command names. (#3070) -
Ctrl-C on a concurrent alias reports the right exit code: Interrupting
wt step <concurrent-alias>could report exit 143 (SIGTERM) instead of 130 (SIGINT), because a per-child SIGINT → SIGTERM → SIGKILL escalation could land a SIGTERM on a child during the grace window; the escalation also serialized across process groups, so repeated Ctrl-C could wait. wt now forwards the user's signal once per process group, so a cooperative child dies from the signal actually sent (130 on Ctrl-C); a second signal kills any survivor immediately. (#3075)
Documentation
- Per-worktree env vars: A Tips & Patterns section shows how to give each worktree its own environment variables with direnv or mise. (#3074)
Internal
- The commit-generation command is not rewritten to disk when its value is unchanged. (#3084)
0.58.0
Improved
-
Table-form
pre-*hooks run concurrently: A multi-entry table hook ([pre-merge]with several keys) now runs its commands concurrently, completing the cut-over announced in v0.37.0: every hook type has one execution semantic, where table form is concurrent and pipeline form ([[pre-merge]]blocks) is serial. Configs that need ordering should use pipeline form; affected configs have warned on every invocation since v0.37.0, withwt config updateoffering a serial-preserving migration. Two adjacent inconsistencies are also gone:wt hook <post-type>foreground runs of multi-entry table hooks now run concurrently like their background counterpart, and single-entry table aliases write to stdout like the other alias spellings, sowt <alias> | …works for that spelling too. (Breaking: multi-entry table-formpre-*hooks no longer run serially) (#3052) -
CI column shows the PR/MR number and review state: The CI column in
wt list --fulland the statusline shows the branch's open PR/MR reference (#3041on GitHub/Gitea/Azure DevOps,!3041on GitLab) instead of a plain dot: colored by CI status, hyperlinked to the PR, dimmed when stale or draft. Review state folds into the color, with magenta for changes requested and cyan for awaiting review.wt list --format=jsongainsci.numberandci.review_state. (#3041, #3044) -
Hook and alias templates render when each step runs: Templates are syntax-checked before the first step runs and rendered as each step executes, so
{{ vars.* }}always reads fresh values and hooks and aliases share one expansion model. Two consequences: a pipeline step with an undefined variable fails when that step is reached (earlier steps run first), and a template error in a background hook surfaces in the hook log rather than failing the foreground command. The background runner also labels template errors the way the foreground does. (#3042, #3047) -
wt config state cache: The regenerable caches (CI status, branch summaries, git-command caches, hints, thewt switch -target) are consolidated underwt config state cache;cache cleardrops them all without prompting, since everything regenerates.wt config state clear, which also wipes hand-authored markers and vars, now asks for confirmation;--yesskips, and non-interactive runs without--yescancel. Theci-status,hints, andprevious-branchsubcommands are deprecated but still work. (#3027) -
Statusline rate-limit segment: Above 90% of the binding window the segment shows the used percentage (
95%(8:30am–1:30pm)) instead of the pace ratio; near the cap, what's left matters more than the rate. The pace form drops the word "pace" (2.9×(Tue–Tue 5pm)), and-vvlogs each window's inputs and selection to.git/wt/logs/trace.logso the binding-window choice can be reconstructed. (#3057, #3053, #3029) -
/wt-switch-createbranch name is optional: The Claude Code plugin command picks a branch name from the task when none is given, and the skill's workflow is one route (create, then enter) with an error-driven fallback instead of three guarded paths. Worktrees created mid-session persist after the session ends. (#3058)
Fixed
-
Plugin worktree hooks fail before side effects: The Claude Code plugin's
WorktreeCreate/WorktreeRemovehooks validate the payload before runningwt; a malformed payload previously created a branch namednullor could remove the wrong worktree.wt remove's help now documents that the positional argument accepts a worktree path as well as a branch name. (#3058, #3060) -
Waiting-for-input marker covers questions, permission prompts, and turn end: The Claude Code plugin sets the 💬 marker on
AskUserQuestion, permission requests, and turn end. Previously only theNotificationevent set it, which never fires for the built-in question picker (and on some platforms not for permission prompts), so the marker stayed 🤖 while Claude waited. Part of #2916. (#3023, thanks @Ismael for reporting) -
wt list --helpno longer panics when piped: With no detectable terminal width (output piped,COLUMNSunset),wt list --helppanicked with a capacity overflow, and the post-commit diffstat truncated filenames to ~10 characters. Both now handle unknown width. (#3040) -
Deprecation warnings match
wt config update: A config deprecation warning now fires exactly whenwt config updatewould change the file. The old detection and migration logic had drifted in several places:ffandno-fftogether silently droppedno-ff, a scalarforgekey was overwritten by the[ci]migration, an empty[ci] platformmigrated without warning, and an emptyapproved-commands = []could be removed by an unrelated update. (#3055) -
Migrated
[forge]keeps[ci]'s spot: The[ci]→[forge]migration rendered the new section at the end of the file and could drop comments above[ci]; it now takes over[ci]'s position, comments included, sowt config showandwt config updatediffs stay minimal. (#3051) -
wt config state getis read-only: The aggregate dump resolved (and re-cached) the default branch, so running it right afterclearsilently repopulated the cache, and on a cold clone it could hit the network. It now reports the cached value or(none).wt config state default-branch getstill resolves and caches. (#3024)
Documentation
- Reading vs resolving cached state:
wt config state default-branch getresolves and caches; the aggregatewt config state getonly reports the cache. The help text and module docs now state the split. (#3028)
Internal
-
wt switch/wt removeorchestration moved frommain.rsto their command modules, and the hook execution call chain lost two delegation layers. (#3049, #3036) -
Config deprecations are driven by a single rule table; each rule is one function that migrates and reports what it changed. (#3045, #3055)
-
terminal_width()returnsOption<usize>instead of ausize::MAXsentinel, making the no-width case a compile-time concern. (#3043) -
Snapshot tests regenerate identically across machines: host-specific paths are guarded, host-dependent env-block markers are normalized, and help snapshots share one settings builder. (#3009, #3026, #3037, #3061)
-
Nightly CI runs lib tests across the full feature powerset. (#3059)
-
MSRV bumped from 1.94 to 1.95. (#2948)
0.57.0
Improved
-
wt step diff --branch:wt step diffgained a-b/--branchflag, mirroringwt step commit, so the diff can target another worktree's branch without leaving the current one. The branch must have a checked-out worktree. (#2995) -
Squash templates can use commit bodies: The squash commit-message template gains an experimental
{{ commit_details }}variable — a list of{ subject, body }objects for the commits being squashed — alongside the existing{{ commits }}(now documented as the commit subjects). Templates can incorporate full commit bodies, not just subject lines. (#2983, thanks @florianilch) -
Recommended Claude Code commit command drops the
CLAUDECODE=prefix: Claude Code removed the nested-session check that rejectedclaude -plaunched from inside another session, so the workaround is gone. The recommended[commit.generation]command shown bywt config createno longer carries a leadingCLAUDECODE=, andwtno longer stripsCLAUDECODEfrom the environment before running commit-generation commands. (#2979)
Fixed
-
Nushell wrapper installs where Nushell actually autoloads it:
wt config shell install nuwrotewt.nuto$nu.default-config-dir/vendor/autoload, which Nushell never autoloads — on Linux the wrapper was written but silently never loaded, sowtwas never wrapped (it happened to work on macOS/Windows by coincidence of path layout). It now installs to$nu.vendor-autoload-dirs | last, and install/uninstall clean up any worktrunk wrapper stranded at the old location. (#2992, thanks @nnutter for reporting) -
Claude Code hooks work for Fish shell users: The plugin's hook commands used
${CLAUDE_PLUGIN_ROOT}brace syntax, which Fish doesn't expand; they now use$CLAUDE_PLUGIN_ROOT, so the activity and worktree-lifecycle hooks fire correctly under Fish. (#2962, thanks @amw) -
Pager no longer wedges the terminal on Ctrl-C (Windows): Interrupting the
--helppager (less) with Ctrl-C on Windows could leave the terminal in a broken state;lessnow quits cleanly on interrupt. (#2969, thanks @ofek for reporting) -
Clearer error when the default branch has no commits: In a freshly initialized repo whose default branch is unborn,
merge/rebase/squash/push(and the diff report) failed withDefault branch main does not exist locallyplus a misleading hint to reset the cached value. They now report that the branch has no commits yet, without the wrong cache-reset suggestion. (#2990) -
diagnostic.mduploads as a gist again: The-vvdiagnostic report inlined raw NUL bytes from NUL-separated git output, sogh gist createrejected it as a binary file. Control bytes in the subprocess preview are now escaped. (#2991) -
wt listtolerates a missing index file: A repo with no<gitdir>/index(nothing ever staged) made the temp-index probe fail; a missing index is now treated as an empty one, matching git's own behavior. (#2884) -
Inline code renders in
--helpsection headings: Terminal--helpshowed literal backticks in headings authored with inline code (e.g. thewt config state logsheadingCommand log (commands.jsonl)). Headings now reduce inline code to plain text under the heading's uniform style. The--stage/--dry-runsubsection headings inwt step commit/squashwere also renamed to sentence case ("Staging", "Dry run"). (#3003)
Documentation
-
wt switchdocs give forge PR/MR URLs equal billing withpr:/mr:: The switch docs now present the full forge-URL form alongside thepr:Nshortcut. (#2970) -
New FAQ entry on moving uncommitted changes to a new worktree. (#3002)
Internal
- Bare-repo prompt opt-out stored as a hint:
worktrunk.skip-bare-repo-promptmoved under theworktrunk.hints.namespace, so it now lists underwt config state hintsand clears withwt config state clear(previously a top-level key that escaped both). Clean cutover: users who already opted out are re-prompted once on their nextwt switch --createin a dotted-name bare repo. (#3001)
0.56.0
Improved
wt listJSON output includesrepo_url: Each item now carriesrepo_url, the repository's web URL derived from the primary remote (absent when there's no parseable forge remote). It's the local checkout's repo — distinct fromci.repo_url, which is the repo a PR/MR targets. (#2941, thanks @bendrucker)
Fixed
-
Bare-repo prompts: Two UX fixes.
wt config create --projectrun in a bare repo with no linked worktrees now explains the next step (wt switch <branch>first, then create from inside the worktree) instead of failing with a generic "no worktree found". And the bare-repo worktree-path prompt no longer fires for symbolic identifiers (-,@,^,pr:/mr:), where the example paths would be misleading — it waits for the next switch to a concrete branch name. (#2951, thanks @ammachado) -
Context gauge spacing in the Claude Code statusline: The context-gauge moon emoji rendered flush against the percent (
🌕42%). Most terminals draw the emoji double-width and bleed it into the next cell, so the moon collided with the digits; it now carries a trailing space (🌕 42%). (#2944) -
Console storm on Windows: Detached background hook processes were created with
DETACHED_PROCESS, which could flash a burst of console windows. They now useCREATE_NO_WINDOWand spawn fully hidden. (#2959, thanks @nathanbabcock)
Documentation
- Codex commit-generation model bumped to
gpt-5.4-mini: The recommended[commit.generation]command for Codex — shown bywt config createand in the LLM-commits docs — now usesgpt-5.4-mini(wasgpt-5.1-codex-mini). (#2949)
Internal
- Config-deprecation layer refactor: The deprecation detector now returns a
Vec<DeprecationKind>instead of a struct of per-field flags, and the per-section config-table walks collapse into two combinators. Behavior-neutral — warning text and migration output are byte-for-byte unchanged. (#2946)
0.55.0
Improved
-
wt switchaccepts forge PR/MR URLs:wt switch https://github.com/owner/repo/pull/123now resolves the same way aswt switch pr:123, and the URL form works anywhere apr:N/mr:Nshortcut does (positional argument and--base). Detection is shape-based, not host-based — anyhttp(s)://URL whose path contains/pull/N,/pulls/N,/-/merge_requests/N, or/pullrequest/Nmatches, covering GitHub (including Enterprise), GitLab, Gitea, and Azure DevOps, including self-hosted instances. (#2898, thanks @thiagowfx for the request) -
-vvstartup pointer namesdiagnostic.md: The-vvpointer now lists the shared log directory once with all three files it will contain —trace.log,subprocess.log, anddiagnostic.md— so the diagnostic bundle is discoverable at startup rather than only when the gist hint fires at exit. The pointer verb readsWriting to …instead ofTracing to …. (#2919)
Fixed
-
Claude Code plugin keeps unmerged branches during worktree cleanup: The plugin's
WorktreeRemovehook passed-D(--force-delete) towt remove, which removes a branch even when it carries commits that aren't merged or pushed. The hook now uses the default removal: a merged or integrated branch is removed cleanly, while one with unmerged commits is kept, with awt remove -D <branch>hint for deleting it deliberately. (#2940, thanks @jbeda for reporting) -
wt listandwt step prunedegrade gracefully on unborn worktrees: A linked worktree created withgit worktree add --orphansits on an unborn branch whoseHEADis the null OID.wt listused to showworking-tree-diff (fatal: ambiguous argument 'HEAD')and a merge-tree error in its columns, andwt step pruneaborted its entire scan withfatal: Needed a single revision, blocking pruning of every other worktree. Both now treat an unborn worktree as having no commits:wt listrenders·for the commit-dependent columns, andwt step pruneskips it (as it does locked worktrees) and continues. (#2937, thanks @nedtwigg for reporting)
Documentation
- Doc-site and
--helpprose cleanup: A writing-prose pass across the FAQ, config, list, remove, Claude Code, hook, LLM-commits, and Tips & Patterns pages (some via--helptext inafter_long_help). (#2922, #2925)
Internal
- Collapsed duplicated code paths and removed dead code left by completed cut-overs: A net reduction across the shell, git, and command layers. (#2931, #2932, #2934)
0.54.0
Improved
-
Rate-limit pace segment in the Claude Code statusline:
wt list statusline --format=claude-codenow surfaces a yellow1.3×pace(10am–3pm)segment when Claude Code's reported five-hour or seven-day rate-limit window is on track to be hit before its reset. The segment uses a Bayesian forecast onP(final ≥ 100%)so early-window bursts (e.g., 5% used at 3% elapsed) don't trigger spurious warnings — only the worse-projected of the two windows is shown, and the segment is hidden entirely when both are safe. The clock format inside the parentheses honorsLC_ALL/LC_TIME/LANG:en_US/en_PH/en_CAget 12-hour (10am–3pm), everything else (including unset andC/POSIX) gets 24-hour (10:00–15:00). (#2899, #2911) -
wt step prunestreams removals and never prompts mid-scan: Prune now bundles integration, removability, and age checks into a single parallel pass and starts removing candidates as soon as they qualify, instead of batching the scan and acting at the end. Dirty / locked / primary worktrees drop out before the age check, so a young + dirty + integrated worktree no longer surfaces asSkipped (younger than 1d)— the(younger than X)message now fires only when the worktree would actually have been pruned. With--yes, every project command is auto-approved as before; without--yes, a candidate whose hooks include an unapproved project command is SKIPPED with(approval required)rather than aborting the scan with an inline prompt the streaming structure couldn't accommodate. The end-of-run hint enumerates the unapproved hook templates from the invoking worktree's config and emits one copy-pasteablewt -C <path> removeline per skipped candidate, annotating candidates whose own.config/wt.tomldiffers from the invoking worktree's. (#2908, #2910) -
wt step prunedefault--min-ageraised from 1h to 1d: A worktree just created from the default branch looks "merged" because its branch still points at the same commit; a one-day floor keeps an unattended prune from sweeping it up before its owner starts work. Explicit--min-age=0sor any other value is unchanged. (#2886) -
Legacy shell-wrapper deprecation warning: Users who upgrade
wtwithout restarting their shell still run the previous release's wrapper, which sets onlyWORKTRUNK_DIRECTIVE_FILEinstead of the new splitWORKTRUNK_DIRECTIVE_CD_FILE/WORKTRUNK_DIRECTIVE_EXEC_FILEpair. That fallback used to be silent;wtnow emits a one-shot per-process warning hinting atwt config shell install. bash, zsh, fish, and PowerShell pick up the new wrapper on the next shell restart; nushell is the one shell where users must rerunwt config shell install nubecause its wrapper is a static file. (#2880) -
-vvno longer floods stderr; raw subprocess sink renamed tosubprocess.log: A-vvinvocation used to spray ~15K lines of stderr per command, forcing a redirect to a file even thoughtrace.logalready mirrored most of it. The debug-levellog::*pipeline now writes to.git/wt/logs/trace.logat-vv; Info-level records (hook output, template expansions, theWriting to …pointer) stay on stderr at every verbosity level. The companion raw-subprocess-bytes file is renamed fromoutput.logtosubprocess.log— the prior name read as "stuffwtprinted" but actually held uncapped multi-MB subprocess bodies (git log -p, patch-id pipelines).RUST_LOGis now honored at every verbosity level:wt -vandwt -vvpreviously hardcoded Info / Debug and silently dropped anyRUST_LOGdirective (RUST_LOG=trace wt -vvwas capped at Debug,RUST_LOG=worktrunk=trace wt -vwas ignored); all three levels now flow through one builder shape withRUST_LOGlayered on top of the flag baseline. (#2892, #2901, #2913)
Fixed
- Data-safety across the worktree merge / remove / prune lifecycle: Five TOCTOU and scoping fixes.
wt mergeandwt removerevalidate cleanliness and branch integration afterpre-removehooks run, so a hook (or concurrent process) that dirties the worktree or advances the branch can no longer trash the directory or triggergit branch -Dagainst the stale pre-hook decision. The background-removal path does the same revalidation and fails closed for submodule worktrees on the fallback path instead of forcing.wt step pruneruns its rename-failure fallbackgit branch -dsynchronously under the write guard for non-current worktrees (no more race against live integration readers on.git/config), scopes hook approval to the worktrees it will actually remove (an unrelated unapproved hook can no longer abort a non-interactive prune), and prunes the stale metadata of detached worktrees whose directory was deleted outside Worktrunk. (#2870)
Documentation
-
-v/-vvhelp and FAQ: The-vhelp text is split out from its 150-character parenthetical, anddocs/content/faq.mdgains a "What does-v/-vvdo?" section with a three-level table. (#2913) -
Hook docs and
pre-startdocstring: Thepre_createfield docstring inHooksConfig(user-visible via generated JSON schema) read "Commands to execute before worktree creation";pre-startactually runs after worktree creation, blocking. Restored the correct wording. Thedocs/content/extending.mdoverview is reworked to read more cleanly: parallel three-paragraph intro for hooks / aliases / custom subcommands, fewer em-dashes, and the Reference section's table no longer duplicates content covered in prose. (#2879, #2912)
Internal
-
Migrated logging from
env_loggertotracing-subscriber: A layered subscriber routes records structurally by target and verbosity. Existinglog::*callers are bridged intotracingviatracing_log::LogTracer.[wt-trace]records emit as typed structured fields with a single formatter rendering the wire shape, so the grammar lives in one place instead of being duplicated across every emit site. -
UncommittedChangeserror renders dirty files in the canonical gutter, matchingConflictingChangesand the project'sformat_with_gutter()convention. (#2887)
0.53.0
Improved
-
wt switch --executedeprecates shell command lines: A future release will switch--execute(-x) to an argv input model — a single program, with arguments after--, run with no implicit shell. This release is the warn phase:-xnow warns when its value is a shell command line, multiple words, or template markup, and the hint shows a copy-pasteable migration (--execute sh -- -c '…') plus a link to comment on the cutover if the new form would regress a workflow. A single program name stays silent. (#2852, #2863) -
wt config showreports the project identifier: The PROJECT CONFIG section now prints the project identifier (<host>/<owner>/<repo>from the primary remote, or the canonical repo path), so you can find the key for a[projects."…"]block in your user config without deriving it by hand.wt config show --format=jsongains a matchingidentifierfield. Closes #2826. (#2827, thanks @airtonix for the request) -
Gemini CLI extension detection:
wt config shownow renders a GEMINI CLI section reporting whether the worktrunk Gemini extension is installed. The agent-integration docs gained install instructions for OpenCode and Gemini CLI alongside Claude Code and Codex. (#2819) -
pre-create/post-createhook aliases: The worktree-creation hookspre-start/post-startnow also acceptpre-create/post-createas silent aliases — in config (top-level,[hooks.*], and per-project sections, in string, table, and array-of-tables form) and on thewt hookcommand line. Docs continue to recommendpre-start/post-start; the canonical names may switch in a later release. Full plan: #2838. (#2840, #2857)
Fixed
-
Hooks resolve project config from the invoking worktree: Worktrunk resolved each hook's
.config/wt.tomlfrom a different worktree depending on the hook, andwt switch --createread the base ref's committed config viagit show— so an uncommitted or branch-local.config/wt.tomlsilently failed to fire creation hooks, andwt config showdisagreed with what actually ran. Every hook now resolves its commands from the.config/wt.tomlof the worktreewtran in — the same filewt config showdisplays. In the common case of a committed, repo-wide config this is unchanged; it diverges only when a branch carries its own working-tree edits. Fixes #2856 and #2818. (#2873, thanks @Oxygen66 and @sirianni for reporting) -
Picker prompts for approval before running project
pre-switchhooks: Selecting a worktree in the interactive picker (wt switchwith no argument) ran a project-definedpre-switchhook from.config/wt.tomlwithout the approval prompt that gates every other hook — unapproved code from a freshly cloned repo executing silently. The picker now routespre-switchhooks through the same approval gate aswt switch <branch>and as its ownpost-switch/pre-start/post-starthooks. (#2858) -
Interactive picker switches with
cd = false: With[switch] cd = false(orwt switch --no-cd), opening the picker (wt switchwith no branch argument) and selecting a worktree printed the branch name and exited — no switch, no hooks, andAlt-ccreated nothing. The picker now runs the same switch pipeline aswt switch <branch>, suppressing only the cd directive:pre-switch/post-switchhooks fire andAlt-ccreates the worktree.--format=jsonworks in the picker too, and replaces the old print-only output for scripting — it both switches and prints a structured result (action,branch,path) to stdout. (#2845, thanks @endigma for the discussion in #2837) -
alt-rin the picker removes the right worktree: The interactive picker identified each row by branch name for itsalt-rremoval signal; detached worktrees all report(detached), so two detached rows collided andalt-rcould remove the wrong worktree. Rows backed by a worktree now carry a unique path-based identity. (#2866) -
--clobberbacks up blocked paths atomically:wt switch --clobberandwt step relocate --clobberback up a path blocking the target before clobbering it. Both used anexists()check followed bystd::fs::rename, which silently overwrites an existing destination — a time-of-check/time-of-use race that could destroy a just-created backup. They now share one helper that moves the blocker with an atomic no-overwrite rename and counts up through-2,-3, … suffixes on a name collision instead of failing. (wt step relocate's backup name changes from.bak-<timestamp>to the extension-aware.bak.<timestamp>form.) (#2849, #2865) -
Squash-merge detection ignores
diff.*git config: Worktrunk's squash-merge integration check comparedgit patch-idhashes computed from two different diff generators — one plumbing (ignoresdiff.*config), one porcelain (honors it). For anyone with a non-defaultdiff.contextordiff.algorithm, the two never agreed, so a genuinely squash-merged branch was reported as not integrated — breakingwt remove("Branch unmerged"), thewt listintegration symbol, andwt step prune. Both sides now use plumbing, immune to everydiff.*setting. (#2821) -
Wedged and orphaned fsmonitor daemons are reaped: With
core.fsmonitor=true, git runs a per-worktreegit fsmonitor--daemon; a wedged one stops answering its IPC socket — which hangsgit statusandwt list— and ignores thestoprequestwt removesends, so it leaks once its worktree is gone (dozens can accumulate).wt removenow resolves the daemon's PID from its IPC socket and force-terminates it (SIGTERM, brief wait, SIGKILL) whenstopdoesn't take, and its background internal sweep additionally reaps any daemon whose socket no longer resolves to a live worktree — covering daemons orphaned bygit worktree remove, a manualrm, or a crashedwt. A daemon serving a live worktree is never reaped. (#2813, #2814) -
Config migration no longer silently drops deprecated config: A deprecated section (
[commit-generation],[select], …) was discarded without writing its canonical replacement when the canonical key already existed as a scalar or an inline-table value — real data loss, now fixed for both shapes. Deprecated template variables are rewritten only inside{{ }}/{% %}tags, so literal command text and{% set %}locals are left intact. System config now passes through the same deprecation-warning gate as user config. (#2788, #2851) -
Hook filtering and the command-approval store are hardened:
--only project:deploy user:lintmatched filter names across the project/user split, so a name given for one source could select an unintended hook from the other; the approval gate and executor now share one source-scoped predicate. Template variables are detected by parsing the template rather than substring matching ({{ vars["env"] }}and bare{{ vars }}were missed), and an undefined variable in a{% if %}predicate is now a clear error instead of being silently ignored. The approvals trust store is written atomically, rejects unknown keys instead of silently dropping approvals, and its migration is locked and validated before it runs. (#2841) -
wtno longer panics on non-UTF-8 arguments, and--formatpassed to a config-state write action now reports the conflict through normal error handling instead of exiting before diagnostics and output run. (#2788) -
Picker,
wt switch, and statusline correctness: The picker now plans eachalt-rremoval against fresh repository state rather than a cache left stale by the previous removal.wt switchprefers an exact local branch over stripping a remote prefix (a local branch literally namedorigin/foowas retargeted), and fails closed on a malformedforge.platforminstead of silently falling back to GitHub. A single-row statusline skips the repo-wide ahead/behind scan, a speedup on large repositories. (#2842) -
Shell-correct escaping for the
--executepayload:wt switch -xbuilds its payload as a shell-escaped string evaluated by the active shell wrapper. POSIX single-quote escaping was applied unconditionally, but PowerShell (Invoke-Expression) and fish (eval) don't share POSIX quoting — under fish a backslash in the payload was silently dropped and a trailing backslash aborted evaluation, and under PowerShell the'\''idiom is invalid. Escaping now keys on the active directive shell. Separately, every othershell_escapecall site is pinned to POSIX escaping rather than the crate's platform-sensitive entry point, which on Windows could pick cmd-style quoting that mis-escapes arguments spliced into a POSIX shell. (#2843, #2815) -
wt hook showlists per-project user hooks:wt hook showdisplayed only global user hooks, omitting per-project hooks defined under[projects."…"]in the user config; it now merges both, matching what actually runs. (#2844) -
Statusline reserves a fixed margin instead of 20% of width: When
wt list statuslineruns as a Claude Code subprocess it can't detect the terminal directly and walks the process tree for a TTY; that fallback reserved 20% of the detected width for Claude Code's own UI, giving up 40 columns on a 200-column terminal. It now reserves a fixed 5 columns. (#2871) -
User-output consistency: An audit against the project's output conventions corrected six messages — state-acknowledging messages ("All shells already configured", the version-check "Up to date") use the info marker rather than success; the
wt step relocatesummary keys its message type on whether anything was relocated; "Diagnostic saved" reports as a success with the@-path convention; and a stray trailing period and a cross-message pronoun were removed. (#2867) -
Repo-wide internal hook logs are written as top-level files: Branch-agnostic internal-operation logs were written into a top-level
internal/directory, whichwt config statethen misclassified as a branch; they now write tointernal-{op}.logfiles alongside the other shared logs. (#2851) -
Nix flake includes
gemini-extension.json: The flake's source filter omittedgemini-extension.json, so a Nix build produced a package missing the Gemini CLI extension manifest. (#2834)
Documentation
-
wt remove --forcehelp and FAQ: Both said--forceoverrides the untracked-files check "for build artifacts";--forceactually discards staged and modified tracked files too. The help text and FAQ now state that--forcediscards staged, modified, and untracked files. (#2869) -
cmux recipe: Re-added a verified cmux integration recipe to Tips & Patterns. (#2836, thanks @endigma for the verified config)
0.52.0
Improved
-
wt step tether: New[experimental]operation that runs a command in its own process group and kills the whole group when the command exits or its worktree is removed (a 250ms portable poll —killpgon Unix,taskkill /T /Fon Windows). A singlepost-starthook (wt step tether -- npm run dev) replaces the usualpost-start-to-launch /pre-remove-to-stop pair, and unlikepre-removeit also cleans up after agit worktree remove, anrm -rf, or a crashed hook — the leak path that eventually saturates macOSfseventsd. Arguments after--run directly with no shell, matchingwt step for-each. (#2785) -
Gemini CLI extension: Worktrunk now ships a Gemini CLI extension for
wt listactivity tracking, installable withgemini extensions install max-sixty/worktrunk. The extension's manifest, hooks, and skills resolve at the repo root, so the GitHub-name install path works without a local clone. (#2803, #2807, thanks @rafavital for the request in #2763)
Fixed
-
Project hooks are frozen at the approval gate: A project-defined
pre-*/post-*hook command was selected from.config/wt.tomltwice — once to build the approval prompt, once at execution — and the operation itself mutates state between the two reads (a merge moves the target ref, an auto-rebase rewrites the feature config, a removal scrubs the worktree,git worktree addmaterializes a--createworktree). The second read could select a command the user never approved; on a freshly cloned repo that is unapproved code execution. The gate now freezes the command set into an immutable plan that the executor consumes verbatim, so post-operation hooks can never run an unapproved command. Behavior is otherwise unchanged, andwt merge --no-hooks(or a declined/empty plan) now returns before loading approvals, so a malformedapprovals.tomlno longer aborts a command that had nothing to authorize. (#2806) -
Declining the
wt mergecommit-append no longer skips hooks: When a project'spre-merge/post-mergehooks were already approved,wt mergebundled the commit-message append into the same prompt; declining the lone append prompt skipped every hook for that run even though the user only meant to skip the append. The append is now gated on its own path (the same onewt step commit/wt step squashuse), so declining it drops only the append. On a fresh repo where both the hooks and the append are unapproved this is now two prompts instead of one bundled prompt. Decline messages are also canonicalized acrossmerge,remove,prune, andswitch(Commands declined, … without hooks). (#2802) -
Windows
wt step prune.git/configrace:wt step prunecould intermittently fail on Windows withunable to access '.git/config': Permission denied— its parallel branch-integration checks read.git/configwhile an inlinegit branch -Drewrote it via git's lockfile rename, which on Windows briefly blocks concurrent readers. Branch-integration reads are now excluded from overlapping thegit branch -Dthat rewrites config. (#2808)
0.51.0
Improved
-
Codex support: Worktrunk now ships a first-class Codex plugin alongside the Claude Code one.
wt config plugins codex installinstalls it;wt config show --fullreports its state. The Codex plugin bundles the shared configuration skill — documentation the agent reads to help set up LLM commits, hooks, and troubleshooting. Codex exposes no turn-end or worktree-lifecycle hooks, so unlike Claude Code it has nowt listactivity tracking or session worktree isolation; the skill is the integration. Docs (#2512, thanks @douglas; #2780, #2782, #2786) -
Experimental project-level commit-message guidance:
[commit.generation] template-appendadds to the commit/squash LLM prompt instead of replacing it (templatestill replaces), and is now valid in both user config and project.config/wt.toml. The user fragment renders into a<user-guidance>block (no approval); the project fragment renders into a gated<project-guidance>block bundled into the existingwt mergehook-approval prompt — declining is non-fatal. User-only commit-generation keys placed in project config still get the "this belongs in user config" redirect. (#2774, #2790, thanks @gabimoncha for the request in #2758) -
/wt-switch-createtakes an optional repo and a--task delimiter: The Claude Code skill now accepts<branch> [<repo>] [-- <task>]. The optional second token names a different repository to create the worktree in;--cleanly separates the task from the rest. Without a--, a path-shaped second token is treated as the repo and the remainder as the task. (#2751) -
wt list --fullandwt statuslinecount untracked files inHEAD±: TheHEAD±working-diff segment now includes untracked-file lines under--fulland inwt statusline, matchingwt step diff. Defaultwt listand the picker stay on the cheap tracked-only path. (#2764) -
wt step pruneandwt listno longer stall on long-divergent branches: The patch-id squash-merge scan (is_squash_merged_via_patch_id) rangit log -pover the entire target-side history — tens of thousands of commits on a fast-moving repo, taking seconds to tens of seconds. It's now capped at 500 commits via a cheap graph-only pre-flight; above the cap the check returns "not squash-merged" (the safe answer). (#2752)
Fixed
-
Config deprecation-layer correctness: Three independent fixes — structural migration now preserves unrelated
[ci]keys and unparsable deprecated sections instead of discarding them;config updateaborts on an approvals-copy I/O failure instead of silently dropping approvals; deprecated template-variable renaming rewrites the parsed TOML tree instead of doing a raw text replace that corrupted occurrences inside escaped strings. (#2783) -
Data-safety and correctness fixes: Six independent single-file fixes —
wt step relocate --clobberrefuses to overwrite an existing backup;wt removere-checks cleanliness immediately before a forced submodule removal (time-of-check/time-of-use);wt switchre-discovers the base worktree via a freshRepositoryafterworktree addinstead of reading a stale cached list;wt switch pr:<N>derives owner/repo from the forge remote rather than the primary remote and validates an empty Azure source branch at the provider boundary; user-controllable branch operands are guarded with--. (#2784)
Documentation
- Alias template timing: A new Aliases subsection documents that templates render at alias dispatch using the invoking worktree's context, so a nested
wtcommand's own template variables resolve against the outer worktree unless wrapped in{% raw %}…{% endraw %}. Fixes #2753. (thanks @viicslen for reporting) (#2754)
Internal
- The Claude Code and Codex plugin payloads are consolidated into one shared directory. (#2789)
WORKTRUNK_BOT_TOKENis renamed toTEND_BOT_TOKENin non-tend CI workflows. (#2781)
0.50.0
Improved
-
Experimental Azure DevOps support:
wt switch pr:<N>resolves Azure DevOps pull requests via theazCLI — auto-detected fromdev.azure.com/ssh.dev.azure.com/*.visualstudio.comremotes, or pinned with[forge] platform = "azure-devops".wt list --fullsurfaces Azure DevOps PR and pipeline CI status, andwt config show --fullreportsazinstall/auth state when Azure DevOps is the detected platform. Requires theazure-devopsCLI extension. GitHub still wins in mixed-remote setups. (#1256, thanks @mikeyroush; thanks @dlecan for #1144) -
Experimental Gitea support extended to
wt listandwt config show:wt switch pr:<N>already resolved Gitea PRs via theteaCLI;wt list --fullandwt config show --fullnow recognize Gitea repos too —wt list --fullshows a CI indicator (open-PR conflicts plus the PR head commit's combined status, falling back to the branch's latest status when no PR exists) linked to the PR, andwt config show --fullreportsteainstall/auth state. (#1320, thanks @SjB; #2702, #2707, #2732) -
Hooks resolve project config from the worktree they act on — no primary-worktree fallback: Previously
pre-removeread the primary worktree's.config/wt.tomlrather than the worktree being removed, andpost-remove/post-switchread the post-removal working directory's config, so apre-remove(orpost-remove) you added on a feature branch never fired when removing that branch's worktree. Now every hook reads the.config/wt.tomlof the worktree it acts on —pre-remove/post-removeread the removed worktree's config (snapshotted before removal),post-switchreads the destination worktree's — and the approval prompt collects hook commands from that same worktree, so a branch-localpre-removealways appears in the prompt before it runs. A worktree with no.config/wt.tomlruns no project hooks; a present-but-malformed config aborts the operation with the parse error instead of silently using the primary's. (Breaking: removing a worktree no longer runs the primary worktree'spre-remove/post-removehooks unless they're also defined in the removed worktree's.config/wt.toml. For an existing worktree that predates a hook added on the default branch, copy the hook into that worktree's.config/wt.tomlto restore the previous behavior; new worktrees branched off the default branch pick it up as before.) (#2690, #2703, #2714, #2717, #2701, #2708, #2727, #2736, #2748) -
wt config alias showwith no name lists every alias:wt config alias show <name>shows one alias's full definition; with no name it now prints that same○ Alias <name> (<source>):block for every configured alias, in name order.wt --help(andwt step --help) drop the inline aliases table for a compact names-only list that points here. (#2684, #2691, #2688) -
wt switchpicker validates hook templates before creating the worktree: Creating a branch from the picker (Alt-C) now runs the same template pre-flight thatwt switch --createdoes, so a project config with a broken hook template (syntax error, undefined variable) fails beforegit worktree addinstead of after — no orphaned worktree left blocking a re-run with the same name. (#2712) -
wt list --branchesskips a serial graph walk on warm runs: Branch ahead/behind counts for themain↕andRemote⇅columns are now cached SHA-keyed, so a warmwt list --branchesno longer pays the single-threadedgit for-each-ref --format='%(ahead-behind:…)'walk that ran before the parallel task pool opened — on a large repo (rust-lang/rust) that walk was ~40% ofwt list's wall time. The push-remote URL is cached and the local-branch scan is shared withcapture_refs, removing two more duplicatefor-each-refinvocations per render. (#2704, #2718, #2673) -
Claude Code plugin ships the
wt-switch-createskill: Installing the worktrunk Claude Code plugin now provides/wt-switch-create <branch> [task], which callsEnterWorktreeto create (or re-enter — it's idempotent) a worktree in worktrunk's sibling layout (<repo>.<branch>/) and re-roots the current Claude session into it; anything after the branch name runs as the task there. (#2737, thanks @onetom for #2631) -
Paths in warning messages are bold: Config-file-not-found, the legacy-fish-config removal warning, completions-not-configured, and outdated-shell-extension warnings now bold the path they mention, matching the convention used elsewhere. (#2677)
Fixed
-
Ctrl-C against a concurrent alias reports exit code 130, not 143:
wt step <concurrent-alias>interrupted by Ctrl-C could exit with 143 (SIGTERM) instead of 130 (SIGINT) when, under load, wt's SIGINT→SIGTERM escalation timer (200ms) fired before the child finished dying from the SIGINT. wt now records the user's originating signal and reports that as its exit code. (#2724) -
Branch names starting with
-no longer confuse git: A branch literally named-foo(creatable viagit update-ref refs/heads/-foo HEAD) could be misparsed as an option by the git subcommands wt invokes — most visibly the hook-approval gatewt switchruns before creating a worktree at such a ref, which reads.config/wt.tomlviagit show <ref>:…, andwt step relocate'sgit worktree add. User-controllable refs now pass--end-of-options(and--verifyforrev-parse) so git treats them as data. (#2711, #2725, #2738) -
wt switchpicker'salt-rremoval no longer runs unapproved project hooks: The picker's removal path was a parallel reimplementation ofwt remove's teardown that ranpre-remove/post-remove/post-switchhooks unconditionally, bypassing the approval gate every other removal path goes through. The picker now routes through the sharedhandle_remove_output(with asilentflag for the in-skim case) and consults the existing approval state read-only — unapproved hooks are skipped, approved ones run. Removing the current worktree viaalt-ralso registerspost-switchhooks against the home worktree now, matchingwt remove/wt merge/wt step prune. (#2746) -
Nushell and PowerShell shell-extension label:
wt config showand the--dry-run uninstallpreview labeled Nushell and PowerShell inconsistently; they now readshell extension & completionslike Bash and Zsh — Fish is the only supported shell whose completions live in a separate file. (#2699, #2705) -
Bash hook syntax highlighting no longer corrupts paths:
format_bash_with_gutterswapped Jinja{{ }}delimiters for internal placeholders that could collide with text in the command (e.g. a Windows tempdir path), splitting it mid-stream in--helpand snapshot output. The placeholders are now chosen to be collision-free. (#2722)
Internal
- Foreground signal forwarding (Ctrl-C handling for hook pipelines,
wt step for-each, concurrent alias groups) is unified into one module. (#2734) CiPlatformmoved to the library crate and is cached onRepoCache; the "override" framing around[forge] platformis dropped. The "invalid CI platform in config" warning is also deduplicated to once perwt listrun instead of firing per branch. (#2692, #2686)- The remove-hook approval helper is shared across
wt remove/wt merge/wt step prune, and thefor-each-refscan primitive is shared with the remote-inventory cache populated from the snapshot path. (#2709, #2735) - CI fails the build if tests leave files behind in the working tree;
LLVM_PROFILE_FILEdefaults to a temp-dir path when not inherited, keeping coverage runs from dropping*.profrawat the repo root. (#2719, #2730, #2713) codename_indexkeeps its hash inu64before narrowing, so thecodenamefilter picks the same word on 32-bit and 64-bit builds (no-op on 64-bit). (#2667)
0.49.0
Improved
-
New
codename(n)template filter: Produces deterministic friendly names from any input string —codename(1)returns a noun,codename(2)returnsadjective-noun, higher counts add more adjectives. The pool is large enough (~1.26M combinations forcodename(2)) that the result usually stands alone as a worktree leaf, e.g.worktree-path = "{{ repo_path }}/../{{ repo }}.{{ branch | codename(2) }}". (#2641, thanks @endigma) -
Picker preview disk cache: The
wt switchpicker now caches Log, BranchDiff, and UpstreamDiff previews to disk under.git/wt/cache/picker-preview/, keyed by SHA + width, so repeat invocations skip thegit log/git diffsubprocesses they already paid for. The Log cache splits SHA-deterministic raw output from the dim/bright styling that depends onmain's position, and a background refresh worker rewrites stale entries after every disk hit so the next visit sees up-to-date ref decorations. State integration bundles the new cache with the existing git-commands cache forwt config state get/state clear --all. (#2628, #2646) -
Picker fans out fewer
git rev-parsecalls:compute_branch_diff_previewpreviously forkedgit rev-parse <default_branch>once per item to key its disk cache. A newRepository::default_branch_sha()reads the SHA from the already-cached branch inventory, collapsing N redundant subprocesses down to one. (#2658) -
"X has uncommitted changes" errors now list the dirty files:
GitError::UncommittedChangescarries the porcelain lines fromgit statusand the renderer prints them between the title and hint, so users hitting the error onwt remove,wt mergecleanup,wt step promote, orwt merge --no-commitsee exactly what's blocking without re-runninggit status. No new git subprocesses —is_dirty()was already runninggit status --porcelainand discarding the output. (Breaking:GitError::UncommittedChangesgained adirty_files: Vec<String>field.) (#2653)
Fixed
-
wt switchintegrates withcdaliases like zoxide: Previously the shell wrapper invoked thecdshell function directly, so users withcdaliased to__zoxide_zor similar sawzoxide: no match foundwhen switching to a fresh worktree. The bash and zsh wrappers now usebuiltin cdto bypass the alias. Fixes #2643. (#2644, thanks @xkumiyu for reporting) -
Empty hook tables no longer panic during
wt switch --create: A config like[post-start]with no entries below it would crash the backgroundHookAnnouncerpath. The config layer now treats an empty hook table as zero steps. Fixes #2634. (#2635, thanks @topit for reporting) -
Repositoryaccessors no longer leak the process CWD:Repository::current_worktree()resolved against the global CWD instead of the Repository's own discovery path, so callers usingRepository::at(p)(output handlers, picker, recovery, tests) silently got aWorkingTreeat the process CWD rather than atp. Five sibling sites had the same bug —project_config_path,project_config,require_current_branch,resolve_worktree_name("@"),resolve_worktree("@"). Fixed at the helper itself, replacing PR #2625's per-site workaround. (#2652) -
WorkingTree::is_linkedtolerates non-git CWDs: When wt's process CWD was outside any git repo (e.g. the Nix build sandbox),infer_default_branch_locallywould error out trying to callis_linkedon a non-git path. The check now returnsfalseinstead. Fixes #2624. (#2625, thanks @DArtagan for reporting) -
Picker no longer overlays warnings on the active TUI: Warnings emitted by
collect::collect(stale default branch, batch-fetch failure, drain timeout) used to print to stderr from a background thread while skim's TUI owned the terminal, corrupting the rendered frame and leaving fragments visible after the user picked. Warnings now stash through the picker and drain to stderr after skim releases the terminal. The drain-timeout warning is also subcommand-agnostic instead of hardcodingwt list. (#2627) -
Picker clears its frame on exit in inline mode: On some Linux terminals the picker's rows remained visible after pressing Enter because skim emitted an unmatched
rmcup(alternate-screen toggle) instead of an explicit erase. Setting skim'sno_clear_startoption forces the explicit erase path. (#2626) -
Failed
git logno longer poisons the picker's preview disk cache: When the underlyinggit logsubprocess errored, the picker still wrote the error string into the preview cache, so the next read served the stale failure instead of retrying. The write is now skipped on failure. (#2651) -
wt step prunesays "removing branch" for branch-only candidates: The error context wrapping everytry_removecall was hardcoded to "removing worktree for X", which misled users when prune was actually removing an orphan branch with no worktree attached. (#2619) -
Claude Code Windows integration uses a cross-platform wrapper: The plugin's
wtinvocation collided with Windows Terminal's built-inwt.exealias, opening Windows Terminal windows instead of running the wt CLI. A newwt.shwrapper script tries the standard Worktrunk binary names (wt,git-wt) and dispatches to the right one across pwsh, Git Bash, and WSL. (#1754, thanks @lucaspimentel) -
wt config plugins opencode installwrites to~/.config/opencode/on macOS: The install path fell through todirs::config_dir(), which resolves to~/Library/Application Support/opencode/on macOS — but that's OpenCode's managed settings directory; user plugins belong under~/.config/opencode/. The path now follows OpenCode's documented precedence:$OPENCODE_CONFIG_DIR > $XDG_CONFIG_HOME/opencode > ~/.config/opencode. Linux behavior is unchanged. Fixes #2654. (#2655, thanks @gwenwindflower for reporting)
Documentation
- cmux workspace integration recipe: Adds cmux to the Agent handoffs section and documents a per-worktree workspace recipe with create/select/close lifecycle hooks. Includes the key gotcha — cmux's socket restricts access to processes with cmux terminal ancestry, so
pre-*hooks must be used instead ofpost-*. (#1907, thanks @alvistar)
Internal
- Consolidated slow CI checks (Nix flake build, Windows long-tail) onto a
nightlyworkflow triggered by anightlyPR label, keeping the PR-blocking suite faster. (#2630, #2636, #2645, #2647, #2648) cargo-affectednow uploads itsreport.jsonas a build artifact for downstream tooling. (#2621)- Test isolation hardening:
wt_command()defaults to an isolated tempdir so tests no longer inherit the process CWD, and thecurrent_or_recovertest was decoupled from the inherited CWD. (#2642, #2649)
0.48.0
Improved
-
--format=jsonextends to seven more commands:wt step rebase,wt step push,wt step commit,wt step squash,wt step relocate,wt step copy-ignored, andwt hook shownow accept--format=json. Shapes follow the existing pattern (additive on stdout; human prose stays on stderr) and use stable snake_caseoutcomediscriminators where the result has multiple variants. (#2560) -
wt step commitandwt step squashgain--dry-run: Renders the prompt, prints the shell invocation that would call the LLM, calls the LLM and prints the generated message in three labeled sections (PROMPT, COMMAND, MESSAGE), then exits without staging, running hooks, or committing. Forcommit,--stageis honored against a temporary index — the previewed prompt matches what a real run would send the LLM, but the user's real index is never touched.--show-promptis now hidden from--helpbut kept working for piping the rendered prompt to another LLM. (#2557) -
New
dirnameandbasenametemplate filters: Two new filters exposePath::parentandPath::file_name, enabling path traversal that previous filters couldn't express. They unblock the bare-repo-in-hidden-directory layout (myproject/.git), where{{ repo }}resolves to.git: users can write{{ repo_path | dirname | basename }}to recovermyproject. (#2592, #2605, thanks @seakayone for reporting #1279 and @Xilis for raising theparent_dirquestion) -
New
[remove] delete-branchconfig option: Settingdelete-branch = falsedefaultswt removeto keeping branches, equivalent to passing--no-delete-branchevery time. CLI flags still override the config either direction. (#2589, thanks @jameslairdsmith for #2587) -
wt-perf timelinesubcommand for trace capture: One command runswt, captures stderr, parses[wt-trace]records, and prints a column-aligned text timeline (sorted by start time, with subprocess totals and externally-measured wall) or emits Chrome Trace Format JSON for Perfetto. Replaces the previousRUST_LOG=debug wt … 2>&1 | wt-perf trace > trace.jsondance. (#2558) -
wt listskips redundant merge-tree probes on dirty worktrees: For dirty worktrees with no unmerged entries, the dirty-tree probe is authoritative and the HEAD-only probe is skipped — one merge-tree subprocess per dirty row instead of two. The dirty probe also reflects the current working state, so when uncommitted changes resolve a HEAD conflict,wt listno longer reports it as conflicting. (#2602) -
Faster alias dispatch: Two changes compound to cut warm alias-dispatch latency by ~25 ms —
Repository::prewarmoverlaps the three independent pre-dispatch reads (rev-parse, git config, user-config TOML) on scoped threads instead of running them in series, andbuild_hook_contextonly executes the four shell-out blocks (default_branch,primary_worktree,commit/short_commit,remote/remote_url/upstream) when the alias body actually references those template variables. (#2556, #2573) -
Short-SHA display honors
core.abbrev: Sites that abbreviated a commit SHA previously sliced&sha[..7]or ran ad-hocgit rev-parse --shortcalls — 7-char prefixes regularly collide in larger repos and none of the slicing sites respectedcore.abbrev. Thestep commit/step squashsuccess lines, thestep push --no-ff"Merged to" line, the{{ short_commit }}template variable, post-remove hook context, the safety-backup ref display, the orphan-check(detached <sha>)label, andwt listrow display all route through one canonical helper now. (#2576, #2577, #2584) -
Shell-integration hint escalates after repeat showings:
worktrunk.hints.<name>migrates from"true"to an integer counter so the system tracks how many times a hint has been displayed. After 5+ displays of the shell-integration install hint, it appends awt config showpointer so users who keep seeing it can investigate why their wrapper isn't intercepting. Legacy"true"values parse as 0, so the next display normalises to 1; first-time-skip behaviour is unchanged. (#2603) -
Cleaner
wt config showshell-integration section for new users: Several follow-ups smooth the section's first impression. "Not configured" rows render as peer status lines (○) with bold shell name, matchingAlready configuredandSkipped, instead of looking like a sub-bullet. Thetype wtverification hint only fires under the user's actual shell, not under every configured shell. On a stock zsh-only macOS,bash/fish/nuno longer render fourSkipped; ~/.foorc not foundrows — a newShell::is_installed()PATH lookup filters them unless the binary is present. The status text now distinguishes "not configured" (no working integration anywhere) from "not active" (installed but not loaded in this session), with the install hint moved directly under the warning, and theSkippedrow's shell name renders bold to match other status rows. (#2562, #2572, #2574, #2579)
Fixed
-
wt step pruneno longer trips a debug_assert on multi-line git errors: Whengit configfailed mid-prune, the multi-line stderr propagated as a top-level anyhow message with an empty chain — exactly the caseprint_command_error'sdebug_assert!(false, "Multiline error without context")is designed to nag on. Debug builds (includingcargo test) exited 101 instead of rendering the error. Targeted.context(...)wrappers on the prune call sites route prune errors through the structured rendering path. (#2567) -
wt config updateno longer prints a redundantwt config updateself-suggestion: Everywtinvocation against a deprecated config emitted a deprecation warning followed by ato apply updates, run wt config updatehint — silly when the user was already runningwt config update. The update command latches warning suppression beforeRepository::prewarm, then renders per-pattern warnings inline alongside its diff.--printis also fully silent on stderr now, matching its pipe-friendly intent. (#2590) -
wt config showiterates PowerShell uniformly with other shells: PowerShell's status row went through a separate code path, producing a slightly different layout on Windows than on Unix. The shell loop now iterates the full set uniformly, so PowerShell renders the same way asbash/zsh/fish/nu. (#2581)
Internal
-
Library API rework (Breaking library API):
cargo-semver-checksreports several breaking changes — removed public exports (worktrunk::git::interrupt_exit_code/worktrunk::git::exit_codein #2611,worktrunk::shell_exec::trace_instantin #2554, structworktrunk::config::LoadedConfigsin #2573); changed parameter count (worktrunk::config::format_alias_variablesnow takes 2 parameters instead of 1, in #2556); newremovefield onResolvedConfig,UserConfig, andUserProjectOverrides(#2589). -
Typed error variants gain a
Diagnostictrait:Displayis now a single-line label suitable for embedding informat!strings, JSON output, or log files;Diagnostic::renderproduces the styled multi-line block (emoji, color, gutter, follow-up hints). Implemented forGitError,WorktrunkError,HookErrorWithHint,TemplateExpandError, andCommandError. The renderer informat_command_errorwalks the anyhow chain viatry_render_diagnosticonce instead of per-type downcast branches. (#2580, #2611) -
Trace spans carry dynamic context: Alias execution spans carry the alias name (
try_alias:deploy,run_alias:deploy);template_rendercarries the command label; the concurrent-group span moved inside the per-command map so each render emits its own record.Cmd::run/Cmd::pipe_intotrace emission consolidated behindWtTraceLog::record_result. (#2554, #2555, #2613) -
HookLog::Sharedfor branch-agnostic logs: The trash sweep atsweep_stale_trashis repo-wide, butHookLog::path()always prefixed a branch segment, so the call site worked around this by passing a fake"wt"branch. The newShared(InternalOp)variant resolves to{log_dir}/internal/{op}.logdirectly, alongside the other top-level shared logs. (#2595)
0.47.0
Improved
-
wt switch <number>suggestspr:N/mr:Nfirst: Whenwt switch 2474fails because no branch by that name exists, the hint now leads with how to switch to the matching PR/MR before mentioning--create. Platform is detected by hostname-matching the primary remote's effective URL (sourl.insteadOfrewrites are respected): GitHub →wt switch pr:N, GitLab →wt switch mr:N, unknown host → both. Non-numeric branch names keep the original hint unchanged. (#2516) -
Faster alias and hook dispatch: Trivial-alias wall time drops 30.4ms → 20.4ms (1.49× via hyperfine) from three changes — replacing the 10ms
try_wait+sleep poll inCmd::streamwith an event-drivenSignals::forever()listener (and the matching 25ms poll inspawn_signal_forwarder), merging the two cold-pathgit rev-parseforks (--git-common-dirand theprewarm_infobatch) into one, and loading user and project config on scoped threads instead of sequentially. Ctrl-C latency on concurrent steps drops from "up to 25 ms" to "as soon as signal-hook delivers". (#2537, #2538, #2541, #2543) -
Running …hook announce uses one canonical grammar: Replaces the overloadedRunning <hook>: ...punctuation with a layered grammar where each separator carries one meaning —&joins concurrent commands within a step,,joins serial steps within a pipeline,;joins source pipelines and hook-type clauses. Source label moves to a per-pipeline suffix annotation (sync, push (user)), so multi-source events no longer double-colon, and multi-hook-type events bundle onto a single line:Running post-commit: mark (user); post-remove: cleanup (user); post-switch: notify (user); post-merge: sync (user) @ ~/repo. (#2504) -
(user)/(project)source labels no longer rendered bold: TheRunning …background-hook announce wrapped each pipeline's source label in<bold>, producing visually noisy bold inner text against parens that weren't bold. Source labels render as plain inner text now; named command names (sync,push, …) stay bold as before. (#2514)
Fixed
-
Integration detection now ORs local + upstream:
wt list,wt remove,wt merge, andwt step prunepreviously checked a single integration target picked byeffective_integration_target, so a branch merged into localmainwhileorigin/mainhad unique commits (or vice versa) was misreported as unintegrated and skipped.integration_reasonnow considers both — integrated if either matches. (#2507, #2513, #2515) -
wt step copy-ignoredno longer writes outside the destination through symlinked directories: A symlinked destination directory (e.g.target -> /tmp/outside) letcopy-ignoredcreate files outside the worktree;--forcemade it riskier still by overwriting outside files. New guarded entry points reject paths whose resolved parent chain escapes the destination root. Leaf-symlink behavior is unchanged. (#2501, thanks @douglas) -
wt step copy-ignoredno longer hardcodes.pi/in built-in excludes: Removed from the default excludes list; users who need it can add it via the[step.copy-ignored]config. (#2527, thanks @indigoviolet for reporting #2526) -
wt step for-each -- <argv>preserves quoting and argument boundaries: The post---argv was rebuilt withargs.join(" ")and passed throughsh -c, breaking anything with spaces,;, or shell metacharacters inside an argv element. It's now exec'd directly with no implicit shell. Users wanting shell features (pipes, redirects,$VAR, globs) writesh -c '<snippet>'explicitly — same pattern asxargs,find -exec. (Breaking: previouslywt step for-each -- <snippet>accepted shell snippets withoutsh -c;for-eachis[experimental].) (#2465)
Documentation
-
extending.mdhooks-vs-aliases comparison gains a stdin row: The comparison table covered invocation, positional handling, approval flags, source filters, and template-context extras, but never said anything about stdin. Hooks have always received the template context as JSON on stdin; aliases inherit the parent's stdin so pipes pass through and interactive TUIs (wt switch) keep the tty. (#2529) -
benchcommand help cites the positionalFILTERinstead of--skip: Criterion's CLI takes filter as a positional argument;--skipexists but the documented form was misleading. (#2547) -
CLAUDE.md codifies the local-first network access policy: Adds a Network Access subsection under Command Execution Principles. Worktrunk is local-first; the only fall-through-to-the-wire helper is the first
Repository::default_branch()call per repo. A TTL cache does not authorize background polling. (#2536)
Internal
-
RefSnapshotreplaces ambient ref-keyed caches: Callers now capture a point-in-timeRefSnapshotand thread it through read paths instead of reading through cachedcommit_shas/integration_reasons/etc., which could go stale when wt itself moved a ref (e.g.wt mergeadvancing the local target). Thehead_shasper-worktree cache is also dropped so{{ commit }}reflects post-rebase HEAD movement. (Breaking library API:Repository::ahead_behind,batch_ahead_behind,effective_integration_target,integration_targetremoved;Repository::integration_reason,worktrunk::copy::copy_dir_recursive,copy_leaf,worktrunk::git::compute_integration_lazy,remove_worktree_with_cleanup, anddelete_branch_if_safeeach gained a parameter.) (#2528, #2530) -
Aliases keyed by (name, source) and EXEC decided per step: Same-name user+project alias collisions used to scrub EXEC for the whole merged pipeline; per-step decisions now keep the EXEC relaxation on user steps and scrub only project steps. Strictly more permissive than before. (#2474, #2521)
-
SpanRAII guard for in-process[wt-trace]attribution: Records[wt-trace] span="name" dur_us=…on drop, parsed by the existing chrome-trace renderer. Wired through alias dispatch, config load,build_hook_context,template_render, andexecute_shell_command, so cold-cache attribution down to ~µs lands intrace.jsonfor https://ui.perfetto.dev. (#2539) -
Codecov uploads enabled on fork PRs: Drops the
github.repository_owner == 'max-sixty'guards on both Codecov upload steps; the action falls back to tokenless whenCODECOV_TOKENis empty. (#2535) -
Nightly benchmark results append to a public gist as JSONL (timestamp, commit SHA, mean_ns, stddev_ns) so results are queryable across runs. (#2531, #2532)
0.46.1
Fixed
- No spurious
Skipping pre-commit hooks (--no-hooks)line after declined approval:wt step commit,wt step squash, andwt mergecollapsed two distinct hook-skip reasons — the user passing--no-hooksand the user declining an interactive approval prompt — into a singleverify: bool. With hooks configured, declining produced both○ Commands declined, committing without hooksand○ Skipping pre-commit hooks (--no-hooks)back-to-back; the second line was wrong because the user never passed--no-hooks. The call chain now carries aHookGate(Run/NoHooksFlag/Silent), so declined-approval paths skip hooks silently while the explicit--no-hooksflag still prints its message exactly once. (#2485)
Internal
-
HookAnnounceris the single entry point for hook dispatch:post-commitandpost-switchnow route through the same announcer used by other phases, and remaining call sites adoptTemplateVarsfor variable assembly. (#2481, #2482, #2484) -
Merge cleanup extracted: The post-merge finish sequence moves into
worktree::finish_after_merge, andwt listthreads the placeholder explicitly instead of reaching throughCell. (#2491, #2492) -
CI consolidated onto a shared composite action: New advisory
cargo-affectedjobs land, and the test matrix and affected-jobs setup now share one composite action. (#2475, #2483, #2486)
0.46.0
Improved
-
sanitize_dbtemplate filter caps output at 48 chars (was 63 — PostgreSQL's identifier limit). The new budget leaves headroom for users composing the output into longer paths or identifiers, e.g., Unix socket paths capped at 107 bytes. The 3-character hash suffix is unchanged, so collision avoidance is preserved at the new budget; only the truncated base shrinks. (Breaking: branches whose previoussanitize_dboutput exceeded 48 chars get a different identifier — most names are well under 48 and pass through unchanged.) (#2467, thanks @yajo for #2397) -
New
hashtemplate filter:{{ value | hash }}produces a 3-character base36 digest of the input — useful for composing custom truncate-with-collision-avoidance recipes whensanitize_db's 48-char budget still isn't tight enough. (#2453) -
Background hook announces collapse to one line per command:
wt merge(with removal) previously emitted two or three separate◎ Running …lines forpost-remove + post-switchandpost-merge. Hooks across all phases of a single command now share one combined announce:◎ Running post-remove: user:cleanup; post-switch: user:notify; post-merge: user:sync. (#2457) -
Picker-driven
post-switchhooks now receivetarget/target_worktree_pathtemplate variables, matching hooks fired fromwt switch <branch>. Closes a pre-existing asymmetry where interactive switching exposed strictly fewer variables than the non-interactive path. (#2470)
Fixed
-
wt <alias>stdout is pipeable again:wt my-alias | tr …(and any other downstream pipe) silently produced no output because the foreground executor redirected every alias body's stdout to wt's stderr — a hook-only redirect that PR #2089 inherited uniformly. Aliases now pass stdout through; hooks andwt step for-eachkeep the merged-stderr behavior so their output stays ordered with wt's own status messages. (#2479, thanks @davidmyersdev for reporting #2478) -
wt switch <symlink>resolves to the existing worktree instead of failing withNo branch named …. The path-based fallback inRepository::worktree_at_pathpreviously compared paths via lexical normalization only, so symlink-equivalent spellings never matched. The same symlink-aware comparison is now used everywhere the library identifies a worktree by path. (#2466) -
Deleted-CWD recovery handles symlinked subdirectories: When a worktree is removed while a shell sits in a subdirectory whose parents include a symlink (e.g.
~/link/repo.feature/src),wtnow finds the parent repository and recovers as expected. Previously the symlink-aware path compared only file names and bailed for any path deeper than the worktree root. (#2464) -
Merge safety backups for slash branches:
WorkingTree::create_safety_backup()flattened/to-in the ref path, so distinct branches likea/banda-bcollided at the samerefs/wt-backup/a-bref — the latest backup for one could clobber the other, and the documentedrefs/wt-backup/<branch>recovery path didn't match what was actually written. Slashes are valid in git ref names; the branch name is now used as-is. (#2463) -
Nushell wrapper migrates to function-level
@complete: Per fdncred's recommendation in nushell/nushell#18128, the wrapper replaces the parameter-level[...args: string@"nu-complete wt"]with a function-level@completeattribute on an untyped[...args]rest, and the completer signature moves from[context: string](with manualsplit row " "reconstruction) to[spans: list<string>]. Net deletion of seven lines of fragile token reassembly; once nushell/nushell#18131 ships in stable nu the wrapper will automatically benefit from--flag="value"quote stripping and~expansion. (#2458)
Internal
-
Hook dispatch unified: A single
HookAnnouncerorchestrates per-command announces across phases, replacing scatteredCommandOrigin-keyed dispatch with closures, and the various background-hook entry points now share one path. Switch and merge sites assemble template variables through a singleTemplateVarsbuilder. (#2457, #2470, #2472, #2477) -
wt listrendering modes collapse: The internalRenderTargetenum replaces three parallel rendering paths, the JSON path skips the per-result render that the formatter never used, and deadis_ttyplumbing is removed. (#2469, #2473) -
Push handling switches to a
PushKindenum instead of sniffing verb strings (#2468);try_aliasandstep_aliasshare one help-intercept implementation (#2471); lazy template expansion in the command executor lives in one place viaresolve_command_str(#2476).
0.45.2
Fixed
- Interactive pickers via aliases no longer freeze with a blank screen: After the v0.44.0 fix that let aliases inherit the controlling tty (#2380),
wt sw(and other aliases that wrapwt switch) still hung — the alias child was placed in a new process group, so when the skim picker calledtcsetattron/dev/ttythe kernel raised SIGTTOU and stopped it mid-render. The interactive (no-stdin-payload) execution path now keeps the alias child in wt's process group; a PID-targeted signal forwarder ensures externally-delivered signals (kill -TERM <wt-pid>) still reach the child. Hooks (which receive JSON on stdin) are unaffected. (#2444)
Improved
wt <alias> --helphint follows output guidelines: The hint emitted onwt <alias> --helppreviously printed to stdout without a status symbol, used backticks around commands, and stacked three indented bullet suggestions. It now renders as an info line plus a single semicolon-joined hint on stderr, with commands styled via underline (dim-safe). (#2447)
0.45.1
Fixed
worktrunkbuilds again with--no-default-features(regression in v0.45.0): The TTY progress spinner added in #2420 usedcrosstermunconditionally, butcrosstermis gated behind theclifeature inCargo.toml.cargo install --locked --no-default-features worktrunkand library consumers depending on worktrunk withdefault-features = falsefailed to compile. The crossterm-using internals are now#[cfg(feature = "cli")]-gated; withoutcli,Progressdegrades to a no-op (the public API is unchanged). The bug slipped past the in-workspacecargo hack check --feature-powerset --no-dev-depsbecause workspace dev-dependencies pullcrosstermin transitively and Cargo's feature unifier leaks it into the lib build. (#2441)
Improved
- Deprecation warnings for
--claude-codeflag andwt hook post-createalias: Both surfaces previously mapped silently to their canonical replacements (--format=claude-codeandpre-start), giving users no signal to migrate before eventual removal. Each invocation now emits a stderr warning, matching the pattern used bywt select,--no-verify, andwt hook approvals. (#2436)
0.45.0
Improved
-
wt remove --foregroundshows a TTY progress spinner: Removing a worktree with a fatnode_modules(or any large trash payload) now prints⠼ Removing 7,272 files · 64.5 MiBto stderr while the unlink proceeds, with a matching(N files · X MiB)suffix on the success line. TTY-gated; pipes and the background path are byte-for-byte unchanged. Driven by the same machinery introduced forwt step copy-ignoredin #2413. (#2420) -
wt step copy-ignoredshows a TTY progress spinner: Large copies ofnode_modules,build/, ortarget/previously ran silently. A stderr-only spinner (⠼ Copying 7,272 files · 64.5 MiB) ticks while the copy proceeds, gated on TTY +verbosity == 0+ not--dry-run, with a 300ms startup delay so sub-second copies stay quiet. (#2413, thanks @tehdb for the suggestion) -
wt switch -c <new> --base <name>accepts a remote-only base:--base releases/4.x.xpreviously failed withNo branch, tag, or commit named "releases/4.x.x"when the branch existed only as a remote-tracking ref. The bare name now resolves through the single matching remote (the existing safety code still unsets the new branch's upstream so a straygit pushdoesn't target the base). Multi-remote and zero-remote cases pass through unchanged. (#2411, thanks @viicslen for reporting #2410) -
LLM
wt step commitsummaries surfaced inwt config state:wt config state getnow includes aSUMMARY CACHEtable (and JSON section) listing per-branch entries, andwt config state clearremoves them alongside markers/vars/CI status. Backed by a new content-addressed layout at.git/wt/cache/summary/{branch}/{hash}.jsonso a cache hit is a single file-existence check. (#2407)
Fixed
-
wt config state clearno longer reports "Cleared 0" when an I/O or config error actually occurred: Across markers, vars, previous-branch, default-branch, and CI-status (single + aggregate), elevenunwrap_or(false)/let _ = unset_config(...)swallows silently turned real failures into "nothing to clear" messages. Genuinegit config --unsetfailures andread_dir/remove_fileerrors now surface; the common "key didn't exist" exit-5 path stays silent. The sharedclear_*machinery for the on-disk caches is now consolidated inworktrunk::cache. (#2394, #2400) -
wt config state ci-status clear <branch>actually clears the cache file: The single-branch path was still callingunset_config("worktrunk.state.<branch>.ci-status")from before the cache moved to.git/wt/cache/ci-status/<branch>.json. The command always took the info branch and never touched the real cache;--allwas already correct. (#2392) -
wt config state get/clear ci-statusfororigin/foo-style branches when a same-named local branch exists: A local branch literally namedorigin/foowould shadow the remote-tracking ref —is_remoteresolved against the remote while the SHA used for cache keys came from the local branch, sogh/glabwas invoked for the remote while the cache tracked the local. A singlefor-each-refquery now sourcesis_remote, the short name, and the HEAD SHA from the same ref. Tags and raw SHAs passed via--branchnow returnBranchNotFoundinstead of being accepted as "local branches" with nonsensical CI lookups. (#2388)
Documentation
-
worktrunk.dev/llms.txtplus.mdcompanions for every page: Each docs page is now also served as clean markdown atworktrunk.dev/<page>.md, with anllms.txtindex per the llms.txt spec so LLM tools can find the docs without scraping HTML. (#2404) -
Static command-output blocks for the GIF-heavy docs pages:
merge,step,remove,hook, andllm-commitsnow include realistic colorized command-output blocks driven by insta snapshots, alongside (or instead of) the GIFs — so they stay in lockstep with whatwtactually prints. (#2405, thanks @drewnoakes for reporting #2403) -
Conda / Pixi installation listed in the install section: The README and worktrunk-page install table now mention the conda-forge package alongside Homebrew, Cargo, winget, and pacman. (#2425, thanks @noamgot for reporting #2424)
-
FAQ documents the new summary cache layout: The "What files does Worktrunk create?" inventory now includes the
.git/wt/cache/summary/{branch}/{hash}.jsonLLM-summary cache. (#2408)
Internal
-
Three on-disk caches unified onto a shared
worktrunk::cachemodule:sha_cache,ci_status, and the newsummarycache now share one implementation of torn-write semantics, error policy, LRU sweep, and clear mechanics. Thesummaries/directory is renamedsummary/to match the singular-operation convention used by every other kind (ci-status,is-ancestor, etc.); stalesummaries/dirs are harmless. (Breaking library API:Repository::clear_git_commands_cacheandRepository::git_commands_cache_countremoved;worktrunk::copy::copy_dir_recursivenow takes 4 parameters instead of 3 andworktrunk::copy::copy_leafnow returnsResult<Option<u64>>instead ofResult<bool>to thread the progress reporter.) (#2407, #2420) -
Repository::root()no longer caches the fallback path for callers outside any work tree, dropping the dedicatedprewarm_is_insidesentinel cache —worktree_roots.contains_key(path)is now a reliable "path is inside a work tree" signal. No external behavior change. (#2390) -
MSRV bumped from 1.93 to 1.94 following the latest stable − 1 policy. (#2423)
-
Claude Code plugin now uses the commit SHA for versioning: The static
version: 1.0.0in the plugin manifest hadn't moved despite ongoing changes to skills and hooks. Removing it lets Claude Code use the commit SHA, so every commit becomes an update for installed users. (#2402) -
Docs-sync pipeline simplification:
--help-pageplain/web paths unified behindPageMode(#2412); blank-line corruption in mixed$ cmd + outputblocks fixed at the root inconvert_dollar_console_to_terminaland theMARKER_OPEN_PREFIX/MARKER_CLOSEconstants extracted (#2417, #2418); three sync tests collapsed intotest_docs_are_in_syncand dead inner snapshot wrappers stripped (#2419); mirrored close form retired in favour of bare with atest_no_nested_auto_generated_markersinvariant guard (#2422); two more sync helpers aligned with the per-file error channel (#2427);write_trackedhelper extracted and 14 auto-generated outputs markedlinguist-generated=trueso PR diffs collapse them (#2409).running-tendskill leads bug-triage asks withwt -vv <command>so a single gist URL replaces multi-step diagnostic chains (#2415, thanks @viicslen for the feedback in #2410). -
CiBranchName::from_branch_reftakes&BranchRefinstead of(&str, bool), removing the historical "string from one source, bool from another" footgun. (#2391)
0.44.0
Fixed
-
Interactive alias children (e.g.
sw = "wt switch") keep the tty again: Alias execution was piping the template context JSON into each child's stdin, displacing the controlling terminal; interactive commands likewt switchthen saw a pipe and bailed withInteractive picker requires an interactive terminal. Hooks still receive the documented JSON-on-stdin contract; aliases now inherit stdin unchanged. (#2380, thanks @KieranP for reporting in #406) -
wt list --remotesstats were shadowed by a same-named local branch: If a user created a local branch literally namedorigin/foo, the remote row fororigin/foosilently reported ahead/behind (and every other integration stat) against the local branch, becausegit rev-parseprefersrefs/heads/overrefs/remotes/. Integration helpers now pass fully-qualified refs; a follow-up refactor makes the disambiguation unrepresentable at the type level by storing full refs onBranchRef. (#2365, #2378) -
{{ commit }}resolves to the per-worktree HEAD inwt step for-eachon detached worktrees: The hook context was reading HEAD via a process-CWD-keyed cache, so whenfor-eachiterated over sibling worktrees with one on detached HEAD,{{ commit }}resolved to the running worktree's SHA instead of the sibling's. (#2382) -
Global-scope
core.worktreeno longer misdetects the repo root in normal non-bare repos: The 0.43.0repo_path()fast path (#2350) readcore.worktreefrom the bulk config map, which merges global and system scope — but git itself only honorscore.worktreefrom local config for worktree discovery. When the bulk map reportscore.worktreewe now delegate togit rev-parse --show-toplevelso git applies its own scope rules. The common case (nocore.worktreeanywhere) still skips the subprocess. (#2362) -
post-createhook config is now rejected with an explicit error instead of silently migrating topre-start: Clears the silent migration ahead of the planned*-start→*-createrename (see #1571). (#2361)
Internal
- Branch enumeration consolidated into a single canonical inventory, with follow-on perf wins: Five overlapping
for-each-refaccessors insrc/git/repository/branches.rscollapsed into two cached scans (refs/heads/andrefs/remotes/), exposed asRepository::local_branches()andremote_branches(). Shared inventory also powersBranch::remotes()(#2371),strip_remote_prefix(#2372), andis_remote_tracking_branch(#2377), dropping arev-parse/git remotesubprocess each.BranchRefstores full refs (#2378);prewarm_inforeturns a typed snapshot with HEAD SHA folded in (#2367); alias on-branch dispatch reuses the cached HEAD SHA (#2374);list_worktreescaches onRepoCache(#2375, #2383);wt listcommit subjects batched pre-skeleton, retiringCommitDetailsTask(#2369, #2379); picker's speculative preview warm-up primesprewarm_infoonce (#2381). (Breaking library API:Repository::list_local_branches,list_remote_branches,list_tracked_upstreams,list_untracked_remote_branches,commit_timestamps,commit_details,current_worktree_info,Branch::upstream_single, andBranchRef { branch, is_remote }fields are removed;Repository::batch_ahead_behindreturns().) (#2368)
0.43.0
Fixed
-
wt step copy-ignoredno longer self-lowers priority in the foreground: Since v0.37.0,copy-ignoredwrapped its work intaskpolicy -b(macOS) /ionice -c3(Linux) unconditionally, which throttled disk I/O for interactive runs and synchronouspre-*hooks — not just the backgroundpost-startflows the lowering was meant for. Detached background hook pipelines now set an internal sentinel, andcopy-ignoredonly self-lowers when it sees it. Interactivewt step copy-ignoredand foreground hooks run at normal priority. Seewt step copy-ignored --help→ Background-hook priority. (#2358, thanks @bram-rongen for reporting #2342) -
wt step commitno longer panics on large CRLF diffs containing multi-byte UTF-8:parse_diff_sectionsusedstr::lines()(strips\nand\r\n) but advanced the byte-offset accumulator byline.len() + 1, under-counting one byte per CRLF line. Once the diff exceeded the 400k filtering threshold the drift landed inside a multi-byte character and panicked withbyte index N is not a char boundary. Now iterates withsplit_inclusive('\n')so offsets match real byte positions regardless of line-ending style. (#2356, closes #2355, thanks @Qnurye for reporting with a minimal reproduction) -
wt step relocatesurfaces failedgit checkout/git worktree moveinstead of printing false success: The four rawCmd::new("git")...run()?call sites inrelocate.rsswallowed non-zero exit codes — only spawn errors propagated through?— so a failed checkout or move returnedOkand the caller printedRelocated 1 worktree. All four sites now route throughrepo.worktree_at(path).run_command(...), the project's fail-fast git pattern. Triggered most often whenworktrunk.default-branchcached a branch that no longer resolves locally. (#2348)
Improved
-
Alias dispatch ~15-20% faster via batched
rev-parse: Parent-side alias dispatch was firing four separategit rev-parsesubprocesses on the worktree path (--is-inside-work-tree,--show-toplevel,--git-dir,--symbolic-full-name HEAD). A newWorkingTree::prewarm_infofolds all four selectors into a single invocation and populates theworktree_roots,git_dirs, andcurrent_branchescaches so later accessors hit cache.benches/alias:warm/153.3 → 43.5 ms (−19.6%),cold/10062.1 → 49.7 ms (−18.2%). On systems with slowexecve(macOS Gatekeeper, AV), three saved forks translate to ~600 ms perwt <alias>. (#2352, thanks @markjaquith for continuing to report #2322) -
Non-submodule repos no longer pay for a failing submodule probe:
Repository::repo_path()used to rungit rev-parse --show-toplevelinside.gitto resolve the submodule case — that probe fails unconditionally on normal repos, costing ~5 ms per call. Now readscore.worktreedirectly from the bulk config map (the same signal git itself uses): bare repos short-circuit oncore.bare, submodules readcore.worktree, normal repos fall through toparent(git_common_dir). hyperfine on a healthy macOS system:wt noopin a normal repo 53.2 → 48.8 ms (1.09×); in a submodule 62.1 → 55.5 ms (1.12×).repo_path()fires ~2× during alias dispatch, so the win compounds. (#2350, thanks @markjaquith for reporting #2322)
Documentation
-
pre-startandpost-starthook rows say when the hook fires, not just what to put in it: The hook types table rows forpre-startandpost-startnow match the phrasing of neighbouring rows (pre-switchhas "Runs before…",post-mergehas "Runs in the target…"): "Runs once when a new worktree is created, blockingpost-start/--executeuntil complete" and "Runs once when a new worktree is created, in the background". (#2360, thanks @ortonomy for reporting #1571) -
Hook docs: Recipes restructured as a table of contents: The "Designing Effective Hooks" umbrella heading was removed; "Recipes" is promoted to a top-level section with each bullet leading with a specifically-named link to its Tips & Patterns section. "Copying untracked files" moved up next to the JSON context section. (#2349, #2351)
-
Refreshed stale docstrings across recently refactored modules. (#2354)
Internal
ahead_behindcollapsed into single get-or-insert accessor:cached_ahead_behindwas open-coded get-or-insert at the call site.ahead_behindnow readscache.ahead_behindfirst and falls back to the merge-base + rev-list computation on miss, caching the result. Thewt listahead/behind task collapses from a manual cache check + dual codepath to a single call.batch_ahead_behindstill primes the cache in bulk on git ≥ 2.36. (Breaking library API:Repository::cached_ahead_behindremoved.) (#2347)
0.42.0
Improved
-
Alias banner is silent when there's nothing to summarize:
◎ Running alias <name>now only prints when the alias has at least one named step worth naming. Single unnamed aliases (ls = "wt list") and all-anonymous pipelines return no announcement — the banner was just echoing the user's typed name. Pipelines with named steps keep their informative summary (◎ Running alias deploy: install; build, lint).-vstill prints the bare form as a confirmation line. (#2339, thanks @markjaquith for reporting #2322) -
Alias dispatch ~30% faster via batched git config reads:
RepoCachenow reads every config key with a singlegit config --list -zinstead of onegit configsubprocess per key. Config-backed accessors (is_bare,primary_remote,remote_url,default_branchfast path,switch_previous,has_shown_hint,core.fsmonitor,core.pager, and more) resolve via an O(1) map lookup. Writes route throughset_config_value/unset_confighelpers that update the on-disk config and the in-memory map together. Benchmarked onbenches/alias:warm/177.4 → 56.9 ms (−29%),cold/10080.5 → 54.5 ms (−30%) — ~25 ms saved perwt <alias>invocation. (#2344, #2346, thanks @markjaquith for reporting #2322) -
Further O(1) single-branch upstream lookups on
wt merge/wt switch: Follow-up to #2337.effective_integration_targetand thewt switchtracking-info report switched from bulkBranch::upstream()(afor-each-refover every local branch) toupstream_single()—wt liststill uses the bulk cache, but one-shot callers no longer pay for it. (#2338) -
Stale cached default branch surfaces a clear error with a reset hint:
default_branch()'s fast path no longer re-validates the cachedworktrunk.default-branchon every call; when the cached value is stale,require_target_branch/require_target_refraise a newStaleDefaultBrancherror that names the cache key and suggests clearing it.wt list --branchesopportunistically warns when the cached default isn't among the enumerated branches — no extragitfork. (The old silent fallback + two preflight warning sites are removed.) (#2344)
Documentation
worktrunkskill: non-interactive hook approval guidance:skills/worktrunk/SKILL.mdnow covers the hook-approval prompt error that agents hit runningwt merge(or any command that runs project hooks) in a non-interactive session. Explainswt config approvals add(interactive, persists to~/.config/worktrunk/approvals.toml) vs--yes(single-invocation bypass for CI/CD), and directs agents to escalate rather than auto---yes, since pre-approval is a trust decision. (#2343)
Internal
-
Flaky
test_switch_picker_preview_panel_main_diffon macOS: Under heavy parallel load, skim'sN/Mmatch counter updated ahead of the list-panel repaint, sowait_for_stablecould declare a screen stable with stale rows still visible (~1-in-120 failure rate). The stability check now rejects states where skim's parsed match count doesn't equal the visible list-row count. Closes #2334. (#2345) -
benches/aliasregression guard for parent-side dispatch overhead: Five-variant harness (wt --versionstartup floor; noop alias at 1/100 worktrees × warm/cold caches) protects the #2337/#2338 O(1) upstream work and the #2344 bulk config read. (#2340) -
Remove/TTFO benches invalidate
wtcaches between iterations:benches/remove.rs::first_outputandbenches/time_to_first_output.rs::removewere reporting warm-cache cost because iter 1 populated.git/wt/cache/and iter 2+ hit it. Now useiter_batched+invalidate_caches_auto(which also clearsworktrunk.default-branch).benches/CLAUDE.mddocuments the rule and the full list of whatinvalidate_caches_autoclears vs. preserves. (#2341)
0.41.0
Improved
-
Hooks accept the same
--KEY=VALUEsmart routing as aliases:wt hook pre-merge --branch=foo --yesbinds{{ branch }}when the template references it; unreferenced--KEY=VALUEtokens and everything after--forward as{{ args }}, now available in hook templates.wt hook --helplists every hook type.--var KEY=VALUEstill works but emits a deprecation warning pointing at the new form. (#2313) -
-vprints resolved template variables for every hook and alias: Before each◎ Running …line,wtshows atemplate variables:block listing every variable in scope for that hook type or alias and the value it resolved to for this invocation. Vars in scope but unpopulated render as(unset)— which is how e.g.target_worktree_pathsurfaces duringwt switch -on hooks that don't receive it. Aliasargsrenders shell-escaped so the table matches what{{ args }}substitutes below. Works in foreground, background hook pipelines, and alias expansion. (#2316, #2324, #2328, thanks @nicolasff for reporting #2309) -
O(1) upstream lookup in alias/hook template context:
Branch::upstream()triggered afor-each-refscanning every local branch — amortized across bulk consumers likewt list, but wasted work for alias/hook template dispatch, which only needs the current branch. A newupstream_single()runs a scopedfor-each-ref refs/heads/<branch>, so the parent-side alias dispatch is fully O(1) in branch count. Noticeable on machines with slow fork cost (macOS Gatekeeper, AV, slow FS). (#2337, thanks @markjaquith for reporting #2322)
Fixed
-
Symbolic switch targets (
-,@,^) resolve before pre-switch hooks fire:wt switch -previously built the pre-switch hook context from the raw-argument, so{{ target }}and{{ target_worktree_path }}were wrong or unset. Symbolic targets are now resolved to the concrete branch name before the hook context is built, so hooks see the destination worktree correctly. (#2310, thanks @nicolasff for reporting #2309) -
Typo errors across
wt,wt step, andwt config alias show/dry-runshare one format: Four typo surfaces previously split across clap-nativeerror:/tip:output (exit 2) and custom anyhow gutters (exit 1). All four now render the same clap-native layout —config alias show/dry-runsay "alias" / "aliases" instead of "subcommand" / "subcommands" since the positional is an alias name. Thewt <typo>andwt step <typo>paths also now runfinish_commandcleanup, so diagnostic dumps and ANSI resets still fire. (#2306, #2307, #2308)
Documentation
-
Extending and hook guides consolidated: Recipes on
extending.mdandhook.mdwere trimmed, overlapping sections (pre-start vs post-start, copy-ignored variants, pipeline forms) folded together, template-engine scope clarified, and dev-server / database / target-specific hook recipes moved to Tips & Patterns. (#2314, #2315, #2317, #2318, #2319, #2321, #2323, #2326, #2329, #2333) -
Render fixes: Tables inside
<details>blocks pick up the site's table styling (#2325); alternate pages excluded from the sitemap with trailing slashes on nav links (#2320); the[Aliases]help link renamed to[Extending Worktrunk guide]so it reads as a doc pointer rather than a self-reference in terminal help (#2330).
Internal
- Zola link regex handles code spans in link text: The skill-sync regex rejected
[`…code…`](@/…)links, silently shipping dead@/…mdreferences into skill reference files. The regex now balances code spans, and a post-transform guardrail panics on any leftover@/…mdso future misses fail loudly. (#2327)
0.40.0
Improved
-
Aliases route
--KEY=VALUEto template variables and forward everything else as{{ args }}:--KEY=VALUE(or--KEY VALUE) bindsKEYwhenever the template references{{ KEY }}—wt deploy --env=stagingsets{{ env }}tostaging. Everything else joins{{ args }}, a space-joined, shell-escaped sequence ready to splice into a command. Withs = "wt switch {{ args }}",wt s some-branchexpands towt switch some-branch. Index with{{ args[0] }}, loop with{% for a in args %}…, count with{{ args | length }}; each element is escaped individually, sowt run 'a b' 'c;d'renders as'a b' 'c;d'— no shell injection. Tokens after--forward unconditionally, bypassing any binding. Hyphens in keys become underscores:--my-var=xbinds{{ my_var }}. Built-in vars can be overridden inside the template —--branch=foosets{{ branch }}for the invocation, but the worktree's actual branch doesn't move. (Breaking:--var KEY=VALUEand--var=KEY=VALUEremoved;wt <alias>no longer errors on unrecognized flags — they forward to{{ args }}.) (#2280, #2287, #2304) -
-y, --yesis a top-level global flag: Lives once onCliinstead of being duplicated across switch, remove, merge, commit, squash, prune, the ten hook subcommands, shell install/uninstall, plugin install/uninstall, and config update.wt -y <anything>,wt <anything> --yes, andwt --yes <anything>all skip approval and confirmation prompts for that invocation. (Breaking: post-alias--yesremoved — use the global formwt -y <alias>.) (#2279, #2290) -
wt config alias showandwt config alias dry-run:showprints the configured template tagged by source (user/project).dry-runpreviews what an invocation would run without executing —wt config alias dry-run s -- target-branchrenders exactly whatwt s target-branchwould produce. Output annotates routing with# bound:and# args:comments so you can see how each token was interpreted. Both warn when the alias name shadows a top-level built-in (e.g.list,switch); the alias is only reachable viawt step <name>.wt <alias> --help/-hprints a hint pointing at these subcommands rather than silently forwarding the flag into{{ args }}; usewt <alias> -- --helpto forward. (Breaking:wt <alias> --dry-runandwt step <alias> --dry-runretired — use the new subcommand.) (#2291, #2304) -
wt config approvalsreplaceswt hook approvals: Approvals cover both project hooks and project aliases, so the old namespace underhookmis-scoped the command.addnow walks both hook and alias commands — a project that only declares aliases can bulk-pre-approve in one shot.wt hook approvalsremains as a hidden alias that emits a deprecation warning and forwards. (#2282) -
Scope-aware template variable validation: A new
ValidationScope(Hook(HookType),SwitchExecute,Alias) drives validation across every template surface.{{ args }}only validates inside aliases;{{ target }}only in switch/start/merge contexts;{{ pr_number }}only in PR-aware switch hooks. A typo like{{ target }}in apre-starthook is caught at validation time instead of failing at runtime with an undefined-var error after the worktree was created. (#2288) -
pr_numberandpr_urltemplate variables for PR/MR worktree hooks: Available inpre-switch,post-switch,pre-start, andpost-startwhen the worktree was created viawt switch pr:N/mr:N. One canonical pair for both GitHub and GitLab — no separatemr_*aliases. Previously the runtime injected these inpre-startonly and the validator rejected them, so the feature was unreachable. (#2300) -
targettemplate variable injected symmetrically on switch/create/start:pre-switchalready injectedtarget(and conditionallytarget_worktree_path);post-switch,pre-start, andpost-startnow do too. A user writing{{ target }}inpost-startno longer hits an undefined-var error at runtime. (#2295) -
Single announce line for combined background hooks: When user and project hooks both fire on post-merge, post-commit, post-start/post-switch, or post-remove, output collapses into one
◎ Running <hook>: user:…, project:… @ <path>line instead of one per source. Extracted a sharedspawn_background_hooksso every site uses the same path. (#2294, #2298) -
wt config state getshows trash and git commands cache: Two categories thatwt config state clearsweeps (.git/wt/trash/staged worktree directories and.git/wt/cache/SHA-keyed caches) were missing fromstate get, so users could clear state without ever knowing those entries existed. (#2292)
Fixed
-
Reject underscore in
varskeys with a clear error:wt config state vars set db_suffix=foopreviously passed validation and then failed with a crypticerror: invalid key: worktrunk.state.main.vars.db_suffixfrom git (git config variable names must match[a-zA-Z][a-zA-Z0-9-]*). Now rejected atvalidate_vars_keywith a message pointing users to use hyphens instead. (#2285, thanks @Mziserman) -
Surface the full anyhow error chain in spawn and ref-update messages:
Failed to spawn pipeline: Failed to spawn detached processpreviously dropped the underlyingio::Error(errno + OS description). Three sites switched from{err}to{err:#}so the full source chain renders — affects pipeline spawn warnings,wt switchLLM-summary preview errors, andgit pushfailure messages. (#2251) -
Template parse errors in aliases now surface before flag routing: A syntax error in any alias step previously caused that step to silently contribute no names to the referenced-var set, which could change how
--KEY=VALUEtokens bound vs. forwarded as positionals; the syntax error only surfaced later at expansion time. Now errors propagate up front so flag routing isn't determined by malformed templates. (#2299)
Documentation
-
Hook template variables grouped by kind: Variables in help text and docs now follow a consistent ordering (worktree, base, target, PR/MR, hook infrastructure) instead of mixed kinds. (#2303)
-
Aliases section rewrite: Replaced the "How arguments are routed" table with a concrete
fly deployexample, restored theuprebase-every-worktree recipe, added asince-mainexample, and reorganized so the simpler "Passing values" section comes before the routing mechanism. (#2304) -
Trim filler in prose and help text: Removed redundant qualifiers and parenthetical hedges across
extending.md,faq.md,tips-patterns.md, and thelist/stephelp text. (#2277, #2289) -
Drop stale
[experimental]from aliases docstring (#2283) and expand the worktrunk skill description with lexical triggers so Claude finds it more reliably (#2301).
Internal
-
Move approval handlers to config module: Reflects the new
wt config approvalshome. (#2286) -
Nix flake reads Rust channel from
rust-toolchain.toml: Single source of truth instead of duplicating the toolchain version. (#2188)
0.39.0
Improved
-
Aliases dispatch from top-level
wt <name>(and graduate from experimental): Configured aliases now resolve as first-class commands —wt deployworks the same aswt step deploy, reading better as an everyday shortcut. Precedence is built-in → alias →wt-<name>PATH binary → unrecognized-subcommand error, matching git's model where[alias]entries shadowgit-fooexternals. The old "alias shadows a built-in" warning is gone; an alias namedcommitnow simply runs viawt commit(onlywt step commitremains shadowed). (#2266) -
wt switch --baseacceptspr:N/mr:N:--basenow routes through the same resolver as the positional branch argument, sowt switch -c feat-x --base pr:42works symmetrically withwt switch pr:42. Same-repo PRs/MRs resolve to the source branch name; fork PRs/MRs fetchrefs/pull/N/head(GitHub) orrefs/merge-requests/N/head(GitLab) and use the resolved SHA, avoiding fork-branch pollution in the local namespace. (#2263, thanks @jrdncstr for the request in #2261) -
WORKTRUNK_PROJECT_CONFIG_PATHenv override: Mirrors the existingWORKTRUNK_CONFIG_PATH(user) andWORKTRUNK_SYSTEM_CONFIG_PATH(system) overrides for the project config. Missing files at the overridden path resolve to no project config, same as a missing.config/wt.toml.wt config show --format=jsonnow reports the overridden path in theproject.pathfield. (#2267)
Fixed
-
wt listhandles[gone]upstreams gracefully: When a branch's configured upstream ref is gone (remote branch deleted, local tracking ref pruned), the Remote column was surfacing a rawfatal: ambiguous argument 'origin/<branch>'error fromgit rev-parse. Upstream resolution now reads%(upstream:track)and treats[gone]the same as no upstream, so the row renders cleanly. (#2262) -
Nix flake build with vendored skim-tuikit: The
[patch.crates-io]path dependency onvendor/skim-tuikitwas being stubbed out by crane'smkDummySrcduringbuildDepsOnly, breaking downstream skim resolution. The flake now preserves real sources for vendored path deps while still benefiting from dependency caching. (#2265, thanks @nickdichev)
Documentation
-
Renamed "external subcommand" to "custom subcommand": The user-facing name for
wt-<name>PATH-dispatched subcommands is now "custom subcommand" in docs and internal code, matching cargo's vocabulary. Avoids overloading "external," which the codebase already uses forshell_execsubprocesses. Internal renames:src/commands/external.rs→custom.rs,Commands::External→Commands::Custom. (#2270) -
Trimmed filler in prose docs: Removed sentences that restated obvious error behavior, duplicated nearby prose, or added visual weight without information across
extending.md,faq.md, andtips-patterns.md. (#2271, #2272)
Internal
- Extracted a shared
did_you_meanhelper used by both top-level andwt stepsuggestion sites, so the 0.7 Jaro-Winkler threshold and sort order are defined in one place. (#2268)
0.38.0
Improved
-
Concurrent execution in
pre-*pipeline hooks: Pipeline blocks ([[pre-start]],[[pre-merge]], etc.) now run their concurrent commands in parallel for foreground hooks, matching the existing behavior in post-* hooks and aliases. The deprecated single-table form ([pre-start]) remains serial. (#2249) -
clifeature unbundles CLI-only deps for library consumers: Theworktrunkcrate is also consumed as a library (e.g. byworktrunk-sync). Everything reachable fromsrc/lib.rspreviously pulled inclap,clap_complete,skim,crossterm,termimad,env_logger, andhumantimetransitively. A newclifeature (on by default) gates these; library consumers withdefault-features = falsedrop from 195 to 126 transitive crates. (#2238) -
Faster
wt listandwt switchon warm caches:-
In-memory caches for remote URLs, commit details, and diff stats in
RepoCacheeliminate ~11 duplicate git subprocesses perwt switch. (#2252) -
list_local_branches()primes ref/SHA caches fromfor-each-refdata already collected;Branch::upstream()uses a single batchfor-each-refcall instead of N per-branchrev-parsecommands. Reducesrev-parsecalls from 53 to 27 on a typical-8 benchmark. (#2255) -
Share
git status --porcelainoutput betweenWorkingTreeDiffTaskandWorkingTreeConflictsTask, halving duplicate subprocesses. (#2259)
-
-
Faster
wt statusline:terminal_width()no longer walks parent processes to find a TTY — the fallback is now behind a dedicated helper used only bywt statuslineunder Claude Code. Picker,wt list, and help callers skip the walker entirely. (#2260) -
switch.picker.timeout-msdeprecated: After progressive rendering landed in 0.37.1, this config field was parsed but silently ignored. It's now flagged as deprecated with migration viawt config update. (Breaking for library consumers:SwitchPickerConfig::timeout_msfield andtimeout()accessor removed.) (#2236)
Fixed
-
Picker panic on terminal resize:
Term::on_resizepanicked withattempt to subtract with overflowwhen the terminal was smaller than the picker's preferred height (reachable underscript(1)with stdin closed, or in small tmux panes). Vendored skim-tuikit now usessaturating_sub. (#2233) -
Picker previews no longer show stale "no commits ahead" text: BranchDiff/UpstreamDiff tabs read async fields that sometimes hadn't landed at skeleton-time precompute, caching wrong "has no commits ahead" / "has no upstream tracking branch" text. Previews now derive only from skeleton-time fields plus direct git queries. (#2245)
-
Picker preview height with
--branches/--remotes: The Down-layout item count estimate only counted worktrees, so with--branchesor--remotesthe estimate underflowed and the preview claimed space the list needed. (#2247) -
GitLab CI status in
wt list:glab ci listnow runs with the correct working directory. (#2244) -
Picker Summary tab empty state: aligns with the other preview tabs (bullet + branch header) instead of a dimmed sentence. (#2246)
Documentation
-
Use concurrent form in multi-key hook examples, now that pre-* concurrent is supported. (#2248)
-
Catalog skim 4.x upgrade impact and stability assessment. (#2239)
-
Picker and collect module docstrings gain phase timing tables and trace instrumentation at key picker phases. (#2250)
Internal
- Simplified
wt-perfoutput;cache-checkJSON adds wasted-time fields, sorts duplicates by wasted time, and renamestotal_extra_callstoextra_calls. (#2253, #2254)
0.37.1
Improved
-
Progressive rendering in
wt switchpicker: The picker now mirrorswt list's skeleton-first model — branch and path render immediately, while status, diff stats, counts, and summaries fill in in place as they resolve. Replaces the previous ~500ms blocking freeze before first render. (#2231) -
Clean rows no longer flash the timeout glyph in the picker: The LLM semaphore is now acquired only around the actual LLM call, so the no-changes and cache-hit fast paths return immediately instead of sitting behind up to 8 concurrent LLM calls. A clean
mainrow in the picker now renders blank rather than the·"timed out" placeholder. (#2222)
Fixed
-
Picker preview styling bleed:
color_print's</>emits SGR 22 to reset<bold>/<dim>, which skim 0.20's ANSI parser silently drops. Preview spans now emit an explicit full reset (\x1b[0m), so dim and bold no longer bleed across the rest of the preview pane. (#2232) -
Picker alt-screen enter/exit asymmetry: In partial-height mode (
height=90%), skim-tuikit skippedsmcupon startup but still emittedrmcupon exit, corrupting the outer terminal's scrollback. The vendored tuikit now pairs enter/exit symmetrically. (#2230) -
Partial first render under tmux: Under tmux PTY pressure, rows past the first ~1024 bytes would silently vanish because
Output::flushusedwriteinstead ofwrite_all. Vendored skim-tuikit fixes the short-write bug. (#2226)
Library
- Expose worktree removal API from the
worktrunklibrary:remove_worktree_with_cleanup,RemoveOptions, andBranchDeletionModeare now public, letting external tools reuse the canonical removal flow (fsmonitor cleanup, trash-path staging) instead of reimplementing it with raw git commands. Motivated byworktrunk-sync. (#2227, thanks @pablospe for the request in #2053)
Documentation
-
Document
worktrunk-sync: Linked from the Extending page and the FAQ as a community-maintained companion tool for rebasing stacked worktree branches. (#2225) -
Catalog vendored skim patches:
vendor/skim-tuikit/PATCHES.mdnow records both landed and candidate patches against skim-tuikit, and a Cargo.toml comment records why skim is pinned to 0.20.x. (#2228, #2229)
Internal
- Drop unreachable
rayon::spawnfallback in the picker orchestrator. (#2216)
0.37.0
Improved
-
Concurrent table form across hooks and aliases:
post-*hooks already ran table form concurrently; aliases in table form ([[aliases.deploy]]) now do too, with output prefixed by a colored{label} │and line-atomic writes.pre-*table form ([[pre-merge]]) is still forced serial but will follow in a future release — it's deprecated now so the serial→concurrent switch is explicit. Runwt config updateto migrate to pipeline form. (#2089, #2135, #2145, #2151) -
--KEY=VALUEshorthand for alias and hook variables:wt step deploy --env=stagingandwt hook pre-start --branch=feature/testnow work without the--varprefix.--my-var=valuebecomes{{ my_var }}in templates. Hooks also accept custom variable names (previously a fixed list; now matches alias behavior) and warn when a--varisn't referenced by any template — catching typos like--brnach=feature. (#2091, #2096, #2117) -
wt stepdiscovers configured aliases: Runningwt step(orwt step --help) now lists user and project aliases alongside the built-in subcommands, each with a one-line template summary. Aliases that shadow a built-in are flagged(shadowed by built-in). (#2131, #2141) -
Shell completions for external
wt-*subcommands: Tab completion now discoverswt-*binaries on PATH and forwards completion requests to them, sowt sync --<tab>shows the external command's flags. Builds on the git-style external subcommand dispatch in 0.36.0. (#2074, thanks @pablospe) -
Persistent on-disk cache for expensive git operations: Five SHA-keyed probes that previously ran live on every
wt listandwt switch— merge-tree conflict checks, the integration/add-probe,is-ancestor,has-added-changes, and branch diff stats — are now cached to disk under.git/wt/cache/. Because commit SHAs are content-addressed, cached results never go stale; an LRU bound (5000 entries per kind) keeps disk usage bounded. User-visible effects:-
wt listand thewt switchpicker open much faster on big repos, especially those with many stale branches. A warm cache skips the expensive probes entirely; a cold cache still benefits from the faster per-worktree check below. -
Dirty-worktree conflict check is ~10× faster on cold cache. Swapped
git stash createforgit write-treeas the ephemeral tree snapshot — same answer, far less plumbing per worktree. (#2119) -
The picker now shows the same status info as
wt list. The old "skip stale branches" shortcut hid conflict and ahead/behind info on branches 50+ commits behind main to keep the picker responsive. The cache makes the shortcut unnecessary, so stale branches now display full status. -
Consistent results during in-progress rebases. Tasks now track the branch ref rather than the worktree's transient HEAD, so rows no longer contradict themselves mid-rebase (e.g.,
is_ancestor=truealongside1 ahead / 1 behind).
Cache is cleared by
wt config state clear. (#2085, #2090, #2098, #2119) -
-
Lower-priority background operations: Extends the CPU/IO priority throttling already used by
wt step copy-ignoredto the backgroundrm -rfinwt removeand the trash sweep, so cleanup doesn't compete with foreground work. On macOS this now usestaskpolicy -b, which throttles disk I/O as well as CPU; Linux usesnice -n 19with best-effortionice. User hooks are unchanged. (#2130, #2133) -
Pipeline structure in alias announcements: Aliases now announce their pipeline structure:
Running alias deploy: install; build, lintrather than the bare alias name. (#2092) -
Graceful per-layer config degradation: A bad env var or a broken user config file no longer wipes the entire config to defaults. Each layer (system, user, env vars) degrades independently — valid layers apply, invalid layers are skipped with a warning. (#2120)
-
Per-variable env var type resolution: When multiple
WORKTRUNK_*env vars target fields of different types (e.g., a numeric and a string field), each is resolved independently against the file config. Previously one incompatible var would drop every env override and the file config. (#2111) -
Clearer deprecation warnings: Structural deprecation warnings follow a consistent
{label}: X is deprecated in favor of Ypattern with a single proposed-diff preview — no more redundant current-config dump. Template variable renames and theapproved-commandsremoval use the same pattern. Every command (not justwt config show) now emits the same per-kind warnings, with a single dedup'd hint per process pointing towt config showfor details andwt config updateto apply. Deprecation warnings are suppressed in non-diagnostic contexts (tab completion, picker,wt list statusline) to keep prompts quiet. (#2147, #2148, #2153, #2171) -
Structured JSON for
wt config state logs:--formatis now a global flag onstate logs,state hints,state ci-status, andstate marker— ordering no longer matters. Logs JSON entries gain first-classbranch,source,hook_type,name,size,modified_at, and absolutepathfields alongside the existing relativefile, so filtering works withjqdirectly. (Breaking: the--hookand--branchfilters onwt config state logs getwere removed in favor ofjq; piping the JSON throughjq 'select(.branch == "...")'replaces them.) (#2156, #2161) -
Cleaner log filenames: Background hook log files skip the collision-avoidance hash suffix when the input is already a safe filename.
main/project/post-merge/clippy.loginstead ofmain-vfz/project/post-merge/clippy-vif.log. Names containing invalid path characters still get the hash. (#2157) -
wt liststall visibility: Whenwt listhangs for 5+ seconds, the progressive footer now names the blocked task and worktree (e.g.○ Showing 13 worktrees (253/254 loaded, no recent progress; waiting on ci-status for feat)), with a pending count when multiple tasks are outstanding. On full timeout, the warning joins the blocked-tasks list onto a single gutter-prefixed line:▲ wt list timed out after 120s (151 results received); blocked tasks: …. (#2203, #2205, #2207) -
-vvlogs full subprocess output to disk; drop-vvv: Captured subprocess stdout/stderr now fan out to two log targets — a bounded preview on stderr mirrored to.git/wt/logs/trace.log(renamed fromverbose.log), and the uncapped body to a new.git/wt/logs/output.log. Large captures (e.g.git log -p | patch-idduringwt list) no longer flood stderr with elision markers and force a rerun — the full body is always on disk. Any-vcount above 2 collapses to-vv. (#2201) -
Clap-native errors for unrecognized subcommands:
wt sandwt step squshnow show clap's formattederror: unrecognized subcommand 'X'with typo suggestions and Usage block, rather than a custom git-style single-line message. The#[command(external_subcommand)]path added in 0.36.0 forwt-<name>dispatch is preserved. (#2212, #2215) -
Quieter
wt listloading placeholders: The·loading indicator no longer appears for commands that finish within 200ms — short renders never flash the dots. The Status column's loading/timeout glyph collapses from⋯to a dim·, and the working-tree gate's loading placeholder collapses from···to a single·, matching the visual weight of neighbouring gates. (#2177, #2181, #2199) -
Fewer
wt list statuslinesubprocesses: Statusline rendering dropped four duplicate git subprocesses per render (rev-parse --git-common-dir×2,--show-toplevel×3,--git-dir×2) by adding a process-widerev-parse --git-common-dircache and canonicalizing input paths inRepository::worktree_at(). (#2209) -
Signals named in background pipeline errors: Killed hook children now report which signal:
pipeline step terminated by signal 15 (SIGTERM): <step>instead of the genericcommand failed with signal. (#2193) -
Nested config typos surface as warnings: Mistyped keys nested inside a known table (e.g.
[merge] squas = true) now produceUnknown field merge.squasrather than going unnoticed. Built on a unified top-level + nested unknown-key analysis that also powers on-save preservation. (#2195) -
sanitize_hashminijinja filter: New template filter that wrapssanitize_for_filename— produces a filesystem-safe name with a 3-char hash suffix so distinct originals never collide, while already-safe inputs pass through unchanged. Useful for matching on-disk hook log filenames fromwt config state logs --format=json. (#2172)
Fixed
-
wt --helpandwt --versionwrite to stdout: Both previously wrote to stderr, breakingversion=$(wt --version)and pipelines likewt --help | grep …. If you have scripts redirecting2>&1as a workaround, drop the redirect. Fixes #2072. (#2073, thanks @koralowiec for reporting; #2155) -
Directive file passes through
wt stepaliases: Inside awt step <alias>body, an innerwt switch --createnow writes itscddirective to the parent shell instead of dropping it. This was the last blocker for "move staged changes into a new worktree" alias recipes. (#2077) -
Detect
AAandDDunmerged status codes: The working-tree conflict check caught 5 of 7 unmerged states but missedAA(both added) andDD(both deleted). Worktrees with these conflict types now fall back to the commit-based check as intended. (#2124) -
Squash detection no longer silently misses branches when
git diff-treefails: The patch-id pipeline used for squash-merge detection didn't check whethergit diff-treeexited cleanly — a failed source command fedgit patch-ida truncated stream, producing a bogus patch-id and reporting "not squashed" when the branch may have been. Pipeline now bails on source-exit non-zero, and streams directly between commands via an OS pipe rather than buffering in-process. (#2136) -
Nushell multi-line
--executepayloads: The nushell wrapper was executing the exec directive file line-by-line, so multi-line payloads ran as separate shell sessions —cdand variable assignments didn't persist across lines. Now matches bash/zsh/fishsourcesemantics. (#2134) -
Redundant "To configure" hint for outdated shell wrappers: When a shell's integration file exists but is stale,
wt config showno longer prints both a specificwt config shell install <shell>hint and the generic "To configure" summary. The summary now appears only when a shell is genuinely not configured. (#2152) -
Ctrl-C aborts
wtcommand loops: Signal-derived child exits (SIGINT/SIGTERM) now abort hook pipelines, alias steps, concurrent groups, and thewt step for-eachworktree loop. Previously, wt's signal handler forwarded SIGINT/SIGTERM to the current child but wt itself survived, andFailureStrategy::Warnsilently swallowed each interrupt — a single Ctrl-C againstwt mergecould charge through remaining hook steps. (#2174, #2182) -
Nested unknown config keys preserved on save: Any unknown key nested inside a known table (e.g.
future-option = trueunder[merge]) was silently deleted on any config save triggered by other mutations (first-run prompt, interactive path customization). Preservation is now computed recursively, so unknown keys survive at every nesting level. (#2180) -
wt step --helphonors-Cand--config: Help previously resolved aliases before applying global flags, sowt -C other step --helplisted aliases from the process cwd and--config custom.tomlwas ignored. Globals are now parsed in a single early pass. (#2176) -
wt step --helpno longer triggers config side effects: Rendering the alias listing in help output no longer emits deprecation warnings to stderr or writes a migration file next to the user config. (#2179) -
wt step <alias> --dry-runwith lazy vars: Dry-run previously expanded every command eagerly, so pipelines that read{{ vars.foo }}set by an earlier step failed with an "undefined vars" error even when the non-dry-run command would succeed. Dry-run now mirrors the hook pattern: templates that referencevars.*are syntax-validated (catching typos like{{ vars..foo }}) and shown raw, while other templates expand eagerly. (#2170) -
wt config showfish completions and false-negative gating: A missing fish completions file used to print a confusing nested hint under "Already configured shell extension" and flip the generic "To configure" summary. It now prints a warning with specific remediation, mirroring the "Outdated shell extension" pattern. The "report a false negative" link is no longer gated on!has_any_configured, so a detector miss in one shell still offers the link when other shells are detected. (#2163) -
Nix build meets Rust 1.93 MSRV:
flake.lockupdated to ship a newer nixpkgs rustc. (#2185, thanks @Lysanleo)
Documentation
-
"Extending Worktrunk" page: Dedicated docs page collecting recipes for custom workflows via hooks and aliases, including a "move staged changes to a new worktree" recipe closing #938. (#2079, #2083, #2088, #2094)
-
OpenCode in agent handoffs: Skill documentation now covers OpenCode alongside other agent CLIs. (#2108, thanks @vinicius507 for the suggestion in #2076)
-
Hook pipeline documentation:
wt hook --helpand web docs now teach pipelines as[[hook]]blocks with TOML notes inconfig commands, and the aliases docs teach[[aliases.NAME]]pipeline blocks. (#2144, #2149, #2154) -
FAQ updates: Qualified the "no background processes" claim; clarified coverage includes shell-integration-tests; config key location uses
git config worktrunk.*. (#2080, #2086, #2126) -
Troubleshooting:
wt listfsmonitor hang: Noted the interaction with core.fsmonitor daemons. (#2194) -
README installation command formatting: Fixed code-block formatting around installation commands. (#2187, thanks @MahmoudMabrok)
Internal
-
Shell wrapper directive file split: The shell integration now writes
cdpaths to a separate file from--executeshell payloads, with thecdpath read literally (cd -- "$(< file)", no shell parsing) and the exec file scrubbed from alias and hook child environments. Hardens against shell injection from hook/alias bodies into the parent session. The legacy single-file form is honored through 0.38; nushell users needwt config shell installto pick up the new wrapper. (#2118) -
MSRV bumped to Rust 1.93: Per the "latest stable − 1" policy. (#2125)
-
Centralized
[wt-trace]emitter: Trace records are now owned bysrc/trace/emit.rsrather than ad-hoclog::debug!format strings, and-vvlog verbosity is fixed. (#2146) -
Unified hook and alias execution paths: Hooks and aliases share the same foreground execution, shell invocation, template expansion, and priority-spawning code. (#2089, #2128, #2140, #2095, #2113)
-
Config migration is now in-memory; no more
.newfiles:wt config showrenders the deprecation diff from in-memory migrated content rather than writing a.newfile next to the user's config.wt config updateowns the sole filesystem mutation; a new--printflag emits migrated TOML to stdout without writing. (#2184)
0.36.0
Improved
-
Git-style external subcommands:
wt foonow runswt-foofrom PATH whenfoois not a built-in, mirroringgit foo→git-foo. Third-party tools can be installed and invoked aswt <name>without touching this repo. Unrecognized commands show a git-style error with typo suggestions. Docs (#2054, thanks @pablospe for the suggestion in #2053) -
{{ owner }}template variable: Expands to the GitHub/GitLab repository owner, useful for constructing URLs or paths in hook templates andworktree-path. (#2051, thanks @greggdonovan) -
Typed env-var config overrides:
WORKTRUNK__LIST__TIMEOUT_MS=30and other typed overrides now work correctly. Previously, string-typed env values silently failed deserialization, wiping all user config and falling back to defaults. (#2062) -
Config error attribution: Config load errors now identify the source — file errors show TOML line/column pointers, env-var errors list the offending
WORKTRUNK_*variable. Previously all failures showed a generic message. (#2068) -
Per-symbol atomic status rendering: The Status column in
wt listand thewt switchpicker now renders each symbol independently — unresolved gates show⋯at their position instead of fabricating defaults when the collect deadline expires. (#2067) -
Hook error messages: Malformed hook command config now lists the three accepted forms (string, named table, pipeline list) with a pointer to
wt hook --help, instead of an opaque serde error. (#2042) -
Stale trash cleanup:
wt removenow sweeps orphaned.git/wt/trashentries older than 24 hours after each removal, reclaiming space from interrupted background removals. (#2039)
Changed
-
wt hook <type>exits successfully when no hooks are configured: Previously errored; now prints a warning and exits 0, so scripts and CI can invokewt hookunconditionally. (#2056) -
Hook output log layout: Log files moved from flat
.git/wt/logs/{name}.logto nested{branch}/{source}/{hook-type}/{name}.log. Per-branch listing/clearing is now O(that branch).logs get --format=jsonpaths changed to relative. Legacy flat files are swept automatically. (#2041)
Fixed
-
wt config showfalse "Not configured": When the shell init line lives in a sourced file (common with dotfile managers),config showno longer reports "Not configured" — it checks whether integration is actually active at runtime. Fixes #1306. (#2066, thanks @wouter-intveld for reporting) -
Remove-then-switch hint: The hint for shadowed remote branches now uses
--foregroundso the chainedwt remove && wt switchactually works (background removal left a placeholder directory blocking the switch). (#2040) -
Conflict detection unified: The
wt switchpicker andwt listnow both run both conflict probes (commit-level and working-tree). Previously the picker skipped the cheaper probe, leaving the fallback unreachable for clean worktrees;wt listnon-full skipped the working-tree probe, missing conflicts from interrupted rebases. (#2064)
Documentation
- Surfaced vars & aliases on homepage and tips-patterns, cross-linked state keys to dedicated docs, tightened hook links. (#2035, #2036, #2037, #2038)
Internal
- Subcommand ordering aligned to documented policies (pipeline order for step, CRUD for state actions). (#2043, #2044)
0.35.3
Improved
-
wt step prunestreams removals inline: Removals and "Skipped" messages now print as each integration check completes, overlapping with still-running checks — previously there was a visible gap of silence while all parallel checks finished before any output appeared. (#2015) -
Fewer redundant
git worktree listcalls in prune and multi-remove:prepare_worktree_removal()now accepts a pre-fetched worktree list, eliminating N+1 subprocess calls when removing many worktrees. (#2025)
Fixed
-
Picker preview UI lag: The picker's preview cache now stores pager-rendered output, so cache hits skip the pager subprocess entirely. Previously, scrolling past an item with a large diff froze the UI briefly on every re-render because the pager ran on every call. (#2021)
-
Template error hint underlining: The "Available variables" hint in template expansion errors now underlines each variable name individually instead of wrapping the entire comma-separated list in a single underline span. (#2028)
Documentation
-
Cross-linked vars references: The vars feature is documented in the hook template variables table,
wt config state varspage, and tips-patterns recipes — these now link to each other so readers can navigate between "how to set" and "how to use in templates". (#2034) -
Clearer project config intro: Improved the project config introduction and template variable heading in
wt confighelp text. (#2032)
0.35.2
Improved
-
Multiple NAME filters for hook subcommands:
wt hook pre-merge --yes insta doctest docruns a subset of hooks in one command, instead of chaining separate invocations. (#2013) -
Branch context in batch removal hooks: During prune or multi-remove, hook announcement messages now include the branch name (
Running post-remove for **branch-name**: project:cleanup), disambiguating which worktree triggered each hook. (#2014)
Fixed
- Bare repo false positive when
core.bareis unset: Repos cloned by Eclipse/EGit (and other tools that don't writecore.bare) were incorrectly detected as bare. Replacedgit rev-parse --is-bare-repositorywithgit config --type=bool core.bare. Fixes #1939. (#1976, thanks @daniel-iwan-datacore for reporting)
0.35.1
Fixed
-
PR lookup on forks respects
gh repo set-default:wt switch pr:Nnow checks the gh-configured default repo when origin points to a fork, instead of always querying the fork's repo (which returns 404). The error message is also context-aware based on the configured default. Fixes #2002. (#2004, thanks @JustinPierce for reporting) -
JSON output stability:
config show --format=jsonlog file sort is now deterministic (filename tiebreaker for identical timestamps).step for-each --format=jsonincludes a consistenterrorfield on all failure variants. (#2001)
Internal
- Continued
TestRepoconsolidation:bare()constructor,at(path)constructor, removed lifetime guard field. (#2000, #2005, #2007)
0.35.0
Improved
-
--no-verifydeprecated in favor of--no-hooks: All commands (switch,remove,merge,step commit,step squash) now use--no-hooks.--no-verifyremains as a hidden alias with a deprecation warning. (#1932) -
JSON output:
--format=jsononconfig show,config statesubcommands,switch,remove,merge,step prune, andstep for-each. (#1969, #1959) -
Per-command hook log files: Each background hook command writes to its own log file instead of sharing a pipeline log. Combined hook announcements (e.g., post-remove + post-switch) display on a single status line. (#1934, #1980)
-
Prune and list performance:
step prunestreams integration checks and removes candidates in parallel (~3x faster on repos with many branches). Multiple caching layers (integration target,git_dir,rev_parse_tree,resolve_preferring_branch) reduce redundantgit rev-parsecalls duringwt list. (#1950, #1957, #1966, #1948, #1943) -
Itemized
state clearoutput:wt config state clearshows per-category counts and cleans up stale trash from incomplete worktree removals. (#1961, #1960) -
Hook pipeline summary: Serial steps separated by
;instead of→, repeated unnamed sources collapsed into counted form (user ×2), and named steps showsource:nameprefix. (#1994) -
Copy-pasteable help text:
--helpoutput strips$prompts from code examples for direct copy-paste in the terminal. (#1992) -
Better PR lookup errors:
wt switch pr:N404 errors now include the repository name and suggestgh repo set-defaultfor fork workflows. Fixes #1925. (#1927, thanks @JustinPierce for reporting) -
Claude Code worktree hooks: WorktreeCreate and WorktreeRemove hooks for the Claude Code plugin. (#1959)
Fixed
-
File permissions lost on copy-ignored:
wt step copy-ignorednow preserves execute bits when copying files via reflink. Fixes #1936. (#1937, thanks @RileyMathews for reporting) -
Git alias breaks
wt: RelativeGIT_DIR/GIT_WORK_TREEpaths inherited from git aliases now normalized to absolute paths at startup. Fixes #1914. (#1915, thanks @yasuhiroki for reporting) -
Diagnostic files in state logs:
verbose.loganddiagnostic.mdnow properly categorized inwt config state logsoutput. (#1981) -
Integration target in removal display: Background removal now shows
origin/main(effective target) instead ofmainwhen the remote is ahead. (#1993) -
Worktree-path hint suppression: The "customize worktree locations" hint no longer appears when project-specific
worktree-pathis configured. (#1941) -
State logs formatting: Missing newline between log sections in
wt config state logsoutput. (#1968) -
Claude Code WorktreeCreate hook: Fixed jq filter using wrong input field. (#1964)
-
OpenCode unicode escaping: Fixed broken emoji markers depending on Bun version. (#1935, thanks @noirbizarre)
Documentation
- Clarified plugin install command. (#1906, thanks @suyua9)
- Fixed inaccurate logs documentation. (#1986)
Internal
- Consolidated
TestRepointo singlesrc/testingmodule, shared across lib and bin unit tests. (#1944, #1963, #1971, #1991) - Simplified dispatch, timeout, and copy pool internals. (#1949, #1930, #1931)
0.34.2
Improved
-
OpenCode integration: Activity tracking plugin shows agent status (
🤖working,💬waiting) inwt list, withwt config plugins opencode install/uninstallfor management. Also adds OpenCode as an LLM commit generation backend. (#1807, thanks @noirbizarre) -
Lower priority for copy-ignored:
wt step copy-ignorednow runs at the lowest OS scheduling priority (renice -n 19), yielding CPU to interactive foreground tasks on large trees. (#1916) -
Diff stats performance: Switched from
--numstat(one line per file) to--shortstat(single summary line), reducing diff output from O(files) to O(1) per worktree. (#1917)
Fixed
-
Remote detection with
includeIfconfig:primary_remote()failed when non-remote git config keys (likeincludeIf.hasconfig:remote.*.url) matched the remote regex. (#1908, thanks @nirvdrum) -
Background hook execution: Fixed three issues — list-form configs lost serial/concurrent semantics in post-merge/post-remove hooks, pipeline
hook_namecontext leaked across steps, and lazy template expansion was broken for name-filtered hooks (e.g.,wt hook post-start db). (#1910) -
Copy-ignored parallelism: The outer loop in
wt step copy-ignoredran on the global rayon pool instead of the dedicated copy pool, effectively serializing top-level entries. Now runs entirely on the 4-thread copy pool. (#1913) -
Windows stack overflow in copy-ignored: Copy pool worker threads used platform default stack size (~2 MiB on Windows), causing overflow with 200+ directories. Now uses explicit 8 MiB stack size across all platforms. (#1911)
-
Nix flake build: Fixed
flake.nixfiltering out thedev/directory, which broke builds after OpenCode integration addedinclude_str!("../../../dev/opencode-plugin.ts"). (#1924, thanks @mariuskimmina)
Internal
- Unified background hook execution into a single pipeline-based path, removing ~260 lines of dual-path branching. (#1912)
- Replaced deprecated
codecov/test-results-actionwithcodecov/codecov-action. (#1918) - Bumped AUR deploy action to v4.1.2 (fixes argument order with Arch Linux's updated
runuser). (#1909)
0.34.1
Improved
step pruneperformance: Integration checks now run in parallel, dramatically reducing prune time for repos with many branches (3+ minutes → seconds with 100+ branches). Fixes #1888. (#1890, thanks @ortonomy for reporting)
Fixed
-
CPU saturation during copy operations: Restored a dedicated 4-thread copy pool that was accidentally removed in v0.34.0, preventing ~1000% CPU usage on copy-heavy operations like
step copy-ignored. (#1905) -
Background pipeline template variables: When
wt switch --createfires both post-switch and post-start hooks, pipeline steps were incorrectly accumulated into a single background process, causing{{ hook_type }}to expand to the wrong value. Each hook type now spawns its own pipeline. (#1904)
Internal
- Extracted shared
classify_unknown_keyto deduplicate config warning logic. (#1902)
0.34.0
Improved
-
Per-branch custom variables: New
wt config state vars set/get/list/clearcommands store custom key-value pairs per branch, accessible as{{ vars.key }}in hook templates andwt step eval. Variables persist in git config and appear inwt list --format=json. (#1006) -
Lazy template expansion in pipelines: Pipeline steps now expand
{{ vars.* }}at execution time rather than at pipeline construction, so variables set by step N are available in step N+1. (#1840) -
wt config plugins claudecommands: Newinstall,uninstall, andinstall-statuslinesubcommands manage Claude Code integration.installregisters the worktrunk plugin via the Claude marketplace,install-statuslineconfigures the Claude Code status line, andwt config showsuggests these commands instead of raw CLI instructions. (#1830, #1834) -
[forge]config section: New explicit[forge]section withplatformandhostnamefields for SSH host aliases and non-standard remotes.ci.platformis deprecated with automatic migration. (#1826) -
Forge detection with
url.insteadOf: Forge platform detection now falls back to the effective URL (after giturl.insteadOfrewrites), fixing CI status, PR/MR detection, and push-remote features for users with SSH aliases or corporate mirrors. (#1771, thanks @amodelaweb; thanks @roytouw for reporting #1790) -
--branchflag forwt step commit: Commit to a specific branch without switching to it — useful in automation and scripts. (#1750) -
Last fetch time in branch-not-found hint: When
wt switchcan't find a branch, the hint now shows when the remote was last fetched (e.g., "last fetched 3h ago") to help identify stale local refs. (#1877) -
Config field renames:
merge.no-ff→merge.ffandswitch.no-cd→switch.cd, using positive-sense naming. Old names continue to work with deprecation warnings and automatic migration viawt config update. (#1856, #1860) -
Syntax highlighting for template blocks: Documentation site now renders
{{ }}template expressions with syntax highlighting. (#1792) -
Hide Claude Code section when CLI unavailable:
wt config showno longer displays the Claude Code integration section if theclaudeCLI is not found. (#1827)
Fixed
-
Copy-ignored too many open files:
wt step copy-ignoredcould exhaust file descriptors on large trees. Now reuses a single thread pool across all copy operations. Fixes #1865. (#1864, thanks @fspeirs) -
Squash-merged branch detection with merge-tree conflicts:
wt step pruneandwt listfailed to detect squash-merged branches when the default branch modified the same files. Now uses patch-id matching as fallback. Fixes #1818. (#1820, thanks @tthyer for reporting) -
Background removal blocked for 1 second:
wt removeblocked unnecessarily due to incorrect shell operator precedence in the background process spawn. (#1858) -
Fish shell getcwd error in Zellij: Removing a worktree while using fish in Zellij produced "error retrieving current directory" messages. (#1787)
-
Alias detection false positive on path substrings:
wt config showincorrectly flagged unrelated aliases when the alias target path contained "wt" as a substring. Fixes #1772. (#1773, thanks @nicolasff for reporting) -
Branch names with dots in vars: Vars parsing incorrectly split branch names containing dots (e.g.,
release.1.0) as nested config keys. (#1837) -
Lazy pipeline vars expansion in background hooks: Background hook execution failed with lazy vars expansion due to raw string quoting and overly strict template validation. (#1855)
-
GitLab MR remote tracking:
wt switch mr:Ncould reuse branches tracking the correct merge-request ref but on the wrong remote. (#1817) -
Fork CI and integration target detection: Fixed CI check-runs querying the wrong repo for forks, branch tracking checking only merge config, and diverged local branches missing remote merges. (#1812)
-
Placeholder directory on non-current worktree removal:
wt removecreated unnecessary empty placeholder directories and slept for 1 second when removing worktrees other than the current one. (#1868, #1874) -
Merge-tree errors silently swallowed:
git merge-treefailures (invalid refs, corrupt repos) were treated as conflicts instead of propagating, triggering expensive patch-id fallback unnecessarily. (#1896) -
Deprecated key in wrong config file: A deprecated section key (e.g.,
[commit-generation]) in the wrong config file (e.g., project config) was silently filtered. Now warns "Key X belongs in Y config as Z". (#1899) -
Config migration mutex panic: Replaced unsafe
unwrap()with error propagation in config deprecation migration. (#1887) -
Hook show outside git repo:
wt hook shownow provides a clear error message when run outside a git repository. (#1809, thanks @noirbizarre)
Documentation
-
Help text rewritten for
switch,merge,hook, andremovecommands. (#1782, #1783, #1785, #1765, #1764) -
Hook documentation restructured: types reordered by paired events, pipeline ordering rewritten with progressive examples, approval prompt shown in color. (#1763, #1756, #1766)
-
Hooks documented in user config reference. (#1845)
-
Deprecated
post-createremoved from documentation. (#1776) -
Arch Linux official package added to installation instructions. (#1872, thanks @ctrl-q)
-
README template syntax fixed. Fixes #1851. (#1852, thanks @IlyaSemenov for reporting)
Internal
-
Config deprecation consolidated from two layers to one pre-deserialization TOML migration. (#1879, #1880, #1876)
-
Benchmark infrastructure extracted into
wt-perfcrate. (#1878) -
wt removeapproval path reuses already-loaded repo/config (~50ms savings). (#1875)
Deprecated
| Old | New | Action |
|---|---|---|
[ci] section |
[forge] section |
wt config update migrates; wt config show warns |
no-ff in [merge] |
ff (reversed) |
wt config update migrates; wt config show warns |
no-cd in [switch] |
cd (reversed) |
wt config update migrates; wt config show warns |
All deprecated fields continue to work. Run wt config update to migrate, or wt config show for details.
0.33.0
Improved
-
Hook execution pipelines: Post-* hooks support TOML array syntax for serial dependencies — steps execute in order, with maps within steps running concurrently.
post-start = [{ install = "npm install" }, { build = "npm run build", lint = "npm run lint" }]runs install first, then build and lint in parallel. Docs (#1713) -
Copy-ignored exclude patterns:
wt step copy-ignorednow skips built-in VCS metadata and tool-state directories (.bzr/,.conductor/,.entire/,.hg/,.jj/,.pi/,.pijul/,.sl/,.svn/,.worktrees/) by default. Additional excludes are configurable via[step.copy-ignored] exclude = [...]in user or project config. (#1667, thanks @shunkakinoki for #1653) -
Copy-ignored parallelized:
wt step copy-ignoreddirectory walks run in parallel with a dedicated 4-thread pool, improving performance on multi-core systems. (#1721) -
Alias append semantics: Aliases now use append semantics across all config layers, matching hook merge behavior. Within user config, per-project aliases append to global aliases on collision (global first). Across configs, project-config aliases also run alongside user aliases (user first, then project with approval) — previously the user version silently suppressed the project version. (#1724, #1727)
-
Agent skill discovery: The website now serves
.well-known/agent-skills/for web-based skill discovery by AI agents. (#1751)
Fixed
-
Picker alt-r skipped remove hooks: Removing a worktree via
alt-rin the picker bypassed pre-remove and post-remove hooks. Pre-remove hooks now run synchronously (non-zero exit aborts removal), and post-remove hooks spawn in the background. (#1710) -
False positive shell integration warning:
wt config showreported "Found wt in ... but not detected as integration" for Nushell and Fish wrapper files that ARE the integration. Fixes #1735. (#1736, thanks @saschabratton) -
Bare repo config path ignored:
wt hook approvals addand other config commands failed to find.config/wt.tomlin bare repositories because they looked relative to the current worktree instead of the primary worktree. Fixes #1744. (#1745, thanks @jrdncstr)
Documentation
-
Help text for
wt stepsubcommands cleaned up — redundant openers removed. (#1737) -
Experimental badge placement fixed in generated documentation. (#1742, #1729, #1734, #1746)
Internal
-
Copy-ignored built-in exclude constants consolidated. (#1738)
-
Cmd::env()acceptsAsRef<OsStr>for direct path compatibility. (#1723) -
Picker width survey snapshots for layout testing at various terminal sizes. (#1613)
0.32.0
Improved
-
Hooks rationalized: Every lifecycle event now has a symmetric
pre-(blocking) /post-(background) pair. This required one rename:post-create→pre-start, reflecting that it runs beforepost-startas a blocking dependency step. A newpost-commithook fires in the background after commits (including squash commits during merge).post-mergeis now background instead of blocking, consistent with all otherpost-*hooks. Configs usingpost-createget a deprecation warning on anywtcommand; runwt config updateto rename automatically. The old name continues to work during the deprecation period. Docs (#1679, closes #1670, thanks @ortonomy for reporting #1571) -
Detached worktree support: Detached HEAD worktrees can now be removed via
wt remove /path/to/worktreeand switched to viawt switch /path/to/worktree. The interactive picker also handles detached worktrees for both operations. (#1665, #1680, thanks @mjakl for reporting #1661) -
In-place worktree removal in picker: Press
alt-rin thewt switchpicker to remove the selected worktree without leaving the picker. Currently hidden from picker legend and help text pending a cursor-reset issue (#1695). (#1677, #1696) -
Smarter column dropping in
wt list: Low-priority columns (Message, Time, Commit) are now dropped when Summary needs more space, using graduated thresholds based on priority distance. Extends the no-data column dropping from v0.31.0. (#1678)
Fixed
-
Bare repo project config ignored:
.config/wt.tomlplaced in the primary worktree of a bare repository was not found when running commands from the bare repo root directory. Config is now loaded from the primary worktree as fallback, and accidental config in the bare repo root itself is skipped. Fixes #1691. (#1692, #1697, thanks @seakayone) -
pre-starthook failure was non-blocking:pre-startwas the onlypre-*hook that warned on failure instead of aborting. Allpre-*hooks now consistently use FailFast. (Breaking:pre-starthook failures that previously only warned now abort the operation.) (#1708) -
Spurious mismatch warning for detached worktree switches: Switching to a detached worktree by path produced a "Branch-worktree mismatch" warning because the directory name was treated as a branch name. (#1686)
-
Detached worktree switch output showed redundant path: Output now shows "detached worktree" instead of repeating the directory name (which duplicated the path after
@). (#1685) -
Picker alt-r removal fixes: Picker removals now validate the worktree synchronously before removing it from the list, perform the actual git removal on a background thread to prevent UI freezing, and correctly handle detached worktrees. (#1699, #1702, #1717)
Documentation
- Changelog and migration guide for hook rationalization. (#1693)
Internal
-
All test git commands now go through
Cmdfor consistent debug logging and timing traces. (#1714, #1716, #1718) -
Worktree removal logic extracted into shared helpers. (#1683, #1700, #1701)
0.31.0
Improved
-
Hook template variables consolidated: Bare variables (
branch,worktree_path,commit) now consistently point to the Active worktree — the destination for switch/create, the source for merge/remove. New directional variables ({{ base }},{{ base_worktree_path }},{{ target_worktree_path }},{{ cwd }}) give hooks explicit access to both sides of two-worktree operations. (Breaking:{{ worktree_path }}changed in pre-switch for existing worktrees and in post-merge — use{{ cwd }}or{{ base_worktree_path }}for the previous behavior.) Docs (#1655, #1660, #1663, thanks @sysradium for reporting #1543) -
Bare repo worktree-path prompt: When a bare repo lives at a hidden path like
.gitor.bare,wt switchnow detects that worktrees would get awkward names (e.g.,project/.git.feature) and offers to configure aworktree-pathoverride. Non-interactive environments show the config to add manually. (#1656, thanks @seakayone for reporting #1279) -
Shell completion for step aliases: Tab-completing
wt step <TAB>now shows configured aliases alongside built-in step commands, with--dry-run,--yes, and--varflags. (#1641) -
wt listreclaims space from redundant columns: When the Path column carries no useful information (all worktree paths are predictable from branch names), its space is reclaimed for Summary and Message. (#1634) -
Syntax highlighting for alias dry-run:
wt step <alias> --dry-runnow uses bash syntax highlighting, matching hook dry-run output. (#1635)
Fixed
-
wt listhang from fsmonitor daemon: On macOS with builtin fsmonitor,wt listcould hang at the "(loading...)" stage becausegit fsmonitor--daemon startinherited pipe file descriptors and held them open indefinitely. (#1648) -
Post-remove hooks ran at wrong directory: Post-remove hooks executed at the user's cwd (which could be the worktree being removed) instead of the primary worktree. (#1645)
-
Picker showed loading indicator for unavailable data: The interactive picker used
⋯(loading) for fields that would never arrive; now uses·(unavailable). (#1651) -
wt hook --dry-runmissing directional variables: Hook dry-run and--show --expandedoutput was missingbase,target, andtarget_worktree_pathvariables for switch, create, and remove hooks. (#1669)
Documentation
-
Bare repository layout guide and
worktree-pathexample in config docs. (#1664) -
Migration guide for template variable changes in hook docs. (#1666)
Internal
-
Renamed internal
selectmodule topicker. (#1650) -
Consolidated merge/remove removal validation. (#1625)
0.30.1
Fixed
-
Narrow terminal layout:
wt switchpicker now uses vertical (Down) layout on terminals narrower than 80 columns, and the Branch column shrinks instead of being dropped — branch names are always visible. (#1564, #1626, thanks @armstrjare for reporting #1563) -
Bash tab completion showed all branches:
wt switch feat<TAB>displayed every branch instead of filtering by prefix, prompting "Display all N possibilities?" for users with many branches. Fish and zsh still use their native substring/fuzzy matching. (#1622, thanks @altruic for reporting #1621) -
Hook command completion pre-filtered in all shells:
HookCommandCompleterfiltered by prefix before returning candidates, preventing fish/zsh substring matching on hook command names. (#1627) -
wt mergefailed withsubmodule.recurse=true: Users withsubmodule.recurse=truein their git config saw push errors during merge. Local push now passes--recurse-submodules=no. (#1619, thanks @viicslen for reporting #1604) -
Worktree sync uses safe
read-tree: Target worktree sync after--no-ffpush usesread-tree -m -uinstead ofreset --hard, consistent with the project's norms. (#1623)
Internal
- Inlined
complete_branchesandcomplete_hook_commandsinto their respective completers. (#1628, #1627)
0.30.0
Improved
-
wt merge --no-ff: Create a merge commit instead of fast-forwarding, for semi-linear history (rebased commits plus a merge commit). Also available asmerge.no-ff = truein user config. Docs (#1438, thanks @siriobalmelli) -
wt step eval[experimental]: Evaluate template expressions from the command line. All hook variables (branch,repo,worktree_path) and filters (hash_port,sanitize,sanitize_db) are available. Designed for scripting:curl http://localhost:$(wt step eval '{{ branch | hash_port }}')/health. Docs (#1004, thanks @EduardoSimon for the feature request in #947) -
wt step push --no-ff: Mirrorswt merge --no-fffor manual step-by-step workflows:wt step commit && wt step rebase && wt step push --no-ff. (#1587) -
Worktree removal now hidden: Removed worktrees are staged in
.git/wt/trash/instead of a visible.wt-removing-*sibling directory. All worktrunk state consolidated under.git/wt/. (#1583, thanks @ortonomy for reporting #1572)
Fixed
-
wt mergecould remove the default branch worktree in bare repos: In bare repository layouts, merging from the default branch worktree could remove it instead of preserving it. (#1620, thanks @viicslen for reporting #1618) -
wt switchpanicked on empty picker selection: Entering a non-existent branch name in the interactive picker caused a panic. Now returns an error message gracefully. (#1566, thanks @dlnilsson for reporting #1565) -
copy-ignoredlost directory permissions: Source directory permissions (e.g., mode 0700 for Postgres data directories) were replaced with default 0755. (#1590, thanks @RileyMathews for reporting #1589) -
copy-ignoredfailed on broken symlinks at destination: If a gitignored file's destination was already an invalid symlink, the copy failed with "No such file or directory". (#1549, thanks @armstrjare for reporting #1547) -
Nushell
$env.PWDerrors afterwt remove: Removing a worktree from inside it produced repeated$env.PWD points to a non-existent directoryerrors in Nushell. (#1508, thanks @mystilleef for reporting #1507) -
Remote URL used
insteadOfrewrites:wt listand PR detection used the rewritten remote URL instead of the raw config value, causing mismatches with CI and forge detection. (#1546, thanks @volkanbicer) -
SIGPIPE from pager quit treated as error: Quitting a pager (e.g.,
qinless) duringwt step diffshowed "terminated by signal 13" instead of exiting cleanly. (#1559) -
Missing vs corrupt git config errors conflated: Missing config keys and corrupt config files both returned the same error, making corrupt configurations hard to diagnose. (#1610)
-
Shell operator precedence in remove command: The
|| truefor fsmonitor stop had incorrect precedence, potentially swallowing failures from the entire removal chain. (#1584) -
Missing shell escaping in error hints: Branch names and paths in suggested
cd ... && git switch ...commands were not shell-escaped. (#1584)
Documentation
-
pnpm post-create example: Added a recipe for running
pnpm installafter worktree creation viacopy-ignored. (#1581) -
Hook execution order: Clarified that post-create hooks run before post-start hooks. (#1573)
0.29.4
Improved
-
Destination branch in pre-switch hooks:
{{ branch }}in pre-switch hooks now expands to the destination branch (as typed by the user) instead of the source worktree's branch. Previously, pre-switch hooks could only see where you were, not where you were going. Docs (#1497, thanks @mayureshwaykole for the discussion in #1494) -
LLM tool commands in example config:
wt config createnow includes double-commented entries for Claude, Codex, opencode, llm, and aichat commands, making them discoverable without reading the docs. Docs (#1531, #1533)
Fixed
- Extra blank line in
config createoutput: The success path printed a blank line between the success message and hint lines, inconsistent with the "already exists" path. (#1525)
Documentation
-
Switch docs: Trimmed upstream tracking paragraph, added missing
pre-switch/post-switchhooks to creation lifecycle, combined GitHub/GitLab sections. (#1521) -
List docs: Restored
--fullprerequisite note for LLM summaries. (#1517) -
Experimental badges in headings: Moved experimental badges from description paragraphs to headings in web docs for cleaner TOC entries. (#1523)
Internal
-
CI improvements: Prevented duplicate inline review comments across cycles, banned blocking
gh pr checks --watch, fixed verify step for concurrency-cancelled runs, stopped hourly audit from flagging CI polling, added rolling file survey to nightly cleaner. (#1514, #1498, #1519, #1520, #1522) -
Simplified review-pr skill: Cut metacognitive coaching and collapsed confidence tiers; 504 → 369 lines (−27%). (#1530)
0.29.3
Improved
-
Unified timeout model for list and picker: Consolidated the picker's per-command timeout and list's experimental
timeout-msinto a shared config with[list] task-timeout-ms(per-task, shared by both) and per-context wall-clock budgets ([list] timeout-ms,[switch.picker] timeout-ms). Picker default budget raised from 200ms per-command to 500ms wall-clock. (#1515, #1487) -
Pre-flight template validation for
wt switch: Switch templates (--executeand hook commands) are now validated before worktree creation, preventing orphan worktrees from syntax errors like{{ unclosed. (#1500)
Fixed
-
wt removeallowed removing default branch worktree: The default branch worktree (e.g., main) could be removed because it was trivially "integrated" into itself. Now blocked unless-Dis used. (#1460, thanks @cperalt for reporting #1448) -
Symlinks copied as regular files in
copy-ignored: Top-level gitignored symlinks were copied as regular files instead of preserved as symlinks, breaking setups like Yarn monorepos. (#1489, thanks @karmeleon for reporting #1488) -
Missing placeholders in WorkingDiff and Upstream columns: These columns showed blank instead of
⋯/·placeholders when data wasn't loaded, breaking the visual loading signal. (#1503)
Documentation
-
Step command docs: Added promote subdoc, improved swap description, linked Operations index to subcommand sections, moved aliases section after subcommands, fixed cross-filesystem fallback description. (#1505, #1495, #1502, #1513)
-
List docs: Documented placeholder symbols (
⋯,·) in help text, rewrote LLM summaries section. (#1496, #1506) -
Homepage: Added headline features (CI status, PR checkout, hash_port) and tips link. (#1501)
-
Experimental badge pills: Styled
[experimental]markers as pill badges in web docs. (#1499)
Internal
-
Deduplicated hook config resolution: Extracted shared hook-type list and made
lookup_hook_configspub(crate). (#1512) -
Agent Skills metadata: Added
metadata.internal: trueto repo-scoped skills sonpx skills addonly offers user-facing ones. (#1491)
0.29.2
Improved
[switch] no-cdconfig option: Disable directory change by default withno-cd = truein the[switch]section. Use--cdflag to override when needed. Useful for tmux workflows where sessions handle navigation. Docs (#1401, thanks @jradtilbrook)
Fixed
-
GPG signature output breaks
wt list: Whenlog.showSignatureis set in git config, GPG verification lines contaminated stdout ingit logcalls, causing parse failures. All git log invocations now pass--no-show-signature. (#1465, thanks @apre) -
Tab completions ignore shell substring matching: The binary was prefix-filtering branch candidates before returning them to the shell, preventing fish substring matching (
auth<TAB>→feature/user-auth) and zsh fuzzy matching. Completions now return all candidates and let the shell apply its own matching. (#1471, thanks @benjaminbauer for reporting #1468) -
Tab completions unusable in large repos: Repos with many remote branches triggered the "do you wish to see all N possibilities?" prompt. Remote-only branches are now excluded when the total exceeds 100. (#1442, thanks @cperalt for reporting #1415)
-
Nushell shell integration broken in Home Manager module: The Nix Home Manager module used
useinstead ofsourcefor the nushell init script, and template definitions were not exported, preventing thewtwrapper function from loading. (#1476, thanks @mystilleef for reporting #1475)
Documentation
- Manual commit message recipes: Added recipes to Tips & Patterns for using
commit.generation.commandconfig to write commit messages by hand with$EDITORinstead of an LLM. (#1469, thanks @viicslen for the feature request in #1467)
Internal
- Skill/CI guidance: Improved Claude bot skills for triage, code review, and CI monitoring. (#1485, #1477, #1474, #1472, #1470, #1458, #1447)
0.29.1
Improved
-
GitHub Enterprise support for
wt switch:wt switch pr:<number>now works with GitHub Enterprise instances by extracting the hostname from the remote URL and passing--hostnametogh. (#1408, thanks @TomRomeo) -
wt switch --no-cdprint mode: Whenwt switch --no-cdopens the interactive picker (no branch argument), selecting a branch prints its name to stdout and exits — useful for scripting. (#1445, thanks @ruudk for the feature request in #1404) -
Shadow warning for step aliases:
wt stepnow warns when a user-defined alias has the same name as a built-in step command (e.g.,commit,rebase), since clap intercepts the built-in before the alias runs. (#1389)
Fixed
-
Post-switch hooks on
wt remove: When removing the current worktree, post-switch hooks now fire correctly as the user is implicitly switched to the primary worktree. Previously, project hooks were silently skipped because config lookup failed from the removed CWD. (#1452, thanks @mjakl for reporting #1450) -
LLM commit session isolation: The recommended Claude command for commit generation now includes
--no-session-persistence, preventing commit conversations from pollutingclaude --continue. (#1454) -
Color formatting in error messages:
DetachedHeadandNotInWorktreeerror messages now support color-print styling, matching other error variants. (#1387) -
Windows error handling: Replaced
std::process::exit()with proper error returns in Windows-specific code paths, so destructors and cleanup run correctly. (#1456)
Documentation
-
Hook JSON context section: Fixed documentation that incorrectly described
hook_typeandhook_nameas extras; added the TOML hook definition showing how JSON stdin is wired. (#1360) -
wt removehelp text: Updated example heading to clarify thatwt removeworks on both worktrees and branches. (#1449) -
Xcode DerivedData cleanup recipe: Added recipe for cleaning Xcode build artifacts across worktrees. (#1423, thanks @RickeyBoy)
Internal
-
Refactoring: Extracted handler functions from
main()dispatch, replaced negated boolean variables with positive-polarity names (no_verify→verify,no_delete_branch→keep_branch). (#1394, #1388, #1393) -
Test reliability: Resolved flaky PTY/timing issues in integration tests, consolidated trivial tests into inline snapshots. (#1459, #1392, #1382, #1390)
-
CI: Added Zola docs validation to PR checks, catching broken internal anchor links before merge. (#1396)
0.29.0
Improved
-
wt step <alias>command: User-defined command templates with template variables ({{ branch }},{{ worktree }}, custom--var KEY=VALUE). Project-config aliases require approval; user-config aliases are trusted. Docs (#1348, thanks @cavanaug for the feature request in #1214) -
Remove worktrees from switch picker:
alt-rinwt switchinteractive picker removes the highlighted worktree directly (no force flags — matches safety defaults). (#1253, thanks @alfredomtx for the feature request in #1251) -
wt hook <type> --dry-run: Preview hook expansion with template variables resolved, without executing. (#1361) -
Hook template variables:
{{ hook_type }}and{{ hook_name }}are now available in hook command templates. (#1364) -
Typo suggestions for step commands: Unknown step commands and aliases now suggest the closest match. (#1363)
-
Syntax-highlighted
--help: Code blocks in--helpoutput now render with language-aware syntax highlighting (TOML, bash) instead of plain dimmed text. Help options are grouped under navigational headings (Picker Options, Automation). (#1365, #1355, #1359) -
Nix Home Manager module: Install worktrunk via Nix Home Manager. (#1287, thanks @DuskyElf; thanks @meicale for reporting #1257)
-
Output styling: Bold names replace quoted names in error messages, underlined references replace bright-black in hints,
@ pathconvention unified in section headings, and branch-worktree mismatch warnings now show both actual and expected paths. (#1375, #1380, #1285, #1376, #1377, thanks @jhigh2000 for reporting #1184)
Fixed
-
--no-cdwith interactive picker: The--no-cdflag is now passed through when usingwt switchwith the interactive picker. (#1331, thanks @cperalt for reporting #1330) -
Remote branches with
/in picker:wt switch --remotesnow correctly handles remote branches with/in the name (e.g.,origin/user/feature). (#1266, thanks @curtbushko for reporting #1260) -
Nushell config path on Windows:
wt config shell installnow uses the platform-appropriate config directory for nushell on Windows. (#1294, thanks @deltoss for reporting #1293) -
Git for Windows per-user install: Detect per-user Git for Windows installations and show a clean error message instead of panicking when Git Bash is not found. (#1261, #1262, thanks @JefMasereel for reporting #1259)
-
JSON output
summaryfield:wt list --format=jsonnow includes thesummaryfield. (#1339) -
Squash merge message: Uses source branch name instead of target branch in the merge commit message. (#1319, thanks @ricafeal)
-
Alias approval errors: Propagate the real error (e.g., "no remote URL found") instead of a vague "Cannot determine project identifier". (#1374)
-
wt step pruneoutput: Summary uses cleaner paired format ("Pruned 1 worktree & branch") and fixes post-remove hook display path for non-current worktrees. (#1344) -
VCS metadata in
copy-ignored: Exclude.git,.hg,.svn,_darcsdirectories fromwt step copy-ignored. (#1250) -
Nix evaluation warning: Use
stdenv.hostPlatform.systeminstead of deprecatedsystem. (#1336, thanks @onelocked)
Documentation
-
Home page SEO: Canonical URL deduplication and consistent tagline. (#1357)
-
LLM commit tools: Add opencode and consolidate other LLM commit tool references. (#1295)
Internal
-
Git plumbing: Replace porcelain commands with plumbing alternatives (
rev-parse --symbolic-full-name,log --no-walk,for-each-ref) for more robust output parsing. Cache deprecated-variable regexes and fix silent wrong results insame_commit()/trees_match(). (#1345, #1338, #1358) -
Error propagation:
repo_path()andShellConfig::get()now returnResultinstead of silently falling back. (#1280, #1262) -
CI improvements: Consolidated setup into composite action, replaced
gh run watchwith poll loops, added conflict resolution for bot PRs in nightly cleaner. (#1273, #1329, #1307)
0.28.2
Improved
wt step pruneoutput: Dirty or locked worktrees are silently skipped instead of printing warnings, and "No worktree found for branch" info messages are suppressed — prune output now shows only what was actually removed. (#1236)
Fixed
-
CWD removal hint: After a worktree is removed while a shell is in that directory, the hint now checks whether
wt switch ^would actually work before suggesting it — falls back to suggestingwt listwhen the default branch worktree doesn't exist (e.g., bare repos). (#1238, thanks @davidbeesley for reporting #1168) -
Submodule detection in worktree removal: Submodule detection now uses
git submodule statusoutput instead of parsing error messages, avoiding locale-dependent and version-dependent string matching. (#1247)
Internal
-
Hook dispatch: Introduced
HookCommandSpecstruct and extracted helper functions to deduplicate hook dispatch code (~50 lines net reduction). (#1248) -
CI skills: Fixed jq escaping in ad-hoc CI polling queries and improved Step 5 dismissal ordering in pr-review skill. (#1241, #1246)
0.28.1
Improved
-
Nushell tab completions:
wt switch <TAB>and subcommand completions now work in nushell. (#1220, thanks @omerxx for reporting #1215) -
wt step prunereliability: Candidates are now removed inline as they're discovered instead of scan-then-remove, with per-candidate error handling (dirty worktrees are warned and skipped). Dry-run and execution summaries now distinguish worktrees, branches, and detached worktrees. Command marked[experimental]. (#1234, #1232, #1223) -
wt step diffperformance: Copies the real git index instead of creating an empty one, preserving git's stat cache so unchanged tracked files are skipped. (#1230)
Fixed
-
Branch delete race on fast-path remove:
wt removenow deletes merged branches synchronously on the fast path instead of deferring to the background process, fixing a race wherewt switch --create <branch>fails with "branch already exists". (#1216) -
Panic in
is_bare()on unusual repositories:is_bare()now propagates errors instead of panicking. (#1221, @bendrucker) -
Help text table coloring: Status symbols and backtick-enclosed text in
--helptables now render with proper ANSI colors. (#1231)
Internal
- CI workflow: Added concurrency group to claude-mention workflow, fixed external contributor PR review permissions. (#1233, #1226)
0.28.0
Improved
-
wt step prunecommand: Remove worktrees whose branches are already merged into the default branch. Skips unmerged and recently created worktrees, with--min-ageto control the staleness threshold. Docs (#1191) -
Color palette in
wt config shell show-theme: Shows each color and style rendered in itself — base colors, modifiers, bold+color and dim+color variants — for diagnosing legibility issues on different terminal themes. (#1185, thanks @jhigh2000 for reporting #1184) -
Smarter column layout in
wt list: The Message column is hidden when the terminal is too narrow for Summary to reach 40 characters, preventing both columns from being truncated to unreadable widths. (#1166)
Fixed
-
Submodules in worktree removal:
wt removenow handles worktrees containing initialized git submodules, which previously failed with "working trees containing submodules cannot be moved or removed". (#1196, thanks @dlecan for reporting #1194) -
CWD recovery validation: Recovery from a deleted worktree directory now validates that candidate repositories actually contain the deleted path as a worktree, preventing false matches when multiple repos share a parent directory. (#1193)
-
Shell-escape paths in
-Cflag hints: Paths containing spaces or special characters in-Chints are now properly shell-escaped. (#1173) -
ANSI handling in CWD recovery: Recovery messages now use
anstreamfor proper ANSI handling on terminals that don't support color. (#1183) -
Worktree path in detached HEAD removal messages: Removal output for detached HEAD worktrees now includes the worktree path for clarity. (#1210)
-
Pruned worktree output: Worktree and branch deletion for pruned worktrees are combined into a single output line instead of two separate messages. (#1211)
Documentation
- Page metadata and SEO: All doc pages now have
<meta name="description">, canonical URLs, and structured data (JSON-LD) for better search engine visibility. (#1167)
Internal
- CI bot improvements: Inline suggestions, confidence-based review scrutiny, consolidated review+CI analysis, self-poll prevention, verified-facts guideline for triage, and explicit issue-closing in nightly cleaner. (#1172, #1181, #1199, #1204, #1212, #1198, #1209)
0.27.0
Improved
-
wt step promotecommand (experimental): Exchange branches between the main worktree and any linked worktree, including swapping gitignored files (build artifacts,.env,node_modules/). Shows mismatch state inwt listwith ⚑ indicator; restore with no arguments from main worktree. Docs (#789, thanks @zpeleg for the feature request in #738) -
Instant worktree removal:
wt removenow renames the worktree to a staging path before spawning the background cleanup, making the path unavailable immediately instead of after a 1-second sleep. Falls back to legacy removal if rename fails (cross-filesystem, permissions). (#773) -
Graceful recovery from deleted worktree directory: When a worktree is removed while a shell is still in that directory,
wt switchandwt listnow recover automatically — find the parent repository from$PWDand proceed without pre-switch hooks. (#1146, thanks @davidbeesley for reporting #1109) -
PR/MR support promoted out of experimental: GitHub PR (
pr:<number>) and GitLab MR (mr:<number>) targets inwt switchare now considered stable — 11 minor releases with no interface changes since v0.15.0. (#1114)
Fixed
-
SSH URLs with ports: Remote matching now handles
ssh://git@host:2222/owner/repo.git— ports are stripped during URL parsing instead of rejecting the URL. (#1151) -
Config path resolution:
wt config createnow resolves the same path as config loading, fixing a mismatch when using XDG directories. (#1135, thanks @christopher-buss for reporting #1134) -
PTY prompt echo interleaving: Approval prompts no longer intermix with echoed input on slower systems. Uses quiescence detection instead of a fixed sleep. (#1133)
-
Better diagnostics when foreground removal fails: When
wt remove --foregroundfails with "Directory not empty", now shows the remaining top-level entries (capped at 10) and suggests trying background removal. (#1150) -
Output formatting consistency: Hints use canonical "To X, run Y" phrasing, config update hints render in gutter blocks with correct
-Cflag for linked worktrees, and ANSI color nesting fixed in hint messages. (#1138, #1137) -
Panic-safe error propagation: Replaced
.unwrap()and.expect()calls in functions returningResultwith proper?andbail!error propagation. (#1127)
Documentation
-
Bot trigger renamed: CI bot responds to
@worktrunk-botinstead of@claude, matching the actual GitHub username. (#1149) -
wt step promotedocumented in worktree model: The branch-exchange operation is noted as the sole exception to the "never retarget a worktree" rule. (#1154)
Internal
-
CI security model: Rulesets, token consolidation, and environment protection hardened for GitHub Actions workflows. (#1118)
-
Nightly CI workflows: Automated review of Claude CI session logs and 24-hour code quality sweep for bugs, missing tests, and stale docs. (#1111)
-
CI reviewer and bot improvements: Better failure tracing, Dependabot PR reviews, thread resolution ordering, LGTM dedup, actionable feedback, automatic response to bot PR comments, and graceful handling of mentions on merged/closed PRs. (#1117, #1128, #1129, #1131, #1141, #1142, #1145, #1147, #1153, #1158, #1164)
0.26.1
Fixed
- Statusline panic without LLM config:
wt list statuslinepanicked when no LLM command was configured. Now skips summary generation gracefully. (#1107)
Internal
- Demo GIFs now show the Summary column in
wt list --fulloutput. (#1104, #1106) - CI session log uploads fixed to use correct path. (#1103)
0.26.0
Improved
-
Summary column in
wt list --full: LLM-generated one-line branch descriptions. Opt-in via[list] summary = truein config (experimental). Requires[commit.generation]config. (#1100) -
wt step diffcommand: Show all uncommitted and untracked changes thatwt mergewould include as a unified diff against the merge base. Pass-- --statfor a summary. Docs Closes #1043. (#1074, thanks @davidbeesley for the feature discussion) -
pre-switchhook: New hook that runs beforewt switchvalidation. Use it to fetch-if-stale or run pre-flight checks before switching. Respects--no-verify. Docs (#1094, thanks @jdb8 for the use case in #1085) -
wt config updatecommand: Automatically apply config migrations — detects deprecated patterns (template variables,[commit-generation],approved-commands), shows a diff preview, and applies with confirmation. Use--yesto skip the prompt. (#1083) -
Configurable picker timeout: New
[switch.picker] timeout-mssetting (default: 200ms,0to disable). The[select]config section is deprecated in favor of[switch.picker]— runwt config updateto migrate. (#1087) -
Command audit log: All hook executions and LLM commands are logged to
.git/wt-logs/commands.jsonlwith timestamps, exit codes, and duration. Auto-rotates at 1MB. View withwt config state logs getor query withjq. (#1088)
Fixed
-
Hook CWD wrong from subdirectories: Hooks invoked from a subdirectory within a worktree ran with incorrect CWD and
{{ worktree_path }}/{{ worktree_name }}template variables resolved incorrectly. (#1097) -
copy-ignoredverbose output and error handling:-vflag was silently ignored, error messages lacked file paths, and broken symlinks from interrupted copies caused failures. Also skips non-regular files (sockets, FIFOs) instead of failing. Fixes #1084. (#1090, thanks @jdb8 for reporting) -
Nushell
wt listpiping:wt list --format json | from jsonfailed in nushell because the wrapper's stdout capture prevented piping. Fixes #1062. (#1081, thanks @omerxx for reporting) -
Approved-commands lost during config migration: Running the config migration could silently discard existing approval data. Now copies
approved-commandsentries toapprovals.tomlbefore migration. (#1079) -
Deprecation messages reference
wt config update: Deprecation warnings now point to the newwt config updatecommand for one-step migration instead of manualmvinstructions. (#1089)
Documentation
-
wt switchhelp text: Updated description to "Switch to a worktree; create if needed" to surface auto-create behavior. (#1082) -
Docs syntax highlighting: Migrated to giallo engine with a warm theme. (#1080)
Internal
-
CI reviewer improvements: File-based GraphQL queries, centralized shell quoting guidance, artifact upload path fixes. (#1091, #1098, #1099)
-
Issue triage for external contributors: CI triage workflow now runs for all external contributor issues. (#1086)
0.25.0
Improved
-
System-wide config file: Load organization-wide defaults from a system config file (
/etc/xdg/worktrunk/config.tomlon Linux,/Library/Application Support/worktrunk/config.tomlon macOS) before user config. Override the path with$WORKTRUNK_SYSTEM_CONFIG_PATH. Visible inwt config show. (#963, thanks @goodtune) -
AI summary preview in
wt switch: New 5th tab shows AI-generated branch summaries using your configured[commit.generation]LLM command. Opt-in via[list] summary = truein config. Summaries are cached in.git/wt-cache/summaries/with hash-based invalidation. Docs (#1049) -
Approvals stored in dedicated file: Approved commands moved from
config.tomltoapprovals.toml, enabling dotfile management of config without exposing machine-local trust state. Existing approvals inconfig.tomlare read automatically with a deprecation warning and migration instructions inwt config show. (#1042) -
Error hints include
--executecontext: Whenwt switch --execute=<cmd>fails, suggested commands now include the full--executeand trailing args so you can copy-paste the fix directly. (#1064) -
wt liststartup performance: Config resolution moved into the parallel phase, running concurrently with other git commands instead of sequentially on the critical path. (#1054)
Fixed
-
Submodule worktree path resolution:
wt switchresolved to.git/modules/instead of the working directory inside git submodules. Fixes #1069. (#1070, thanks @SokiKawashima for reporting) -
Per-project
[list] timeoutignored: The timeout setting from per-project config ([projects."name".list]) was not applied — only the global config value was used. (#1063) -
Empty repos crash
wt list: Repositories with no commits (unborn HEAD) caused errors. Now renders empty cells for commit-dependent fields. (#1058) -
Stray blank lines before hints in error output: Error messages with hints (↳) had an extra blank line separating the hint from its subject. (#1072)
Internal
-
Shell escaping consolidation: Dropped
shlexcrate, consolidated onshell_escapeacross the codebase. (#1065) -
CI reviewer improvements: Resolve review threads, skip trivial re-approvals, default to suggestions. (#1068)
0.24.1
Improved
-
Template error messages: Template expansion errors now show what failed, the failing template line, and available variables for undefined variable errors. (#1041)
-
Interactive picker preview speed: Preview pre-computation is parallelized via rayon, reducing the chance of a blocking cache miss when switching preview tabs. (#1048)
-
wt switchperformance: Switching to existing worktrees defers path computation, reducing startup latency. (#1029, #1030, #1031)
Fixed
-
PowerShell wrapper swallows
-Dflag: The wrapper's[Parameter(ValueFromRemainingArguments)]promoted it to an "advanced function", causing PowerShell to consume-Das-Debuginstead of passing it towt.exe. Fixes #885. (#1057, thanks @DiTo97 for reporting) -
Nushell shell integration: Multiple fixes for nushell — auto-detect for install even without vendor/autoload directory (#1032), detection checks multiple config paths (#1038), uninstall cleans all candidate locations (#1050), wrapper hardening and improved diagnostics (#1059). (thanks @arnaudlimbourg for #1032, #1038, and @omerxx for reporting in #964)
-
Interactive picker leaves screen artifacts: The picker left visual artifacts after exiting. Fixes #1027. (#1028, #1044, thanks @davidbeesley)
-
Statusline counts files outside sparse checkout cone: Branch diff statistics in the statusline included files outside the sparse checkout cone, inflating counts. (#1024, thanks @bendrucker)
-
Template placeholders leak into displayed commands:
{{ }}delimiters in hook commands were incorrectly syntax-highlighted, showing ANSI artifacts instead of the template text. (#1022) -
Hook announcement trailing colon: Hook announcements like "Running post-merge project:sync:" had a trailing colon that created visual noise. (#1025)
-
Blank line after approval prompts: Approval prompts showed an extra blank line after the user pressed Enter. (#1040)
Internal
-
Automated Claude PR review: Added workflow for automated code review on PRs. (#1037)
-
Time-to-first-output benchmarks: Added benchmarks for
remove,switch, andliststartup latency. (#1023)
0.24.0
Improved
-
Nushell support (experimental): Initial nushell shell integration — shell wrapper, completions, and
wt config shell installsupport. This is a proof-of-concept and will need iteration before it's usable; if you're a nushell user feel free to try it and report issues. (#964, thanks @arnaudlimbourg) -
Version check in
wt config show --full: The diagnostics section now queries GitHub for the latest release and shows "Up to date", "Update available", or "Version check unavailable". Gated behind--fullso normal commands are unaffected. Closes #1003. (#1011, thanks @risperdal for requesting) -
Fish outdated wrapper detection:
wt config shownow detects when the installed fish shell wrapper has outdated code (e.g., from a previous version) and shows "Outdated shell extension" with a reinstall hint, instead of incorrectly reporting "Not configured". (#1009)
Fixed
-
LLM subprocess blocked in Claude Code sessions: Claude Code sets
CLAUDECODE=1which blocks nested invocations, breakingwt step commitandwt mergecommit generation. Now strips the env var before spawning the LLM command. (#1021) -
Blank line between hint and subject in config show: The "To configure, run wt config shell install" hint was visually detached from the shell entries it referred to. (#1007)
Documentation
-
Status symbol descriptions: Corrected quick start documentation —
↕means diverged from default branch (not unpushed commits),+means staged changes (not uncommitted changes). (#1017) -
Claude Code commit command: Added
CLAUDECODEenv var unsetting to the Claude Code documentation for commit message generation. (#1020)
Internal
-
Environment variable prefix standardization: Renamed remaining
WT_TEST_*env vars toWORKTRUNK_TEST_*, completing the prefix migration. (#1016) -
Plugin metadata: Aligned plugin description with Cargo.toml tagline (#1019), fixed duplicate skills declaration (#1014, thanks @jacksonblankenship for reporting #1013), corrected marketplace source path (#1012).
0.23.3
Improved
-
Error display for failed commands: Failed git commands are now shown in a separate bash-highlighted gutter block instead of inline parenthesized text, making long commands much more readable. (#1001)
-
PowerShell detection and diagnostics: Detect PowerShell via
PSModulePathenvironment variable so Windows users get "shell requires restart" instead of "not installed".wt config shownow displays the detected shell and verification hints. Fixes #885. (#987, thanks @DiTo97 for reporting)
Fixed
-
Fish shell wrapper incompatible with fish < 3.1: The shell wrapper used
VAR=value commandsyntax which requires fish 3.1+. Now usesenv VAR=value ...for compatibility with all fish versions. Fixes #999. (#1000, thanks @chrisrickard for reporting) -
Symlink paths resolved in display messages: Status messages like "Created worktree @ path" showed canonical paths instead of the user's symlink path. Now consistent with cd directives. Fixes #968. (#985, thanks @brooke-hamilton for reporting)
Documentation
-
Deduplicated manual shell setup: Removed duplicated per-shell eval snippets from
wt config --help, referencingwt config shell init --helpinstead. (#986) -
PowerShell diagnostic guidance: Added PowerShell-specific debugging steps to shell integration and troubleshooting references. (#993)
0.23.2
Improved
--forceflag forwt step copy-ignored: Overwrite existing destination files when copying gitignored files to new worktrees. Closes #971. (#974, thanks @williamgoulois for requesting)
Fixed
-
wt switch pr:NNNN/mr:NNNNfails in repos without fetch refspecs: Same-repo PRs and MRs failed with "No branch named X" in single-branch clones or bare repos because fetch didn't create remote tracking branches, and worktree creation relied on DWIM. Now uses explicit refspecs and-bfallback. (#965, thanks @andoniaf) -
Progressive table garbled when output exceeds terminal height:
wt listoutput was corrupted when more lines than the terminal height, because cursor-up commands tried to reach scrolled-off lines. Now detects overflow and falls back to a clean full-table print. (#981) -
Symlink paths resolved to canonical in cd directives: When navigating via symlinks, cd directives wrote canonical paths, silently moving users out of their symlink tree. Now preserves the user's logical path. Fixes #968. (#976, thanks @brooke-hamilton for reporting)
-
Terminal artifacts when cancelling interactive picker: Pressing Esc to cancel the picker left terminal artifacts and a misplaced cursor. Now skim handles cleanup symmetrically for both cancel and accept. (#984)
Documentation
- Hook examples: safer port cleanup: Added
-sTCP:LISTENtolsofin hook examples to prevent accidentally killing unrelated processes with connections to the port. (#952, thanks @andoniaf)
0.23.1
Improved
-
Interactive picker runs hooks:
wt switchwithout arguments (the interactive picker) now runs post-switch, post-start, and post-create hooks, matching the non-interactive path. (#942) -
Combined hook output during removal: Post-remove and post-switch hooks during worktree removal are now shown on a single output line instead of two separate lines. (#943)
Fixed
-
Shell escape corruption with template filters: Shell escaping was applied before template rendering, so filters like
sanitizeoperated on already-escaped strings, corrupting values with special characters (e.g., apostrophes in branch names). (#944) -
wt switch -history corruption:wt switch foowhile already infoowould incorrectly recordfooas the previous branch, breakingwt switch -ping-pong. (#944) -
--basewithout--createshowed wrong error: Using--basewithout--createcould produce misleading errors (e.g., "No previous branch") instead of the expected warning that--baserequires--create. (#944)
0.23.0
Improved
-
Preserve subdirectory position when switching:
wt switchnow lands in the same subdirectory of the target worktree if it exists, falling back to the root if it doesn't. Docs (#939, thanks @frederik-suerig for requesting) -
wt switch --no-cd: Skip the directory change after switching, useful for scripting or running commands in another worktree without leaving your current shell position. Docs (#932, thanks @ArnaudRinquin for requesting) -
Alt-cto create worktree from picker: In the interactive picker, pressAlt-cto create a new worktree using the current query as the branch name. (#933) -
Faster preview tab switching: Preview tabs (HEAD±, log, main…±, remote⇅) are now pre-computed in a background thread, making tab switching near-instant. (#935)
Fixed
-
Pager width detection: Makes preview pane width available to pagers via
$COLUMNS, so tools like delta can use it for correct side-by-side rendering (e.g.,pager = "delta --width=$COLUMNS"). Fixes #924. (thanks @tnlanh for reporting) (#930) -
ANSI style bleeding in preview tabs: Fixed styling artifacts where dividers appeared emphasized and diffstat lines appeared dim. (#931)
-
URL template expansion with
--skip: Skip URL template expansion when--skip url-statusis used, avoiding unnecessary work. (#923) -
Hook error consistency:
wt hook <type>now errors consistently for all hook types when no hooks are configured, instead of silently succeeding for some types. (#916)
Documentation
- Improved install instructions in release notes. (#918)
Internal
- CI: check for existing fix PRs before creating duplicates. (#922)
0.22.0
Improved
-
wt switchintegrates interactive picker:wt switchwithout arguments now opens the interactive picker (previouslywt select). The separatewt selectcommand is deprecated with a warning directing users to usewt switchinstead. Closes #890. (thanks @strangemonad for the suggestion) (#894) -
TOML syntax highlighting: Config output from
wt config showandwt config shell show-themenow renders TOML with syntax highlighting (table headers cyan, string values green, comments dimmed). (#905) -
Bash syntax highlighting improvements: Multi-line bash commands in hook previews now preserve syntax highlighting across newlines. Wrapped continuation lines are indented with 3 extra spaces to distinguish terminal-forced wraps from actual newlines. (#906)
-
Unified background hook output: Contiguous post-switch and post-start hooks are now combined into a single output line instead of two separate lines. (#908)
Documentation
- Removed redundant horizontal rules before H1 headers in documentation pages. (#909)
Internal
- Updated GitHub Actions and Rust nightly versions. (#910)
- Bumped tree-sitter ecosystem to 0.26 for unified multi-line highlighting. (#906)
- Dependency updates: minijinja 2.15.1, clap, indexmap, ignore, thiserror, time, and others. (#912, #913)
0.21.0
Improved
- Absolute paths in
worktree-pathtemplates: New{{ repo_path }}variable enables absolute path configurations like{{ repo_path }}/../{{ repo }}.{{ branch | sanitize }}. Tilde expansion is also supported (~/worktrees/{{ repo }}/{{ branch }}). Fixes #902. (thanks @bingryan for reporting) (#904)
Documentation
- Documented prefix stripping in
worktree-pathtemplates using minijinja's built-inreplacefilter and slicing syntax. Closes #900. (thanks @laurentkempe for requesting) (#903)
0.20.3
Fixed
-
PowerShell auto-configuration on Windows: When running
wt config shell installfrom cmd.exe or PowerShell, both PowerShell profile files are now created automatically (Documents/PowerShell and Documents/WindowsPowerShell). Fixes #885. (thanks @DiTo97 for reporting) (#898) -
-Cflag respected in hook context: The-Cflag now correctly sets the worktree path for hooks, fixingwt -C /path hook ...commands that were using the wrong context. (#899) -
--configpath validation: Now warns when--configpoints to a non-existent file instead of silently using defaults. (#895)
Documentation
-
Fix shell quoting in hook examples — template variables are auto-escaped, so manual quoting caused issues with special characters. (#895)
-
Updated documentation to use tool-agnostic terminology for LLM commit messages. (#891)
Internal
-
Consolidated PR/MR resolution into unified
remote_refmodule. (#893) -
Simplified command structure and removed dead code. (#892)
-
Eliminated Settings types, added accessor methods to Config types. (#896)
0.20.2
Fixed
-
PowerShell shell integration: Fixed shell integration not working on Windows PowerShell. The init script now includes
| Out-Stringto convert array output to a string. Existing configs without this fix are detected as "not installed" sowt config shell installwill update them automatically. Fixes #885. (thanks @DiTo97 for reporting) (#888) -
Branch removal message: "No worktree found for branch X" now shows as info (○) instead of warning (▲) when removing a branch-only, since this is expected behavior. (#887)
Documentation
- Documented main worktree behavior in
wt step relocate --help. (#889)
0.20.1
Improved
-
wt statusline --format=json: Output current worktree as JSON (same structure aswt list --format=json). Also adds--format=claude-codeas canonical syntax (the old--claude-codeflag remains supported). Fixes nested worktree detection that incorrectly identified parent worktrees. (#875) -
wt config showshell status: Each shell integration line now starts with the shell name (e.g., "bash: Already configured...") for easier scanning. (#881) -
wt config showperformance: 8x faster (~1.2s → ~150ms) by using PATH lookup instead of runningclaude --version. (#883)
Fixed
-
Config TOML formatting: Fixed spurious empty
[commit]header appearing when only[commit.generation]is configured. (#879) -
Documentation URLs: Fixed broken worktrunk.dev URLs in fish wrapper and config templates. (#882)
Documentation
-
Fixed
worktree-pathexample on tips page. (#876, thanks @uriahcarpenter) -
Fixed OSC 8 hyperlink sequences leaking through to web docs as garbage text. (#870)
Internal
-
Demo snapshot mode for regression testing of command output. (#871)
-
CI improvements: nextest binary compatibility fix, pinned runner versions, weekly renovation workflow. (#878, #884)
0.20.0
Improved
-
wt step relocatecommand: Move worktrees to their expected paths based on theworktree-pathtemplate. Supports--dry-runpreview, filtering by branch name, and--committo auto-commit dirty worktrees before moving. Handles complex scenarios including worktree swaps (A→B, B→A), chains, and the--clobberflag to back up blocking non-worktree paths. Docs (#790) -
LLM setup prompt: First-time interactive prompt when users attempt
wt merge,wt step commit, orwt step squashwithout LLM configuration. Detects available tools (claude, codex) and offers auto-configuration with?to preview the generated config. Addskip-commit-generation-promptto user config to suppress. (#867) -
Consistent prompt styling: Interactive prompts now use consistent cyan styling via
prompt_message()formatting. (#858)
Fixed
- Path display in error messages: User-facing paths now consistently use
format_path_for_display(), fixing cases where raw.display()output could show inconsistent path formats. (#856)
Documentation
- Added Quick Start section to front page showing the switch → list → merge workflow. (#864)
- Updated template documentation: removed deprecated
template-fileoptions, added{{ git_diff_stat }}variable, clarified squash-only variables. (#854) - Fixed stale documentation for
[commit.generation]config format, statusline context gauge, and CI status for remote-only branches. (#853)
Internal
- Bumped nix crate from 0.30.1 to 0.31.1. (#860)
- Refactored deprecation detection for better modularity. (#852)
0.19.0
Improved
-
LLM commit configuration redesign: The
[commit-generation]section is now[commit.generation], andcommand+argsare unified into a single shell-executedcommandstring. Existing configs continue to work — a deprecation warning shows the new format and creates a.newconfig file you can apply withmv. Claude Code (claude -p) and Codex (codex exec) are documented as first-class options alongsidellm. See the LLM commits guide. (#809, #837) -
Per-project hooks: User config can define hooks per-project that append to global hooks. Execution order: global → per-project → project config. Configure under
[projects."owner/repo".hooks]. (#842) -
Context window gauge for Claude Code: Statusline mode shows a moon phase gauge (🌕🌔🌓🌒🌑) for context window usage. (#840)
-
CI status for remote-only branches:
wt list --remotesshows CI status for branches that only exist on the remote. (#817) -
Hook log file lookup:
wt config state logs get --hook=<spec>returns the path to a specific hook's log file. (#816, thanks @EduardoSimon for requesting) -
Branch/fork info in PR/MR display:
wt switch pr:Nshows the source branch (e.g.,feature-auth) or fork reference (e.g.,contributor:feature) alongside PR details. (#808) -
Claude Code section in
wt config show: Shows Claude CLI installation status, plugin status, and statusline configuration. (#833) -
Deprecation details moved to
wt config show: Other commands show a brief pointer instead of full deprecation details. (#828) -
Config validation suggests correct file: When a config key belongs in user config but appears in project config (or vice versa), the warning suggests the correct location. (#804)
-
Tilde paths in hints: Shell command hints use
~instead of full home directory paths when safe. (#710) -
Improved
--createconflict error:wt switch --create pr:101shows the existing branch name in the error. (#807) -
CI status prioritized in statusline: CI status is retained longer when the statusline truncates. (#845)
Fixed
-
Template expansion bugs: Fixed
worktree_path_of_branchnot respecting shell_escape flag, Windows CI cache rename failures, andWORKTRUNK_MAX_CONCURRENT_COMMANDS=0meaning "no limit". (#847, #849) -
Hook and CI status panics: Fixed panic when serializing mixed named/unnamed hook configs, banned colons in hook names to prevent parsing ambiguity, and fixed GitLab MR detection when multiple MRs exist without project ID. (#846, #848)
-
Pre-commit hooks for clean worktree squash: Pre-commit hooks are collected for approval when squashing on a clean worktree. Previously only collected when dirty. (#695)
-
Hint message formatting: Fixed ANSI escape code interference in dim hint messages. (#836)
-
Spurious [commit] header: Fixed config migration showing
[commit]section header when onlycommit-generationfields needed migration. (#834)
Documentation
- Added at-a-glance examples to config documentation. (#826)
- Clarified user project-specific settings section. (#835)
- Consistent worktree terminology throughout docs. (#813)
- Added tip for monitoring hook logs. (#838)
Internal
- Replaced manual quote escaping with
shell_escapecrate. (#810) - Used
sanitize-filenamecrate for filename sanitization. (#832) - Cached CI tool availability checks. (#831)
- Moved inline imports to module top level. (#818, #819, #820, #822)
0.18.2
Improved
-
PR/MR context display:
wt switch pr:Nandmr:Nnow show PR/MR details (title, author, state, URL) after fetching. (#782) -
Fork PR branch conflicts: When a fork PR's branch name conflicts with an existing local branch (e.g., contributor opens PR from their
main), worktrunk now creates a prefixed branch likecontributor/maininstead of failing. Closes #714. (thanks @vimtor for reporting)
Fixed
-
Help output formatting: Fixed double blank lines appearing after demo comments in help output. (#795)
-
Error handling reliability: Replaced fragile string-based error parsing with structured approaches for git stash, GitHub CLI, and GitLab CLI operations. (#787)
Documentation
-
ci-status help text: Improved clarity of the ci-status configuration documentation. (#794)
-
wt remove help text: Simplified short description and added documentation for
pre-removeandpost-removehooks. (#792) -
Subcommand documentation: Fixed generated website docs for subcommands (like
wt step copy-ignored,wt config state) to include their short descriptions. (#793)
0.18.1
Fixed
- Submodule worktree paths: Worktrees are now created in the correct location when running inside a git submodule. Previously, worktrees were created relative to the parent repo's
.git/modules/directory instead of the submodule's working directory. (#762, thanks @lajarre; #777, thanks @mhonsel for reporting) - Shell integration warnings: Warnings about shell integration now check if the current shell has integration configured, not whether any shell does. This fixes misleading "shell requires restart" messages when e.g. bash had integration but the user was running fish. (#772)
- "Not found" error messages: Improved error message phrasing — "No branch named X" instead of "Branch X not found", "Branch X has no worktree" instead of "No worktree found for branch X". Context-appropriate hints now appear (e.g.,
wt removeno longer suggests--create). (#774)
Internal
- Unified PR/MR reference resolution, reducing code duplication. (#778)
0.18.0
Improved
- Post-remove hook: New hook type runs after worktree removal. Template variables (
{{ branch }},{{ worktree_path }},{{ commit }}) reference the removed worktree, enabling cleanup scripts for containers, servers, or other resources. (#757) - Graceful handling of missing worktree directories:
wt removenow prunes stale git metadata when the worktree directory was deleted externally (e.g.,rm -rf), making the command more idempotent. Fixes #724. (thanks @strangemonad for reporting) - Config validation warnings at load time: Unknown fields in config files (typos like
[commit-gen]instead of[commit-generation]) now show warnings immediately instead of only inwt config show. (#758)
Fixed
- Age column shows "future" on NixOS/direnv:
wt listno longer usesSOURCE_DATE_EPOCHfor time calculations, which NixOS and direnv commonly set to past timestamps for reproducible builds. Fixes #763. (thanks @ngotchac for reporting) - CI status with URL-based pushremote: CI detection now works when
branch.<name>.pushremoteis set to a URL directly (asgh pr checkoutdoes) instead of a remote name. (#769) - GitLab nested groups in URL parsing: URLs like
gitlab.com/group/subgroup/reponow correctly identifyrepoas the repository name instead ofsubgroup. This was a security fix — previously, approval bypass was possible across sibling repos in the same parent group. (#768) - GitLab CI status detection: Fixed multiple issues with
glabCLI compatibility — MR lookup now uses two-step resolution, "manual" pipelines show as running instead of failed, and rate limit errors are handled properly. Fixes #764. (thanks @ngotchac for reporting)
Internal
- Refactored accessor functions to use bare nouns per Rust convention. (#765)
- Clarified target/integration naming across codebase. (#755)
0.17.0
Improved
- Per-project config overrides (Experimental): Override settings per-project in user config. Supports
worktree-path,commit-generation,list,commit, andmergesections. Config precedence: CLI arg > project config > global config > default. Closes #596. (#749) - Search all remotes for branch existence: Branch existence checks and completions now search all remotes instead of just the primary remote, matching git's behavior. When a branch exists on multiple remotes, completions show all of them (e.g.,
feature ⇣ 2d origin, upstream). (#744) - CI detection for fork workflows: CI status detection now searches all remotes and uses
gh config get git_protocol/glab config get git_protocolfor fork URL protocol preference instead of inferring from existing remotes. (#753)
Fixed
- Same-repo PR switching with stale refs:
wt switch pr:Nfor same-repo PRs now fetches the branch before validation, fixing "Branch not found" errors when local refs were stale. (#742) - Project identifier collision for repos without remotes: Repos without remotes now use their full canonical path as the project identifier instead of just the directory name, preventing approval collisions between unrelated repos (e.g.,
~/work/myprojectvs~/personal/myproject). Users with remoteless repos will need to re-approve commands. (#747)
Internal
- Cross-platform path handling improvements using
path-slashcrate andPath::file_name(). (#750) - Renamed
WorktrunkConfigtoUserConfiginternally. (#746)
0.16.0
Improved
- Background hook verbosity: Background hooks (post-start, post-switch) now show a single-line summary by default instead of per-hook output. Use
-vto see detailed output with expanded commands. We're open to feedback on this change — let us know in #690. (thanks @clutchski for reporting)
Internal
- Fixed dead Apple documentation link in copy-ignored rationale. (#743)
0.15.5
Fixed
- Hook execution order: Hooks now run in the order defined in the config file. Previously, HashMap iteration randomized the order. Fixes #737. (thanks @ngotchac for reporting)
0.15.4
Improved
- Git progress for slow worktree creation: When
git worktree addtakes more than 400ms (common on large repos), worktrunk now shows a progress message and streams git's output instead of going silent. (#725) - Verbose template expansion output:
-vnow shows template expansion details: the template, expanded command, and any undefined variables with SemiStrict fallback behavior. (#712) - Shell integration hint for explicit path invocation: When running wt via explicit path (e.g.,
./target/debug/wt) with shell integration configured, the warning now suggests runningwt switch <branch>to use the shell-wrapped command. (#721)
Fixed
- Unsafe upstream when creating branch from remote base:
wt switch --create feature --base=origin/mainno longer sets up tracking to origin/main, preventing accidental pushes to the base branch. Fixes #713. (thanks @kfirba) - Credential redaction in debug logs: URLs with embedded credentials (e.g.,
https://token@github.com/...) are now redacted in-vvdebug output. (#718) - Hook preview shows template on expansion failure:
wt hook show --expandednow displays both the error message and original template when expansion fails, instead of hiding the template. (#722)
Documentation
- Homebrew install uses core tap: Install command updated from
max-sixty/worktrunk/wttoworktrunk. (#716, thanks @chenrui333) - Hook docs reordered: post-start (background) is now the recommended default, with post-create for blocking dependencies. (#733)
Internal
- Simplified GitHub/GitLab CI status detection. (#730)
- Previous worktree gutter changed from
-to+for visual consistency. (#699)
0.15.3
Fixed
--executecommand display: Shows the expanded command in a gutter with path context instead of showing the raw template before expansion. (#708)- CRLF line endings in error display: Multiline errors with Windows (
\r\n) or old Mac (\r) line endings now display correctly instead of falling through to single-line handling. (#707)
Documentation
- Arch Linux install via AUR: Added installation instructions and shell integration command. (#709, #561, thanks @razor-x)
0.15.2
Improved
wt config shell completions <shell>: Generate static shell completion scripts for package managers and custom installation. (#701, thanks @chenrui333)- Debug logging threshold: Now requires
-vvinstead of-vfor debug logging and diagnostic file generation, freeing-vfor future use. (#702)
Fixed
- Fork PR fetching:
wt switch pr:Nnow works whenoriginpoints to a fork by fetching PR refs from the upstream remote. Shows actionable error withgit remote addcommand if upstream remote is missing. (#704) - Fork PR branch naming: Fork PR branches now use the original branch name (e.g.,
feature-fix) instead ofowner/feature-fix, sogit pushworks correctly. (#706) - Config race conditions: File locking prevents corruption when multiple
wtprocesses modify config simultaneously. (#693) - Nested worktree detection: Current worktree indicator (
@) now shows on the correct worktree when worktrees are nested (e.g.,.worktrees/layout inside repo). (#697) - Symlink path resolution: Worktree commands work correctly on systems with symlinks (e.g., macOS
/var→/private/var). (#696) - Pre-remove hook failures: Shell no longer cd's to main worktree when pre-remove hooks fail, leaving user in their current location. (#692)
- PowerShell completion robustness: Completion registration errors no longer break the shell wrapper function. (#674)
Documentation
- Added missing
orphan(∅) symbol andno_worktreestate to JSON output documentation. (#687) - Clarified Unicode handling in shell detection. (#700)
Internal
- Refactored large files into focused modules. (#688)
- Consolidated integration reason computation into Repository method. (#689)
- Added verbose level tracking infrastructure for future
-voutput. (#703) - PowerShell template uses
WORKTRUNK_BINfor test isolation. (#674)
0.15.1
Improved
wt config showdiagnostics: When shell integration is not active, now shows how the command was invoked, the binary path (if different), and$SHELLenvironment variable. Helps diagnose setup issues. (#683)- Help pager follows git convention:
-hnever opens a pager,--helpuses pager when available. Closes #583. (#651, thanks @razor-x) - Verbose mode logging:
-vnow logs command stdout/stderr and all spawned processes including background hooks,wt for-eachcommands, and shell probes. (#680)
Documentation
- FAQ reordered: Questions now ordered by frequency and importance.
Internal
- AUR package: Worktrunk now published to Arch Linux AUR on each release. (#585, thanks @razor-x)
- Codecov Test Analytics: Integration tests now report to Codecov Test Analytics. (#682)
0.15.0
Improved
wt switch pr:<number>syntax (experimental): Switch directly to a GitHub PR by number. Same-repo PRs delegate to normal switch flow; fork PRs fetch from refs/pull/N/head and configure pushRemote. (#673, closes #657, thanks @wladpaiva for requesting)--forcehint for dirty worktrees: Whenwt removefails due to uncommitted changes, the hint now shows the full command:wt remove <branch> --force. (#671)
Documentation
- Windows install guidance: Winget as recommended install (ships
git-wtby default), plus the App Execution Aliases workaround to usewtdirectly. Closes #133. (thanks @ctolkien for reporting, @shanselman for the aliases tip, @Farley-Chen for #648) - Caddy subdomain routing pattern: Clean URLs like
feature-auth.myproject.lvh.mevia Caddy reverse proxy with dynamic route registration. - tmux session per worktree pattern: Dedicated tmux session with multi-pane layout per worktree.
0.14.2
Fixed
wt remove --forceworks with dirty worktrees: The--forceflag was documented to allow removal with uncommitted changes, but worktrunk's own cleanliness check blocked it before git could apply the flag. Fixes #658. (thanks @pedro93)- Correct output when switching to existing local branch: When switching to a local branch that tracks a remote, worktrunk incorrectly reported "Created branch X" instead of "Created worktree for X". Now only reports branch creation when git's DWIM actually creates a new tracking branch from a remote. Fixes #656. (thanks @guidupuy-ws)
- PowerShell handles multiple
wt.exebinaries: On Windows, when both Windows Terminal'swt.exeand worktrunk'swt.exeexist in PATH, shell integration errored with "Cannot convert 'System.Object[]' to the type 'System.String'". Now correctly uses the first match. Relates to #648. (thanks @Farley-Chen)
0.14.1
Improved
--baseaccepts commit-ish refs:wt switch --create --basenow accepts HEAD, tags, commit SHAs, and relative refs (e.g.,HEAD~2), not just branch names. Fixes #630. (thanks @myhau)- Upfront validation for target refs:
wt mergeandwt stepcommands now validate target refs before approval prompts, giving clearer "Branch X not found" errors immediately. - Visual hierarchy in help: Section dividers, improved heading structure, and sentence case in
--helpoutput.
Fixed
- macOS shell freeze during
copy-ignored: Atomicclonefile()on directories saturated disk I/O, blocking shell startup. Now uses per-file reflink which is slower but keeps the system responsive. copy-ignoredno longer copies nested worktrees: Whenworktree-pathplaces worktrees inside the main worktree,copy-ignorednow skips them. Also now copies symlinks (fixesnode_modules/.bin/etc.). Fixes #641. (thanks @razor-x)- Context-aware hints for
wt config create: Hints now suggest relevant next steps based on which configs exist.
0.14.0
Improved
worktree_path_of_branch(branch)template function: Look up the filesystem path of any branch's worktree in hooks. Enables copying files between worktrees:setup = "cp {{ worktree_path_of_branch('main') }}/config.local {{ worktree_path }}". Returns empty string if no worktree exists for the branch.- Per-task timeout for
wt list: Configure timeout for git operations via[list] timeout-msin user config. Shows timeout count in footer. Use--fullto disable timeout for complete data collection. - Atomic COW directory cloning on macOS:
wt step copy-ignoredusesclonefile()syscall on APFS for O(1) directory cloning instead of file-by-file copying. ~12-15x faster for large directories liketarget/. - Template variable renamed:
main_worktree_path→primary_worktree_pathfor clarity. Old name still works with deprecation warning.
Fixed
wt step copy-ignoredin bare repositories: Fixed "this operation must be run in a work tree" error when using bare repo setups. Closes #598. (thanks @sbennett33 for reporting)
Internal
- Help system extraction: Moved help and invocation utilities from main.rs to dedicated modules.
wt listmodel refactor: Split monolithic model.rs into modular directory structure.
0.13.4
Fixed
- LESS flag concatenation with long options: Fixed "invalid option" error when users have long options in LESS (e.g.,
LESS=--mouse). The pager auto-quit feature from v0.13.1 now correctly separates flags. Fixes #594. (thanks @tnlanh for reporting)
Internal
- Homebrew formula generation: Release workflow now uses cargo-dist for Homebrew formula generation, simplifying the release process.
0.13.2
Improved
- Validate before approval prompts:
wt switchandwt removenow validate operations before prompting for hook approval, so users don't approve hooks for operations that will fail.
Fixed
- Homebrew formula SHA256 hashes: Fixed release workflow that was setting incorrect checksums for Intel and Linux binaries, causing
brew installto fail. Fixes #589. (thanks @kobrigo for reporting)
0.13.1
Fixed
- Pager auto-quit: Help text now auto-quits when it fits on screen, even when
LESSis set without theFflag (common with oh-my-zsh'sLESS=-Rdefault). Fixes #583. (thanks @razor-x for reporting) --createhint for remote branch shadowing: Improved recovery hint when--createshadows a remote branch — now shows the full recovery command.
0.13.0
Improved
wt listparallelization improvements: Better parallelization of worktree operations reduce latency in some conditions. RespectsRAYON_NUM_THREADSenvironment variable for controlling parallelism.- Template variables in
--execute: Hook template variables ({{ branch }},{{ worktree_path }}, etc.) are now expanded in--executecommands and trailing args. With--create,{{ base }}and{{ base_worktree_path }}are also available. - Fish shell Homebrew compatibility: Fish shell integration now installs to
~/.config/fish/functions/wt.fishinstead ofconf.d/, ensuring PATH is fully configured before the wt function loads.wt config showdetects legacy installations andwt config shell installhandles migration automatically. (#586 — thanks @ekans & @itzlambda) - Chrome Trace Format export: Performance traces can be exported for analysis with Chrome's trace viewer or Perfetto.
--dry-runflag for shell commands:wt config shell installandwt config shell uninstallnow support--dry-runto preview changes without prompting.- Nested subcommand suggestions: When typing
wt squashinstead ofwt step squash, the error now suggests the correct command path. - Orphan branch indicator:
wt listshows∅(empty set) for orphan branches with no common ancestor to the default branch. - Improved
-vvdiagnostic workflow: Bug reporting hint now uses a gist workflow to avoid URL length limits.
Fixed
wt switch --create --baseerror message: Now correctly identifies the invalid base branch instead of the target branch. Fixes #562. (thanks @fablefactor)- AheadBehind column loading indicator: Shows
⋯when not yet loaded instead of appearing empty, distinguishing loading state from "in sync". - Post-merge hook failure output: Simplified error messages and removed confusing
--no-verifyhint. wt selectlog preview: Graph structure is now preserved when displaying commit history, and columns dynamically align.
Documentation
- FAQ entry for shell setup issues: Added troubleshooting guidance for common shell integration problems.
- Template variables reference: Consolidated template variables documentation into hook.md.
- Clarified
--forcevs-Dflags: Updatedwt removedocumentation. (thanks @hlee-cb) - Performance benchmarks: Added documentation for
copy-ignoredperformance.
0.12.0
Improved
wt select --branchesand--remotesflags: Control which items appear in the selection UI. Shares the[list]config section withwt listfor consistent defaults.- Graceful degradation when default branch unavailable: When the default branch cannot be determined (e.g., misconfigured),
wt listshows warnings and empty cells rather than failing.wt switch --createwithout--basegives a clear error message. - Remove
--refreshflag from state commands:wt config state default-branch getandwt config state ci-status getnow purely read cached state. To force re-detection, use the explicit workflow:clearthenget. (Breaking:--refreshflag removed) - Windows: Require Git for Windows: Removed PowerShell fallback. Worktrunk now requires Git for Windows (Git Bash) and shows a clear error message pointing to the download page if not found. (Breaking: PowerShell no longer supported)
Fixed
- Flag styling in messages: Flags like
--clobberand--no-verifyin parentheses now inherit message color instead of using bright-black styling. - Nix flake: Remove apple_sdk framework dependency. (#525, thanks @MattiasMTS)
gh issue createhint: Now includes--webflag to open the issue form in browser.
Internal
- Binary size reduced ~1MB: Trimmed unused config/minijinja features (13MB → 12MB).
- Repository module split: Split 2200-line module into 8 focused submodules for maintainability.
0.11.0
Improved
- Nix flake for packaging: New
flake.nixfor Nix users with crane for efficient Rust builds. (#502, thanks @marktoda; thanks @Kabilan108 for requesting) sanitize_dbtemplate filter: New filter that transforms strings into database-safe identifiers with a 3-character hash suffix for collision/keyword safety. (#498, thanks @hugobarauna for requesting)wt selectperformance: 500ms timeout for git commands improves TUI responsiveness on large repos with many branches. (thanks @KidkArolis for reporting #461)wt selectstale branch handling: Branches 50+ commits behind the default branch now skip expensive operations, showing...in the diff column. Improves performance on repos with many stale branches.- Global merge-base cache: Cached merge-base results improve
wt listperformance by avoiding redundant git calls. wt config showgit version: Now displays the git version alongside the worktrunk version.wt step copy-ignoreddefault: Now copies all gitignored files by default. Use.worktreeincludeto limit what gets copied (previously required.worktreeincludeto specify what to copy).- Trace log analysis: New
analyze-tracebinary for analyzing[wt-trace]performance logs.
Fixed
- Statusline truncation: No longer truncates when terminal width is unknown, fixing Claude Code statusline display.
- Shell completions: Deprecated args like
--no-backgroundno longer appear in tab completions. wt removeprogress ordering: Progress message now appears after pre-remove hooks, not before.wt listindex lock: Uses--no-optional-locksfor git status to avoid lock contention with parallel tasks.
0.10.0
Improved
wt step copy-ignored: Copy gitignored files listed in.worktreeincludebetween worktrees. Useful for syncing.envfiles, IDE settings, and build caches to new worktrees via post-create hooks. Uses COW (reflink) copying for efficient handling of large directories. Matches Claude Code Desktop's worktree file syncing behavior.--foregroundflag: Debug background hooks by running them in the foreground. Available onwt hook post-start,wt hook post-switch, andwt remove. Replaces the deprecated--no-backgroundflag.--varflag for hooks: Override template variables when running hooks manually, e.g.,wt hook post-create --var target=main.ci.platformconfig: Explicitly set CI platform (githuborgitlab) for GitHub Enterprise or self-hosted GitLab where URL-based detection fails.- Upstream diff in
wt select: Tab 4 shows ahead/behind diff vs upstream tracking branch (remote⇅), matching the column inwt list. {{ base }}and{{ base_worktree_path }}variables: New template variables for creation hooks (post-create, post-start, post-switch) to access the base branch name and worktree path.-vvdiagnostic reports: Double-verbose flag writes a diagnostic report to.git/wt-logs/diagnostic.mdwith environment info, configs, and logs for easy bug reporting.
Fixed
- Warning ordering: Warnings about state discovered during evaluation now appear before the action message, making them feel like considered observations rather than afterthoughts.
- Config validation in
wt config show: Now validates TOML syntax and schema, displaying parse errors with details.
Documentation
- Undocumented features: Added documentation for
--show-promptand--stageflags onwt step commit/squash,skip-shell-integration-promptconfig, and[select] pagerconfig.
0.9.5
Improved
- Pager config for
wt select: New[select] pagerconfig option to customize the diff pager inwt selectpreviews. Auto-detects delta/bat when not configured. - Infinity symbol for extreme diffs:
wt listshows∞instead of9Kfor diffs >= 10,000 commits, avoiding misleading values.
Fixed
- Windows shell integration message: Warning now shows just the command name instead of the full absolute path, and gives targeted advice when only the
.exesuffix differs. - URL column width: Column width in
wt listnow accounts for hyperlink display showing just:PORTinstead of full URLs.
Internal
- Deprecated
template-fileandsquash-template-file: Legacy LLM template config options now show deprecation warnings. - Path handling improvements: Replaced string manipulation with proper Path/PathBuf stdlib methods throughout the codebase.
0.9.4
Improved
- Diagnostic report generation:
wt list --verbosegenerates diagnostic reports (.git/wt-logs/diagnostic.md) when warnings or errors occur, with agh issue createcommand hint when GitHub CLI is available. - Alias bypass detection:
wt config showdetects shell aliases that point to binary paths (e.g.,alias gwt="/usr/bin/wt") and warns that they bypass shell integration with suggested fixes. - Switch message clarity: Messages now explicitly state what was created — "Created branch X and worktree" vs "Created worktree for X" vs "Switched to worktree for X".
- Worktree-path hint: One-time hint after first
wt switch --createsuggestingwt config createto customize worktree locations. - Path mismatch warnings:
wt removeandwt mergeshow warnings when worktree paths don't match the config template. - CLI command ordering: Commands reordered by usage frequency in
--help(switch, list, remove, merge...).
Fixed
- Progress counter overflow: Fixed
wt listprogressive rendering when URL sends caused completed count to exceed expected count. - Windows shell integration: Shell function now correctly strips
.exesuffix, relying on MSYS2/Git Bash automatic resolution (fixes #348). - Prunable worktrees: Gracefully handle worktrees where the directory was deleted but git still tracks metadata.
- Help text tables: Disabled clap text wrapping to preserve markdown tables in
--helpoutput.
Documentation
- FAQ entries: Added entries for "What files does Worktrunk create?" and "What can Worktrunk delete?".
Internal
- Hint state management: New
wt config state hintssubcommand for viewing and clearing shown hints. - Deprecated config deduplication: Migration files (
.new) only written once per repo, tracked via git config hints.
0.9.3
Improved
- Terminal hyperlinks for URLs: The URL column in
wt listnow shows clickable links (OSC 8) in supported terminals, displaying a compact:portthat links to the full URL. - Statusline truncation: Statusline output now intelligently truncates by dropping low-priority segments (URL, CI) before high-priority ones (branch, model) when exceeding terminal width.
- Statusline URL: When a project has a
[list] urltemplate configured, the URL now appears in statusline output for shell prompts. - Bare repo default branch detection: Uses
symbolic-ref HEADas a heuristic for detecting the default branch in bare repos and empty repos before the first commit. - Terminology: Renamed "path mismatch" to "branch-worktree mismatch" for clarity. In JSON output (
wt list --format=json), the fieldpath_mismatchis nowbranch_worktree_mismatch.
Fixed
- Empty bare repo bootstrap:
wt switch --create mainnow works in empty bare repos by handling unborn branches correctly.
Documentation
- CLI help text: Improved descriptions across multiple commands including
wt,wt list,wt select,wt step,wt merge,wt remove, andwt hook. - Web docs copy button: Fixed copy button position so it stays at top-right when scrolling horizontally through code blocks.
Internal
- Claude Code plugin detection:
wt config shownow displays whether the worktrunk Claude Code plugin is installed, with install hints if needed. - Hyperlink diagnostics:
wt config showshows hyperlink support status (active/inactive).
0.9.2
Fixed
- Locked worktree detection:
wt removenow detects locked worktrees upfront and shows a clear error with unlock instructions, instead of reporting success but silently failing. (#408, #412) - Windows Git Bash shell integration: Shell detection now handles Windows-style paths in
$SHELL(e.g.,C:\Program Files\Git\usr\bin\bash.exe). Fixes #348. (#398)
Documentation
- CLI help text clarity: Improved descriptions for
wt,wt list,wt step push,wt step squash,wt remove, andwt config state. (#410) - Installation commands: Removed
$prefixes from install commands for easier copy-paste. (#405, thanks @muzzlol)
Internal
- Home worktree lookup: Centralized with
find_home()andhome_path()methods for more consistent behavior with bare repos. - Windows CI: Added cross-platform mock infrastructure for testing Windows-specific behavior.
0.9.1
Improved
- Shell integration debug info:
wt config shownow displays invocation details (path, git subcommand mode, explicit path usage) to help diagnose shell integration issues. "Shell integration not active" is now a warning instead of a hint.
0.9.0
Improved
- Shell integration prompt: When shell integration isn't active after
wt switch, an interactive prompt offers to install it. The prompt remembers your choice and falls back to a hint for non-TTY environments. - Template variable names: Renamed for clarity:
repo_root→repo_path,worktree→worktree_path,main_worktree→repo. Addedmain_worktree_pathfor accessing the main worktree's absolute path. Deprecated names work with migration warnings and auto-generated.newconfig files. - Shell integration warnings: Specific diagnostic messages when shell cd won't work: "shell integration not installed", "shell requires restart", "ran ./wt; shell integration wraps wt", or "ran git wt; running through git prevents cd".
- RUNTIME section in
wt config show: Displays binary name, version, and shell integration status to help debug invocation issues. - Clickable CI indicator: The CI status indicator (●) in
wt listoutput is now a clickable link to the PR in terminals that support OSC 8 hyperlinks. wt switchhelp text: Clarifies the difference fromgit switchand documents common failure conditions.
Fixed
- Hook path display: Hook announcements show the execution path when shell integration isn't active.
- Approval matching with deprecated vars: Approvals now match regardless of whether they were saved with deprecated or current variable names.
- Documentation filter syntax: Fixed incorrect Jinja filter examples that showed
~concatenation with|filter without parentheses. (#373, thanks @coriocactus)
Documentation
- Pre-remove hook example: Added pattern for cleaning up background processes (e.g., killing dev servers) when worktrees are removed.
0.8.5
Improved
- Windows
git-wtcommand: Winget now ships withgit-wtas a workaround to the Windows Terminalwtnaming conflict. We're still considering better options — see #133.
0.8.4
Improved
- Shell integration detection: More robust detection of
git wt(space) vsgit-wtpatterns.wt config shownow displays line numbers for detected shell integration. - Windows
wt selecterror: Shows a helpful error message with alternatives instead of "unrecognized subcommand".
Fixed
- Markdown table rendering: Escaped pipe characters (
\|) in help output now render correctly. - Dim styling on wrapped lines: Dim text attribute now preserved on continuation lines when text wraps.
- Path occupied hint: Fixed tilde expansion issue where
~/...paths didn't work in shell commands.
Documentation
- Hook design guide: Added comprehensive guide for designing hooks.
- Command docs: Added
wt config showto command documentation. - Windows paths: Documented MSYS2 auto path conversion for Windows shell integration.
Internal
- Output system: Consolidated output functions, removed redundant aliases.
- Zsh compinit: Improved handling of "insecure directories" warning in tests.
0.8.3
Improved
- Hook execution path: Shows the execution path when post-merge hooks run in a different directory than where the user invoked the command (e.g., with
--no-remove). - TTY check for
wt select: Now fails gracefully when run in a non-interactive terminal instead of hanging. - Background hooks:
post-startandpost-switchhooks spawn in background via stdin piping, matching their normal behavior duringwt switch. - Occupied path error message: When a worktree path is occupied by a different branch, the error now explains the situation clearly and suggests
git switch. - Shell integration hint: Shows a hint to restart the shell when shell integration is configured but not active.
- Message style: Removed 2nd person pronouns ("you/your") from user-facing messages following CLI guidelines.
Fixed
wt hook post-startblocking: Fixed bug wherewt hook post-startran in foreground blocking the command, instead of spawning in background like during normalwt switch --create.- Approval bypass with
project:prefix: Fixed security issue where usingproject:filter prefix (e.g.,wt hook pre-merge project:) bypassed the approval check, allowing unapproved project commands to run.
Documentation
- License file: Added combined MIT and Apache-2.0 license file.
- Demo GIFs: Added demo GIFs to command pages on the documentation site.
- Install instructions: Simplified to single-line commands.
Internal
- Pre-commit hooks: Updated to immutable tags.
- Lychee exclusions: Cleaned up link checker configuration.
0.8.2
Improved
- Concurrent hook execution:
wt hook post-startandwt hook post-switchnow run all commands concurrently (matching their normal background behavior) instead of sequentially with fail-fast. Multiple failures are collected and reported together.
Documentation
- Nested bare repo layout: Added worktree-path template example for nested bare repo layout (
project/.gitpattern). Uses relative paths like../{{ branch | sanitize }}to create worktrees as siblings to the .git directory.
0.8.1
Improved
- Shell and PowerShell installers: Added one-line install commands for Linux/macOS and Windows.
- Consistent terminology: CLI now uses "branch name" consistently instead of mixing "worktree" and "branch". The
wt removeargument is renamed fromworktreestobranchesto reflect that worktrees are addressed by branch name.
Fixed
- Switch hints: Removed incorrect
wt switch @hint and improved error output spacing.
Documentation
- Dev server and database patterns: Added practical examples for running per-worktree dev servers with subdomain routing and databases with unique ports.
0.8.0
Improved
- Separate
--yesand--forceflags:--force/-frenamed to--yes/-yfor skipping prompts (all commands). New--force/-fonwt removeforces removal of worktrees with untracked files (build artifacts, node_modules, etc.). (Breaking:--forceno longer skips prompts; use--yes) - Clearer branch deletion output:
wt removeoutput now shows "worktree & branch" when the branch is deleted, or plain "worktree" with a hint when kept. Makes scanning output for branch fate easier. post-switchhook on remove: Whenwt removeswitches to the main worktree, post-switch hooks now run in the destination.- Allow merge commits by default:
wt step pushno longer rejects history with merge commits. Removed--allow-merge-commitsflag. (Breaking: flag removed)
Fixed
- Orphan branches in
wt list: Branches with no common ancestor with the default branch no longer cause errors. - Remote branch filtering:
wt list --remotesnow filters out branches that are tracked as upstreams, not just branches with worktrees. - Error message spacing: Reduced double-newline spacing in error messages.
0.7.0
Improved
- Working tree conflict detection:
wt list --fullnow detects conflicts using uncommitted working tree changes, not just committed content. This catches conflicts earlier—before committing changes that would conflict with the target branch. - Dev server URL column: New optional URL column in
wt listconfigured via[list] urltemplate in project config (.config/wt.toml). URLs show with health-check styling: normal if the port is listening, dimmed otherwise. - Shell integration simplification: The shell wrapper is now self-contained with all directive handling inlined. Removes the separate helper function that could become unavailable if shell initialization order changed.
- Performance: Repository caching reduces git subprocess spawns; parallelized pre-skeleton operations for faster initial display.
- Improved error hints: When a worktree path already exists during creation, the error hint now correctly suggests
--create --clobber.
Fixed
- Docs syntax highlighting: Fixed syntax highlighting colors being stripped by 1Password browser extension on the documentation site.
0.6.1
Improved
post-switchhook: New hook that runs in the background after everywt switchoperation. Unlikepost-start(which only runs on creation),post-switchruns on all switch results. Use cases include renaming terminal tabs, updating tmux window names, and IDE notifications.- Signal forwarding for hooks: Hooks now receive SIGINT/SIGTERM when the parent process is interrupted, allowing proper cleanup. Previously, non-interactive shells continued executing after signals.
- Faster
wt listskeleton: Time-to-skeleton reduced by caching default branch lookup, batching timestamp fetching, and deferring non-essential git operations. Skeleton shows·placeholder for gutter symbols until data loads. - Clearer
--clobberhint: Error message now says "to overwrite (with backup)" instead of "to retry with backup".
Documentation
- State side-effects: Added section explaining how Worktrunk state operations may trigger git commands.
wt mergelocation: Clarified thatwt mergeruns from the feature worktree.
0.6.0
Improved
- Single-width Unicode symbols: Replaced emojis (🔄, ✅, ❌) with single-width Unicode symbols (◎, ✓, ✗, ▲, ↳, ○, ❯) for better terminal compatibility and consistent alignment.
- Output system overhaul: Clean separation of output channels (data→stdout, status→stderr, directives→file) means piping works with shell integration active.
wt list --format=json | jqandwt switch feature | tee log.txtboth work correctly. Background processes useprocess_group(0)instead ofnohupfor more reliable detachment. - Trailing arguments for
--execute:wt switch --executenow accepts arguments after--, enabling shell aliases likealias wsc='wt switch --create -x claude'thenwsc feature -- 'implement login'. hash_porttemplate filter:{{ branch | hash_port }}hashes the branch name to a deterministic port number (10000-19999), useful for running dev servers without port conflicts.sanitizetemplate filter:{{ branch | sanitize }}explicitly replaces/and\with-for filesystem-safe paths. (Breaking:{{ branch }}now provides raw branch names. Update templates that use{{ branch }}in filesystem paths to use{{ branch | sanitize }}instead)- Log directory in state output:
wt config state logsandwt config state getnow show the log directory path under a LOG FILES heading. - Actionable error hints: Error messages now include hints about what command to run next.
- Unified directory change output:
wt removenow shows "Switched to worktree for {branch} @ {path}" matchingwt switchformat. - Consistent "already up to date" formatting: Standardized message wording and styling across commands.
Fixed
wt step rebasewith merge commits: Fixed incorrect "Already up-to-date" when a branch has merge commits from merging target into itself.
Documentation
- Local CI workflow: Added "Local CI" section to
wt merge --helpexplaining how pre-merge hooks enable faster iteration. - Colored command reference: Web docs now preserve ANSI colors in command reference output.
- Clarified terminology: Help text uses "default branch" instead of hardcoded "main".
0.5.2
Improved
--clobberflag forwt switch: When encountering a stale directory or file at the target worktree path,--clobbermoves it to a timestamped.bakfile instead of failing.- Relative paths in
wt list: Paths are now shown relative to the main worktree (.,./subdir,../repo.feature) instead of a computed common prefix that could degenerate to/. - Multiline error formatting: Errors with context now show a header describing what worktrunk was trying to do, with the full error chain in a gutter block.
- Semantic switch messaging: Switching to an existing worktree now shows ⚪ (info) instead of ✅ (success), reflecting that nothing was created.
Fixed
- Symbol styling in removal messages: Integration symbols (
_,⊂) now render in their canonical dim appearance instead of inheriting the message's cyan color. - ConflictingChanges error formatting: Fixed double newlines in the error message output.
0.5.1
Improved
- Integration status in removal messages: Shows integration symbols (
_for same commit,⊂for integrated) when removing worktrees, matchingwt listdisplay. - Concurrent command limiting: Limits concurrent git processes to 32 (configurable via
WORKTRUNK_MAX_CONCURRENT_COMMANDS), preventing resource exhaustion on repos with many branches. - Better error display for
wt list: Task errors are now collected and displayed as warnings after the table renders, instead of being silently swallowed. - Remove continues on partial failures:
wt removecontinues removing other worktrees when some fail, reporting all errors at the end. - Bash syntax highlighting: Shell commands in error gutters now have syntax highlighting.
- Shell integration is command-aware: Detection and removal works correctly when installed as
git-wtor other names. - CI fetch error documentation: Yellow warning symbol (⚠) in CI column is now documented in help text.
Fixed
- CI status with multiple workflows: Fixed incorrect status when multiple workflows exist (e.g.,
ciandpublish-docs). Now uses GitHub's check-runs API to aggregate all workflow statuses. - State storage unification: Unified branch-keyed state under
worktrunk.state.<branch>.*. Numeric branch names now work. (Existing CI cache and markers regenerate on first access)
Internal
- Environment variable prefix: Standardized to
WORKTRUNK_prefix (e.g.,WORKTRUNK_MAX_CONCURRENT_COMMANDS). - Automatic winget package publishing on releases.
0.5.0
Improved
- Path column hidden when redundant: Path column is deprioritized when all paths match the naming template, showing only at wider terminal widths (~125+ columns).
- Better error formatting: Errors with context now show a header with the root cause in a gutter block, improving readability for git errors.
- Clearer integration target: Separated
default_branch(for stats like ahead/behind) fromtarget(for integration checks), catching branches merged remotely before pulling.
Fixed
- Untracked files block integration: Untracked files now prevent a worktree from being flagged as integrated, avoiding accidental data loss on removal.
- Dirty worktree count includes untracked: Summary now correctly counts worktrees with untracked files as dirty.
- Branch name disambiguation: Fixed
refname:shortissues when a branch and remote have the same name. - JSON output uses kebab-case: Enum values changed from snake_case to kebab-case (e.g.,
same_commit→same-commit). (Breaking: scripts parsing JSON output may need updates) - Legacy marker format removed: Plain-text markers no longer parsed. (Breaking: re-set markers with
wt config state marker set)
Internal
- Unified command execution: All external commands now go through
shell_exec::run()for consistent logging and tracing.
0.4.0
Added
--no-rebaseflag forwt merge: Fails early with a clear error if the branch is not already rebased onto target, rather than auto-rebasing. Useful for workflows that handle rebasing separately. (#194)
Changed
- Branch-first argument resolution:
wt switchandwt removenow check if the branch has a worktree anywhere before checking the expected path. If you typewt switch foo, you get branch foo's worktree, not whatever happens to be at the expected path. (#197)
Fixed
--no-commitincorrectly skipped rebasing:wt merge --no-commitnow correctly rebases before stopping (if needed), rather than skipping the rebase entirely. (#194)- Pager for
wt config show --full: The pager now works correctly with the--fullflag, showing diagnostics properly. (#198) - Statusline stdin handling: Fixed flaky behavior on Windows CI by using standard is_terminal() check instead of timeout-based approach. (#210)
Improved
- Path-occupied error messages: When
wt switchcan't create a worktree because the path exists, error messages now show which branch occupies the path and provide actionable commands to fix the situation. (#195, #206, #207) - Switch mismatch detection: Better error messages when path/branch mismatches occur, with hints showing the expected path. (#195)
0.3.1
Fixed
- Branch names with slashes: Branch names like
fix/feature-nameno longer break git config markers. Slashes are now escaped for git config compatibility. (#189, thanks @kyleacmooney) - stdin inheritance for
--execute: Interactive programs (vim, python -i, claude) now work correctly with--executeon non-Unix platforms. (#191) - Filenames with spaces/newlines: Git status parsing now handles filenames containing spaces and newlines correctly using NUL-separated output.
- Concurrent approval race condition: Multiple concurrent approval/revocation operations no longer overwrite each other. Approvals now reload from disk before saving.
- Dirty worktrees incorrectly marked integrated: Priority 5 integration check now requires clean working tree state, preventing worktrees with uncommitted changes from being flagged as safe to remove.
- Type changes not detected as staged: Index status check now recognizes file type changes (
Tstatus) as staged changes. - User hook failure strategy: Hook failure strategy now correctly applies to user hooks instead of always using fail-fast.
- Branch variable in detached HEAD:
{{ branch }}now correctly expands to "HEAD" in detached HEAD worktrees instead of "(detached)".
Improved
- Self-hosted GitLab support: CI auth checks now detect the GitLab host from the remote URL, supporting self-hosted GitLab instances instead of always checking gitlab.com.
- Platform-specific CI status:
wt list --fullandwt config shownow show only the relevant CI tool (GitHub Actions or GitLab CI) based on the repository's remote URL. - LLM error reproduction: When LLM commands fail, error messages now show the full reproduction command (e.g.,
wt step commit --show-prompt | llm) for easier debugging. - Location format: Messages now use
@instead ofatfor location phrases (e.g., "Switched to feature @ /path"). - Switch help text: Clarified that
wt switchcreates worktrees automatically for existing branches, not just for new branches with--create.
0.3.0
Added
--show-promptflag for LLM commands:wt step commit --show-promptandwt step squash --show-promptoutput the rendered LLM prompt without executing the command. Useful for debugging templates or manually piping to LLM tools. (#187)- Diff size limits and diffstat for LLM prompts: Large diffs (>400K chars) are progressively filtered—first removing lock files, then truncating to 50 lines/file, max 50 files. New
git_diff_stattemplate variable shows line change statistics. (#186) MainState::Emptystatus: New_symbol for clean same-commit branches (safe to delete), distinguished from–(en-dash) for same-commit branches with uncommitted changes. Previously, both showed_. Only Empty branches are dimmed and considered "potentially removable". (#185)
Changed
- State subcommands default to
get: Runningwt config state default-branchnow defaults toget, making the command shorter. Use explicitgetsubcommand to access options like--refreshor--branch. (#184) - Clearer integration reason messages: Updated descriptions to be more precise—"same commit as" instead of "already in" for SameCommit, "ancestor of" for Ancestor, "no added changes" for NoAddedChanges, "tree matches" for TreesMatch.
0.2.1
Changed
- Unified state management:
wt config varandwt config cachereplaced bywt config statewith consistent get/set/clear semantics for all runtime state. New subcommands:default-branch,ci-status,marker,logs,show. (#178) - Comprehensive state overview:
wt config state showdisplays all state (default branch, switch history, markers, CI cache, logs) with--format=jsonsupport. (#180)
Added
git-wtbinary for Windows: Newgit-wtbinary avoids conflict with Windows Terminal'swtcommand. Build with--features git-wt. Shell init/install now accept--cmdto specify which binary name to use. (#177)- Diffstat in select preview: The log preview (Tab 2) in
wt selectnow shows line change statistics (+N -M) matchingwt list's HEAD± column format. (#179)
Fixed
- Windows compatibility: Multiple test and runtime fixes for Windows including stdin timeout handling, path canonicalization, and cross-platform test behavior. (#167, #168, #169, #170, #171, #174, #176)
0.1.21
Fixed
- Windows path handling in shell templates: Fixed path quoting in hook templates on Windows by using
cygpathto convert native Windows paths to POSIX format for Git Bash compatibility. Template variables like{{ worktree }}and{{ repo_root }}now work correctly. (#161) - Hook errors show
--no-verifyhint: When hooks fail duringwt merge,wt commit, orwt squash, the error message now includes a hint about using--no-verifyto skip hooks. (4a89748)
0.1.20
Changed
--doctorrenamed to--full: Thewt list --doctorflag is nowwt list --full. The new name better reflects that it shows extended information (binaries status, full diff stats). (171952e)- CLI binaries status in
wt config show --full: Shows installation and authentication status ofghandglabCLI tools in a new BINARIES section. (171952e) - CI tool hints:
wt list --fullshows a hint when CI status is unavailable, with specific guidance on which CLI tool to install or authenticate. (171952e)
Fixed
- GitHub StatusContext checks: CI status now includes StatusContext checks (used by some CI systems like Jenkins, CircleCI, and external status checks) in addition to CheckRuns. (690da88)
- Windows Git Bash detection with WSL: Fixed detection of Git Bash when WSL is installed. Previously, the WSL bash shim in PATH could be found instead of Git Bash, causing hook execution failures. (b48b0ba)
0.1.19
Added
wt step for-eachcommand: Run commands across all worktrees sequentially. Supports template variables ({{ branch }},{{ worktree }}, etc.) and JSON context on stdin. Example:wt step for-each -- git pull --autostash. (#138)
Changed
- Content integration detection always enabled: The
⊂(content integrated) symbol now appears without requiring--full. Squash-merged branches are detected automatically. (f39c442) - SIGINT forwarding: Ctrl+C now properly terminates child processes in hooks, preventing orphaned background commands. (#136)
Fixed
- Windows path handling: Fixed path canonicalization issues on Windows that caused worktree detection failures. Uses
dunceto handle Windows verbatim paths (\\?\) that git cannot process. (#125)
0.1.18
Added
- Windows support: Git Bash with PowerShell fallback enables worktrunk on Windows. Git Bash is preferred (same bash hook syntax across platforms); PowerShell works for basic commands with limitations. (#122)
- Winget publishing: Release workflow now publishes to Windows Package Manager. (079c9df)
Changed
- Approvals command moved:
wt config approvalsis nowwt hook approvalssince approvals manage hook commands. (b7b1b9e) - Approval prompts show templates: Approval prompts now display command templates (what gets saved) rather than expanded values. (2315d26)
- Preview mode renamed: The
historypreview mode is nowlogfor clarity. (0461152)
Fixed
- PR/MR source filtering: Filter PRs by source repository instead of author, fixing false matches when multiple users have PRs with the same branch name. (e9ccdf7)
0.1.17
Added
- User-level hooks: Define hooks in
~/.config/wt.tomlthat run for all repositories. Newwt hook showcommand displays configured hooks and their sources. (#118) - SSH URL support: Git SSH URLs (e.g.,
git@github.com:user/repo.git) now work correctly for remote operations and branch name escaping. (92c2cef) - Help text wrapping: CLI help text now wraps to terminal width for better readability. (fe981c2)
Changed
- JSON output redesign:
wt list --format=jsonnow outputs a query-friendly format. This is a breaking change for existing JSON consumers. (236eae8) - Status symbols: Reorganized status column symbols for better scannability. Same-commit now distinguished from ancestor in integration detection. (5053af8, a087962)
Fixed
- ANSI state reset: Reset terminal ANSI state before returning to shell, preventing color bleeding into subsequent commands. (334f6d9)
- Empty staging error: Fail early with a clear error when trying to generate a commit message with nothing staged. (b9522bc)
0.1.16
Added
- Squash-merge integration detection: Improved branch cleanup detection with four ordered checks to identify when branch content is already in the target branch. This enables accurate removal of squash-merged branches even after target advances. New status symbols:
·for same commit,⊂for content integrated via different history. (6325be2) - CI absence caching: Cache "no CI found" results to avoid repeated API calls for branches without CI configured. Reduces unnecessary rate limit consumption. (8db3928)
- Shell completion tests: Black-box snapshot tests for zsh, bash, and fish completions that verify actual completion output. (#117)
Changed
- Merge conflict indicator: Changed from
⊘to⚔(crossed swords) for better visual distinction from the rebase symbol. (f3b96a8)
Documentation
- Hook JSON context: Document all JSON fields available to hooks on stdin with examples for Python and other languages. (af80589)
- CI caching: Document that CI results are cached for 30-60 seconds and how to use
wt config cacheto manage the cache. (4804913) - Status column clarifications: Clarify that the Status column contains multiple subcolumns with priority ordering. (1f9bb38)
0.1.15
Added
wt hookcommand: New command for running lifecycle hooks directly. Moved hook execution fromwt steptowt hookfor cleaner semantic separation. (#113)- Named hook execution: Run specific named commands with
wt hook <type> <name>(e.g.,wt hook pre-merge test). Includes shell completion for hook names from project config. (#114)
Fixed
- Zsh completion syntax: Fixed
_describesyntax in zsh shell completions. (6ae9d0f) - Fish shell wrapper: Fixed stderr redirection in fish shell wrapper. (0301d4b)
- CI status for local branches: Only check CI for branches with upstream tracking configured. (6273ccd)
- Git error messages: Include executed git command in error messages for easier debugging. (200eea4)
0.1.14
Added
- Pre-remove hook: New
pre-removehook runs before worktree removal, enabling cleanup tasks like stopping devcontainers. Thanks to @pwntester in #101. (#107) - JSON context on stdin: Hooks now receive worktree context as JSON on stdin, enabling hooks in any language (Python, Node, Ruby, etc.) to access repo information. (#109)
wt config create --project: New flag to generate.config/wt.tomlproject config files directly. (#110)
Fixed
- Shell completion bypass: Fixed lazy shell completion to use
commandbuiltin, bypassing the shell function that was causing_clap_dynamic_completer_wterrors. Thanks to @cquiroz in #102. (#105) - Remote-only branch completions:
wt removecompletions now exclude remote-only branches (which can't be removed) and show a helpful error with hint to usewt switch. (#108) - Detached HEAD hooks: Pre-remove hooks now work correctly on detached HEAD worktrees. (#111)
- Hook
{{ target }}variable: Fixed template variable expansion in standalone hook execution. (#106)