v0.53.0

max-sixty/worktrunkv0.53.0May 22, 2026by github-actions[bot]

Upgrading?

AI Summary

This release deprecates shell command lines in `wt switch --execute` and adds project identifier reporting to `wt config show`. It includes numerous fixes for hook approval, interactive picker behavior, and data safety across the worktree lifecycle.

Key Highlights

  • `wt switch --execute` deprecates shell command lines (warn phase)
  • `wt config show` reports the project identifier
  • Gemini CLI extension detection
  • Hooks now resolve project config from the invoking worktree
  • Picker now correctly handles `cd = false` and unique worktree identification
  • `--clobber` backs up blocked paths atomically

New Features

  • Project identifier reporting
  • Gemini CLI extension detection

Full Release Notes

## 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](https://github.com/max-sixty/worktrunk/pull/2852), [#2863](https://github.com/max-sixty/worktrunk/pull/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](https://github.com/max-sixty/worktrunk/issues/2826). ([#2827](https://github.com/max-sixty/worktrunk/pull/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](https://github.com/max-sixty/worktrunk/pull/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](https://github.com/max-sixty/worktrunk/issues/2838). ([#2840](https://github.com/max-sixty/worktrunk/pull/2840), [#2857](https://github.com/max-sixty/worktrunk/pull/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](https://github.com/max-sixty/worktrunk/issues/2856) and [#2818](https://github.com/max-sixty/worktrunk/issues/2818). ([#2873](https://github.com/max-sixty/worktrunk/pull/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](https://github.com/max-sixty/worktrunk/pull/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](https://github.com/max-sixty/worktrunk/pull/2845), thanks @endigma for the discussion in [#2837](https://github.com/max-sixty/worktrunk/issues/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](https://github.com/max-sixty/worktrunk/pull/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](https://github.com/max-sixty/worktrunk/pull/2849), [#2865](https://github.com/max-sixty/worktrunk/pull/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](https://github.com/max-sixty/worktrunk/pull/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](https://github.com/max-sixty/worktrunk/pull/2813), [#2814](https://github.com/max-sixty/worktrunk/pull/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](https://github.com/max-sixty/worktrunk/pull/2788), [#2851](https://github.com/max-sixty/worktrunk/pull/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](https://github.com/max-sixty/worktrunk/pull/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](https://github.com/max-sixty/worktrunk/pull/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](https://github.com/max-sixty/worktrunk/pull/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](https://github.com/max-sixty/worktrunk/pull/2843), [#2815](https://github.com/max-sixty/worktrunk/pull/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](https://github.com/max-sixty/worktrunk/pull/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](https://github.com/max-sixty/worktrunk/pull/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](https://github.com/max-sixty/worktrunk/pull/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](https://github.com/max-sixty/worktrunk/pull/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](https://github.com/max-sixty/worktrunk/pull/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](https://github.com/max-sixty/worktrunk/pull/2869))

- **cmux recipe**: Re-added a verified cmux integration recipe to Tips & Patterns. ([#2836](https://github.com/max-sixty/worktrunk/pull/2836), thanks @endigma for the verified config)

## Install worktrunk 0.53.0

### Install prebuilt binaries via shell script

```sh
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

```sh
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

```sh
brew install worktrunk && wt config shell install
```

## Download worktrunk 0.53.0

|  File  | Platform | Checksum |
|--------|----------|----------|
| [worktrunk-aarch64-apple-darwin.tar.xz](https://github.com/max-sixty/worktrunk/releases/download/v0.53.0/worktrunk-aarch64-apple-darwin.tar.xz) | Apple Silicon macOS | [checksum](https://github.com/max-sixty/worktrunk/releases/download/v0.53.0/worktrunk-aarch64-apple-darwin.tar.xz.sha256) |
| [worktrunk-x86_64-apple-darwin.tar.xz](https://github.com/max-sixty/worktrunk/releases/download/v0.53.0/worktrunk-x86_64-apple-darwin.tar.xz) | Intel macOS | [checksum](https://github.com/max-sixty/worktrunk/releases/download/v0.53.0/worktrunk-x86_64-apple-darwin.tar.xz.sha256) |
| [worktrunk-x86_64-pc-windows-msvc.zip](https://github.com/max-sixty/worktrunk/releases/download/v0.53.0/worktrunk-x86_64-pc-windows-msvc.zip) | x64 Windows | [checksum](https://github.com/max-sixty/worktrunk/releases/download/v0.53.0/worktrunk-x86_64-pc-windows-msvc.zip.sha256) |
| [worktrunk-aarch64-unknown-linux-musl.tar.xz](https://github.com/max-sixty/worktrunk/releases/download/v0.53.0/worktrunk-aarch64-unknown-linux-musl.tar.xz) | ARM64 MUSL Linux | [checksum](https://github.com/max-sixty/worktrunk/releases/download/v0.53.0/worktrunk-aarch64-unknown-linux-musl.tar.xz.sha256) |
| [worktrunk-x86_64-unknown-linux-musl.tar.xz](https://github.com/max-sixty/worktrunk/releases/download/v0.53.0/worktrunk-x86_64-unknown-linux-musl.tar.xz) | x64 MUSL Linux | [checksum](https://github.com/max-sixty/worktrunk/releases/download/v0.53.0/worktrunk-x86_64-unknown-linux-musl.tar.xz.sha256) |



### Install via Cargo

```sh
cargo install worktrunk && wt config shell install
```

### Install via Winget (Windows)

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

### Install via AUR (Arch Linux)

```sh
paru worktrunk-bin && wt config shell install
```

Related Repositories