Release Notes
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--branch=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)
Install worktrunk 0.37.0
Install prebuilt binaries via shell script
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/max-sixty/worktrunk/releases/download/v0.37.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.37.0/worktrunk-installer.ps1 | iex"; git-wt config shell installInstall prebuilt binaries via Homebrew
brew install worktrunk && wt config shell installDownload worktrunk 0.37.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