Release Notes
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)
Install worktrunk 0.53.0
Install prebuilt binaries via shell script
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/max-sixty/worktrunk/releases/download/v0.53.0/worktrunk-installer.sh | sh && wt config shell installInstall prebuilt binaries via powershell script
powershell -ExecutionPolicy Bypass -c "irm https://github.com/max-sixty/worktrunk/releases/download/v0.53.0/worktrunk-installer.ps1 | iex"; git-wt config shell installInstall prebuilt binaries via Homebrew
brew install worktrunk && wt config shell installDownload worktrunk 0.53.0
| File | Platform | Checksum |
|---|---|---|
| worktrunk-aarch64-apple-darwin.tar.xz | Apple Silicon macOS | checksum |
| worktrunk-x86_64-apple-darwin.tar.xz | Intel macOS | checksum |
| worktrunk-x86_64-pc-windows-msvc.zip | x64 Windows | checksum |
| worktrunk-aarch64-unknown-linux-musl.tar.xz | ARM64 MUSL Linux | checksum |
| worktrunk-x86_64-unknown-linux-musl.tar.xz | x64 MUSL Linux | checksum |
Install via Cargo
cargo install worktrunk && wt config shell installInstall via Winget (Windows)
winget install max-sixty.worktrunk && git-wt config shell installInstall via AUR (Arch Linux)
paru worktrunk-bin && wt config shell install