github max-sixty/worktrunk v0.53.0
0.53.0

8 hours ago

Release Notes

Improved

  • wt switch --execute deprecates 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: -x now 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 show reports 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=json gains a matching identifier field. Closes #2826. (#2827, thanks @airtonix for the request)

  • Gemini CLI extension detection: wt config show now 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-create hook aliases: The worktree-creation hooks pre-start/post-start now also accept pre-create/post-create as silent aliases — in config (top-level, [hooks.*], and per-project sections, in string, table, and array-of-tables form) and on the wt hook command line. Docs continue to recommend pre-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.toml from a different worktree depending on the hook, and wt switch --create read the base ref's committed config via git show — so an uncommitted or branch-local .config/wt.toml silently failed to fire creation hooks, and wt config show disagreed with what actually ran. Every hook now resolves its commands from the .config/wt.toml of the worktree wt ran in — the same file wt config show displays. 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-switch hooks: Selecting a worktree in the interactive picker (wt switch with no argument) ran a project-defined pre-switch hook from .config/wt.toml without the approval prompt that gates every other hook — unapproved code from a freshly cloned repo executing silently. The picker now routes pre-switch hooks through the same approval gate as wt switch <branch> and as its own post-switch/pre-start/post-start hooks. (#2858)

  • Interactive picker switches with cd = false: With [switch] cd = false (or wt switch --no-cd), opening the picker (wt switch with no branch argument) and selecting a worktree printed the branch name and exited — no switch, no hooks, and Alt-c created nothing. The picker now runs the same switch pipeline as wt switch <branch>, suppressing only the cd directive: pre-switch/post-switch hooks fire and Alt-c creates the worktree. --format=json works 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-r in the picker removes the right worktree: The interactive picker identified each row by branch name for its alt-r removal signal; detached worktrees all report (detached), so two detached rows collided and alt-r could remove the wrong worktree. Rows backed by a worktree now carry a unique path-based identity. (#2866)

  • --clobber backs up blocked paths atomically: wt switch --clobber and wt step relocate --clobber back up a path blocking the target before clobbering it. Both used an exists() check followed by std::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 compared git patch-id hashes computed from two different diff generators — one plumbing (ignores diff.* config), one porcelain (honors it). For anyone with a non-default diff.context or diff.algorithm, the two never agreed, so a genuinely squash-merged branch was reported as not integrated — breaking wt remove ("Branch unmerged"), the wt list integration symbol, and wt step prune. Both sides now use plumbing, immune to every diff.* setting. (#2821)

  • Wedged and orphaned fsmonitor daemons are reaped: With core.fsmonitor=true, git runs a per-worktree git fsmonitor--daemon; a wedged one stops answering its IPC socket — which hangs git status and wt list — and ignores the stop request wt remove sends, so it leaks once its worktree is gone (dozens can accumulate). wt remove now resolves the daemon's PID from its IPC socket and force-terminates it (SIGTERM, brief wait, SIGKILL) when stop doesn't take, and its background internal sweep additionally reaps any daemon whose socket no longer resolves to a live worktree — covering daemons orphaned by git worktree remove, a manual rm, or a crashed wt. 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:lint matched 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)

  • wt no longer panics on non-UTF-8 arguments, and --format passed 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 each alt-r removal against fresh repository state rather than a cache left stale by the previous removal. wt switch prefers an exact local branch over stripping a remote prefix (a local branch literally named origin/foo was retargeted), and fails closed on a malformed forge.platform instead 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 --execute payload: wt switch -x builds 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 other shell_escape call 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 show lists per-project user hooks: wt hook show displayed 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 statusline runs 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 relocate summary 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, which wt config state then misclassified as a branch; they now write to internal-{op}.log files alongside the other shared logs. (#2851)

  • Nix flake includes gemini-extension.json: The flake's source filter omitted gemini-extension.json, so a Nix build produced a package missing the Gemini CLI extension manifest. (#2834)

Documentation

  • wt remove --force help and FAQ: Both said --force overrides the untracked-files check "for build artifacts"; --force actually discards staged and modified tracked files too. The help text and FAQ now state that --force discards 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 install

Install 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 install

Install prebuilt binaries via Homebrew

brew install worktrunk && wt config shell install

Download 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 install

Install via Winget (Windows)

winget install max-sixty.worktrunk && git-wt config shell install

Install via AUR (Arch Linux)

paru worktrunk-bin && wt config shell install

Don't miss a new worktrunk release

NewReleases is sending notifications on new releases.