## File: README.md

  Worktrunk

[](https://worktrunk.dev) [](https://opensource.org/licenses/MIT) [](https://github.com/max-sixty/worktrunk/actions?query=branch%3Amain+workflow%3Aci) [](https://codecov.io/gh/max-sixty/worktrunk) [](https://github.com/max-sixty/worktrunk/stargazers) [](https://github.com/max-sixty/tend) > **August 2026**: Worktrunk was [released](https://x.com/max_sixty/status/2006077845391724739?s=20) at the start of the year, and has quickly become the most popular git worktree manager. It's built with love (there's no slop!). Please let me know any frictions at all; I'm intensely focused on continuing to make Worktrunk excellent, and the biggest help is folks posting problems they perceive. Worktrunk is a CLI for git worktree management, designed for running AI agents in parallel. Worktrunk's three core commands make worktrees as easy as branches. Plus, Worktrunk has a bunch of quality-of-life features to simplify working with many parallel changes, including hooks to automate local workflows. A quick demo: > ### πŸ“š Full documentation at [worktrunk.dev](https://worktrunk.dev) πŸ“š ## Context: git worktrees AI agents like Claude Code and Codex can handle longer tasks without supervision, such that it's possible to manage 5-10+ in parallel. Git's native worktree feature give each agent its own working directory, so they don't step on each other's changes. But the git worktree UX is clunky. Even a task as small as starting a new worktree requires typing the branch name three times: `git worktree add -b feat ../repo.feat`, then `cd ../repo.feat`. ## Worktrunk makes git worktrees as easy as branches Worktrees are addressed by branch name; paths are computed from a configurable template. Commands that take a branch also accept the path of the worktree it is checked out in. > Start with the core commands **Core commands:** | Task | Worktrunk | Plain git | | --- | --- | --- | | Switch worktrees | wt switch feat | cd ../repo.feat | | Create + start Claude | wt switch -c -x claude feat | git worktree add -b feat ../repo.feat && \ cd ../repo.feat && \ claude | | Clean up | wt remove | cd ../repo && \ git worktree remove ../repo.feat && \ git branch -d feat | | List with status | wt list | git worktree list (paths only) | > Expand into the more advanced commands as needed **Workflow automation:** - **[Hooks](https://worktrunk.dev/hook/)** β€” run commands on create, pre-merge, post-merge, etc - **[LLM commit messages](https://worktrunk.dev/llm-commits/)** β€” generate commit messages from diffs - **[Merge workflow](https://worktrunk.dev/merge/)** β€” squash, rebase, merge, clean up in one command - **[Interactive picker](https://worktrunk.dev/switch/#interactive-picker)** β€” browse worktrees with live diff and log previews - **[Copy build caches](https://worktrunk.dev/step/#wt-step-copy-ignored)** β€” skip cold starts by sharing `target/`, `node_modules/`, etc between worktrees - **[`wt list --full`](https://worktrunk.dev/list/#full-mode)** β€” [CI status](https://worktrunk.dev/list/#ci-status) and [AI-generated summaries](https://worktrunk.dev/list/#llm-summaries) per branch - **[PR checkout](https://worktrunk.dev/switch/#pull-requests-and-merge-requests)** β€” `wt switch pr:123` to jump straight to a PR's branch - **[Dev server per worktree](https://worktrunk.dev/tips-patterns/#dev-server-per-worktree)** β€” `hash_port` template filter gives each worktree a unique port - **[Aliases](https://worktrunk.dev/extending/#aliases) & [per-branch variables](https://worktrunk.dev/config/#wt-config-state-vars)** β€” custom `wt ` commands and branch-scoped state for hook templates - ...and **[lots more](#next-steps)** Multiple parallel agents, same simple commands: ## Install **Homebrew (macOS & Linux):** ```bash brew install worktrunk && wt config shell install ``` Shell integration allows commands to change directories. **Cargo:** ```bash cargo install worktrunk && wt config shell install ``` **Windows & other** **Windows.** `wt` defaults to Windows Terminal's command, so Winget additionally installs Worktrunk as `git-wt` to avoid the conflict: ```bash winget install max-sixty.worktrunk git-wt config shell install ``` Alternatively, disable Windows Terminal's alias (Settings β†’ Apps β†’ Advanced app settings β†’ App execution aliases β†’ "Terminal"/"Terminal Preview") to use `wt` directly. > Free code signing provided by [SignPath.io](https://signpath.io/), certificate by [SignPath Foundation](https://signpath.org/) β€” [policy](https://worktrunk.dev/code-signing/). **Arch Linux:** ```bash sudo pacman -S worktrunk && wt config shell install ``` **Conda / Pixi** (community-maintained [feedstock](https://github.com/conda-forge/worktrunk-feedstock)): ```bash conda install -c conda-forge worktrunk && wt config shell install ``` Or with [Pixi](https://pixi.sh): `pixi global install worktrunk && wt config shell install`. ## Quick start Create a worktree for a new feature: ```console $ wt switch --create feature-auth βœ“ Created branch feature-auth from main and worktree @ ~/repo.feature-auth ``` This creates a new branch and worktree, then switches to it. Do your work, then check all worktrees with [`wt list`](https://worktrunk.dev/list/): ```console $ wt list Branch Status HEADΒ± main↕ main…± Remoteβ‡… Commit Age Message @ feature-auth + ↑ +27 -8 ↑1 +31 4bc72dc 2h Add authenticati… ^ main ^⇑ ⇑1 0e631ad 1d Initial commit β—‹ Showing 2 worktrees, 1 with changes, 1 ahead, 1 column hidden ``` The `@` marks the current worktree. `+` means staged changes, `↑1` means 1 commit ahead of main, `⇑` means unpushed commits. When done, either: **PR workflow** β€” commit, push, open a PR, merge via GitHub/GitLab, then clean up: ```bash wt step commit # commit staged changes gh pr create # or glab mr create wt remove # after PR is merged ``` **Local merge** β€” squash, rebase onto main, fast-forward merge, clean up: ```console $ wt merge main β—Ž Generating commit message and committing changes... (2 files, +53, no squashing needed) Add authentication module βœ“ Committed changes @ a1b2c3d β—Ž Merging 1 commit to main @ a1b2c3d (no rebase needed) * a1b2c3d Add authentication module auth.rs | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ lib.rs | 2 ++ 2 files changed, 53 insertions(+) βœ“ Merged to main (1 commit, 2 files, +53) β—Ž Removing feature-auth worktree & branch in background (same commit as main, _) β—‹ Switched to worktree for main @ ~/repo ``` For parallel agents, create multiple worktrees and launch an agent in each: ```bash wt switch -x claude -c feature-a -- 'Add user authentication' wt switch -x claude -c feature-b -- 'Fix the pagination bug' wt switch -x claude -c feature-c -- 'Write tests for the API' ``` The `-x` flag runs a command after switching; arguments after `--` are passed to it. Configure [post-start hooks](https://worktrunk.dev/hook/#hook-types) to automate setup (install deps, start dev servers). ## Next steps - Learn the core commands: [`wt switch`](https://worktrunk.dev/switch/), [`wt list`](https://worktrunk.dev/list/), [`wt merge`](https://worktrunk.dev/merge/), [`wt remove`](https://worktrunk.dev/remove/) - Set up [hooks](https://worktrunk.dev/hook/) for automated setup - Explore [LLM commit messages](https://worktrunk.dev/llm-commits/), [interactive picker](https://worktrunk.dev/switch/#interactive-picker), [Claude Code integration](https://worktrunk.dev/claude-code/), [CI status & PR links](https://worktrunk.dev/list/#ci-status) - Browse [tips & patterns](https://worktrunk.dev/tips-patterns/) for recipes: aliases, dev servers, databases, agent handoffs, and more - [Extending Worktrunk](https://worktrunk.dev/extending/) β€” customize workflows with hooks & aliases - Run `wt --help` or `wt --help` for quick CLI reference ## Further reading - [Claude Code: Best practices for agentic coding](https://www.anthropic.com/engineering/claude-code-best-practices) β€” Anthropic's official guide, including the worktree pattern - [Shipping faster with Claude Code and Git Worktrees](https://incident.io/blog/shipping-faster-with-claude-code-and-git-worktrees) β€” incident.io's workflow for parallel agents - [Git worktree pattern discussion](https://github.com/anthropics/claude-code/issues/1052) β€” Community discussion in the Claude Code repo - [@DevOpsToolbox's video on Worktrunk](https://youtu.be/WBQiqr6LevQ?t=345) - [git-worktree documentation](https://git-scm.com/docs/git-worktree) β€” Official git reference ## Contributing - ⭐ Star the repo - Tell a friend about Worktrunk - [Open an issue](https://github.com/max-sixty/worktrunk/issues/new?title=&body=%23%23%20Description%0A%0A%3C!--%20Describe%20the%20bug%20or%20feature%20request%20--%3E%0A%0A%23%23%20Context%0A%0A%3C!--%20Any%20relevant%20context%3A%20your%20workflow%2C%20what%20you%20were%20trying%20to%20do%2C%20etc.%20--%3E) β€” feedback, feature requests, even a small friction or imperfect user message, or [a worktree pain not yet solved](https://github.com/max-sixty/worktrunk/issues/new?title=Worktree%20friction%3A%20&body=%23%23%20The%20friction%0A%0A%3C!--%20What%20worktree-related%20task%20is%20still%20painful%3F%20--%3E%0A%0A%23%23%20Current%20workaround%0A%0A%3C!--%20How%20do%20you%20handle%20this%20today%3F%20--%3E%0A%0A%23%23%20Ideal%20solution%0A%0A%3C!--%20What%20would%20make%20this%20easier%3F%20--%3E) - Share: [X](https://twitter.com/intent/tweet?text=Worktrunk%20%E2%80%94%20CLI%20for%20git%20worktree%20management&url=https%3A%2F%2Fworktrunk.dev) Β· [Reddit](https://www.reddit.com/submit?url=https%3A%2F%2Fworktrunk.dev&title=Worktrunk%20%E2%80%94%20CLI%20for%20git%20worktree%20management) Β· [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fworktrunk.dev) > ### πŸ“š Full documentation at [worktrunk.dev](https://worktrunk.dev) πŸ“š ### Star history --- ## File: docs/content/claude-code.md +++ title = "Agent Integration" description = "Worktrunk plugins for Claude Code, Codex, OpenCode, and Gemini CLI: a configuration skill, wt list activity tracking, and Claude-only worktree isolation." weight = 23 [extra] group = "Reference" +++ Worktrunk ships a plugin for each supported agent CLI. What a plugin provides depends on the hooks that CLI exposes: | Capability | Claude Code | Codex | OpenCode | Gemini CLI | |---|:-:|:-:|:-:|:-:| | Configuration skill | βœ“ | βœ“ | | βœ“ | | Activity tracking (πŸ€–/πŸ’¬ in `wt list`) | βœ“ | βœ“ | βœ“ | βœ“ | | Worktree isolation | βœ“ | | | | | `/wt-switch-create` command | βœ“ | | | | The configuration skill is documentation the agent reads to help set up LLM commits, hooks, and troubleshooting. Activity tracking shows which worktrees have running sessions. Worktree isolation needs worktree-lifecycle hooks and `/wt-switch-create` needs session working-directory switching β€” both Claude Code-only, so Codex, OpenCode, and Gemini users invoke `wt switch --create` and `wt remove` directly. Codex tracks activity through its own `Stop` and `SessionEnd` hooks. ## Installation ### Claude Code {{ terminal(cmd="wt config plugins claude install") }} Manual equivalent: {{ terminal(cmd="claude plugin marketplace add max-sixty/worktrunk|||claude plugin install worktrunk@worktrunk") }} ### Codex {{ terminal(cmd="wt config plugins codex install") }} This configures the Worktrunk marketplace in Codex. Then run `/plugins` in Codex and install Worktrunk from the marketplace. Manual equivalent: {{ terminal(cmd="codex plugin marketplace add max-sixty/worktrunk") }} To remove the marketplace entry, run `wt config plugins codex uninstall`. Already-installed plugins are left unchanged. ### OpenCode {{ terminal(cmd="wt config plugins opencode install") }} This writes the activity-tracking plugin to OpenCode's global plugins directory, `~/.config/opencode/plugins/worktrunk.ts` (honoring `$OPENCODE_CONFIG_DIR` and `$XDG_CONFIG_HOME`). `wt config plugins opencode uninstall` removes it. ### Gemini CLI {{ terminal(cmd="gemini extensions install https://github.com/max-sixty/worktrunk") }} Gemini loads the extension natively from the repository, so there is no `wt` wrapper. `gemini extensions uninstall worktrunk` removes it. ## Configuration skill With the `/worktrunk` skill, the agent can help with: - Setting up LLM-generated commit messages - Adding project hooks (pre-start, pre-merge, pre-commit) - Configuring worktree path templates - Fixing shell integration issues Claude Code is designed to load the skill automatically when it detects worktrunk-related questions. ## Activity tracking The Claude Code, Codex, OpenCode, and Gemini plugins track agent sessions with status markers in `wt list`: {% terminal(cmd="wt list") %} wt list **Branch** **Status** **HEADΒ±** **main↕** **main…±** **Remoteβ‡…** **Path** **Commit** **Age** **Message** @ main ^⇑ ⇑1 . 33323bc 1d Initial commit + feature-api ↑ πŸ€– ↑1 +1 ../repo.feature-api 70343f0 1d Add REST API endpoints + review-ui ? ↑ πŸ’¬ ↑1 +1 ../repo.review-ui a585d6e 1d Add dashboard component + wip-docs ? – ../repo.wip-docs 33323bc 1d Initial commit β—‹ Showing 4 worktrees, 2 with changes, 2 ahead {% end %} - πŸ€– β€” agent is working - πŸ’¬ β€” agent is waiting or idle All four plugins clear the marker when a session ends. A stale marker can remain if the agent process is killed before its session-end hook runs. In every case, `wt config state marker clear` removes a marker manually. ### Manual status markers Set status markers manually for any workflow: {% terminal() %} wt config state marker set "🚧" # Current branch wt config state marker set "βœ…" --branch feature # Specific branch git config worktrunk.state.feature.marker '{"marker":"πŸ’¬","set_at":0}' # Direct {% end %} ## Worktree isolation (Claude Code only) Claude Code agents can run in isolated worktrees (`isolation: "worktree"`). By default, Claude Code creates these with `git worktree add`. The plugin's `WorktreeCreate` and `WorktreeRemove` hooks route this through `wt switch --create` and `wt remove` instead, so worktrees created by agents get worktrunk's naming conventions, hooks, and lifecycle management. ## `/wt-switch-create` command (Claude Code only) `/wt-switch-create [] [] [-- ]` starts a task in a fresh worktree without leaving the session: it creates the worktree, switches into it, and runs the task (all arguments optional). The worktree shows up in `wt list`; merge or remove it with `wt merge` / `wt remove`. ## Statusline (Claude Code only) `wt list statusline --format=claude-code` outputs a single-line status for the Claude Code statusline. Claude Code runs it in the background, which is what makes the occasional 1–2 second CI fetch invisible. `~/w/myproject.feature-auth !πŸ€– @+42 -8 ↑3 ⇑1 #3035 Opus πŸŒ” 65% 1.4Γ—(10am–3pm)` Worktree state comes from the same cells [`wt list`](@/list.md) renders; Claude Code's stdin JSON adds the model, the `πŸŒ” 65%` context gauge, and the rate-limit pace notice. [`wt list statusline`](@/list.md#wt-list-statusline) documents every segment, how the links behave, and the JSON fields behind them.
Add to `~/.claude/settings.json`: ```json { "statusLine": { "type": "command", "command": "wt list statusline --format=claude-code" } } ``` --- ## File: docs/content/config.md +++ title = "wt config" description = "Manage user & project configs. Includes shell integration, hooks, and saved state." weight = 15 [extra] group = "Commands" +++ Manage user & project configs. Includes shell integration, hooks, and saved state. ## Examples Install shell integration (required for directory switching): {{ terminal(cmd="wt config shell install") }} Create user config file with documented examples: {{ terminal(cmd="wt config create") }} Create project config file (`.config/wt.toml`) for hooks: {{ terminal(cmd="wt config create --project") }} Show current configuration and file locations: {{ terminal(cmd="wt config show") }} ## Configuration files | File | Location | Contains | Committed & shared | |------|----------|----------|--------------------| | **User config** | `~/.config/worktrunk/config.toml` | Worktree path template, LLM commit configs, etc | βœ— | | **Project config** | `.config/wt.toml` | Project hooks, dev server URL | βœ“ | Organizations can deploy a system-wide config file for shared defaults β€” run `wt config show` for the platform-specific location. **User config** β€” personal preferences: ```toml # ~/.config/worktrunk/config.toml worktree-path = ".worktrees/{{ branch | sanitize }}" [commit.generation] command = "MAX_THINKING_TOKENS=0 claude -p --no-session-persistence --model=haiku --tools='' --safe-mode --setting-sources='user' --system-prompt=''" ``` **Project config** β€” shared team settings: ```toml # .config/wt.toml [pre-start] deps = "npm ci" [pre-merge] test = "npm test" ``` # User Configuration Create with `wt config create`. Values shown are defaults unless noted otherwise. Location: - macOS/Linux: `~/.config/worktrunk/config.toml` (or `$XDG_CONFIG_HOME` if set) - Windows: `%APPDATA%\worktrunk\config.toml` ## Worktree path template Controls where new worktrees are created. **Available template variables:** - `{{ repo_path }}` β€” absolute path to the repository root (e.g., `/Users/me/code/myproject`. Or for bare repos, the bare directory itself) - `{{ repo }}` β€” repository directory name (e.g., `myproject`) - `{{ owner }}` β€” primary remote owner path (may include subgroups like `group/subgroup`) - `{{ remote_repo }}` β€” repository name in the primary remote URL, without `.git` (e.g., `myproject`); differs from `{{ repo }}`, the directory on disk, when a clone was renamed - `{{ branch }}` β€” raw branch name (e.g., `feature/auth`) - `{{ branch | sanitize }}` β€” filesystem-safe: `/` and `\` become `-` (e.g., `feature-auth`) - `{{ branch | sanitize_db }}` β€” database-safe: lowercase, underscores, hash suffix (e.g., `feature_auth_x7k`) - `{{ branch | codename(2) }}` β€” deterministic friendly name from a ~1.26M-combo pool (e.g., `malleable-opah`) This is a smaller set than [the variables hooks and aliases get](@/hook.md#template-variables). **Examples** for repo at `~/code/myproject`, branch `feature/auth`: Default β€” sibling directory (`~/code/myproject.feature-auth`): ```toml worktree-path = "{{ repo_path }}/../{{ repo }}.{{ branch | sanitize }}" ``` Inside the repository (`~/code/myproject/.worktrees/feature-auth`): ```toml worktree-path = "{{ repo_path }}/.worktrees/{{ branch | sanitize }}" ``` Friendly branch-derived names (`~/code/myproject.malleable-opah`): ```toml worktree-path = "{{ repo_path }}/../{{ repo }}.{{ branch | codename(2) }}" ``` Friendly names with branch identity in a parent directory (`~/code/worktrees/feature-auth/malleable-opah`): ```toml worktree-path = "{{ repo_path }}/../worktrees/{{ branch | sanitize }}/{{ branch | codename(2) }}" ``` Centralized worktrees directory (`~/worktrees/myproject/feature-auth`): ```toml worktree-path = "~/worktrees/{{ repo }}/{{ branch | sanitize }}" ``` By remote owner path (`~/development/max-sixty/myproject/feature/auth`): ```toml worktree-path = "~/development/{{ owner }}/{{ repo }}/{{ branch }}" ``` Bare repository (`~/code/myproject/feature-auth`): ```toml worktree-path = "{{ repo_path }}/../{{ branch | sanitize }}" ``` `~` expands to the home directory. Relative paths resolve from `repo_path`. ## LLM commit messages Generate commit messages automatically during merge. Requires an external CLI tool. ### Claude Code ```toml [commit.generation] command = "MAX_THINKING_TOKENS=0 claude -p --no-session-persistence --model=haiku --tools='' --safe-mode --setting-sources='user' --system-prompt=''" ``` ### Codex ```toml [commit.generation] command = "codex exec -m gpt-5.6-luna -c model_reasoning_effort='low' -c system_prompt='' --sandbox=read-only --json - | jq -sr '[.[] | select(.item.type? == \"agent_message\")] | last.item.text'" ``` ### OpenCode ```toml [commit.generation] command = "opencode run -m anthropic/claude-haiku-4.5 --variant fast" ``` ### llm ```toml [commit.generation] command = "llm -m claude-haiku-4.5" ``` ### aichat ```toml [commit.generation] command = "aichat -m claude:claude-haiku-4.5" ``` See [LLM commits docs](@/llm-commits.md) for setup and [Custom prompt templates](#custom-prompt-templates) for template customization. ## Command config ### List Persistent flag values for `wt list`. Override on command line as needed. ```toml [list] summary = false # Enable LLM branch summaries (requires [commit.generation]) full = false # Show CI status and LLM summaries (--full) branches = false # Include branches without worktrees (--branches) remotes = false # Include remote-only branches (--remotes) json-schema = 2 # JSON output schema: 2 (envelope) or 1 (bare array, the current default); unset emits 1 with a warning columns = ["branch", "status", "ci", "path"] # Columns to show, in order β€” built-ins or custom headers (omit for the default set) timeout-ms = 0 # Wall-clock budget for the entire collect phase; 0 disables ``` `columns` selects and orders the columns the `wt list` table and the `wt switch` picker render; `--format json` ignores it and always emits every field. Omit it for the default set. It is meant to drive a per-invocation [alias](@/extending.md#aliases) (`wt --config-set 'list.columns=[…]' list`), giving a named view without disturbing the default `wt list`. A static setting works but pins one layout over a table that otherwise adapts to `--full` and terminal width. Valid built-in names: - `branch` β€” The branch name - `status` β€” Git status symbols, plus any user-defined status - `working-diff` β€” Uncommitted line changes against `HEAD` (header `HEADΒ±`) - `ahead-behind` β€” Commits ahead of and behind the default branch (header `main↕`) - `branch-diff` β€” Line changes against the default branch (header `main…±`) - `summary` β€” An LLM-generated summary of the branch - `upstream` β€” Commits ahead of and behind the upstream tracking branch (header `Remoteβ‡…`) - `ci` β€” CI status of the head commit - `path` β€” The worktree's path - `url` β€” Dev-server URL from the `[list] url` template - `commit` β€” The head commit's short hash - `age` β€” Time since the last commit - `message` β€” The head commit's subject A selection mixes built-ins with [custom columns](#custom-columns), each named by its `[list.custom-columns]` header (`columns = ["branch", "Ticket", "ci"]`), and is exhaustive: only the listed columns render. Omit `columns` to keep the default set, where custom columns append automatically. A built-in name wins a header collision; the gutter type indicator always shows. Listing a column forces it on, space permitting: `ci` shows without `--full`, since `--full` only bundles columns into the default table rather than gating a named one. A column whose data source is missing still stays hidden β€” `summary` needs an LLM command (`[commit.generation]`), `url` needs a `[list] url` template β€” since listing can't supply the data. #### Custom columns Custom columns add per-branch context to the `wt list` table. Each `[list.custom-columns]` entry is a column: the key is the header, the template renders each row's cell. ```toml [list.custom-columns.Ticket] template = "{{ vars.ticket }}" # Required; the result is the cell text width = 20 # Optional max display width (default: 40) priority = 9 # Optional drop order when the terminal narrows; # lower = kept longer (default: 9, the URL band) ``` Templates may reference `{{ branch }}`, `{{ worktree_path }}`, `{{ worktree_name }}` (empty for branch-only rows), and two per-branch namespaces: - `{{ vars.* }}` β€” values stored with [`wt config state vars set`](@/config.md#wt-config-state-vars). - `{{ git.branch.* }}` β€” the branch's own git config under `branch..*`, read straight from `git config` (e.g. `{{ git.branch.jira }}` for a key you set yourself, or the git-native `description`). Git lowercases config variable names, so `branch..nvciShelf` reads as `{{ git.branch.nvcishelf }}`. All standard filters work (`sanitize`, `hash_port`, `codename`, …). A row where the template renders empty (e.g. a branch without the key) shows an empty cell; a column that is empty for every row is dropped from the table. `wt list --format json` includes the rendered values under `columns`. A `Jira` column reading a key kept in git config, and a `Summary` column showing just the first line of the git-native branch description: ```toml [list.custom-columns.Jira] template = "{{ git.branch.jira }}" [list.custom-columns.Summary] template = "{{ git.branch.description | lines | first }}" ``` ### Commit Shared by `wt step commit`, `wt step squash`, and `wt merge`. ```toml [commit] stage = "all" # What to stage before commit: "all", "tracked", or "none" ``` ### Merge Most flags are on by default. Set to false to change default behavior. ```toml [merge] squash = true # Squash commits into one (--no-squash to preserve history) commit = true # Commit uncommitted changes first (--no-commit to skip) rebase = true # Rebase onto target before merge (--no-rebase to skip) remove = true # Remove worktree after merge (--no-remove to keep) verify = true # Run project hooks (--no-hooks to skip) ff = true # Fast-forward merge (--no-ff to create a merge commit instead) ``` ### Remove Persistent flag values for `wt remove`. Override on command line as needed. ```toml [remove] delete-branch = true # Delete branch after removal (--no-delete-branch to keep) ``` ### Switch ```toml [switch] cd = true # Change directory after switching (--no-cd to skip) [switch.picker] pager = "delta --paging=never" # Example: override git's core.pager for diff preview ``` ### Step ```toml [step.copy-ignored] exclude = [] # Additional excludes (e.g., [".cache/", ".turbo/"]) ``` Built-in excludes (VCS metadata and tool-state directories) always apply; [the `wt step copy-ignored` docs](@/step.md#wt-step-copy-ignored) list them. User config and project config exclusions are combined. ### Aliases Command templates that run as `wt `. See the [Extending Worktrunk guide](@/extending.md#aliases) for usage and flags. ```toml [aliases] greet = "echo Hello from {{ branch }}" url = "echo http://localhost:{{ branch | hash_port }}" ``` Aliases defined here apply to all projects. For project-specific aliases, use the [project config](@/config.md#project-configuration) `[aliases]` section instead. ### User project-specific settings User config can include a `[projects]` table for project-specific settings β€” worktree layout, setting overrides, anything else β€” separate from the [project config](@/config.md#project-configuration) shared with teammates. Entries are keyed by project identifier β€” `//` derived from the primary remote URL (no `.git` suffix), or the canonical repo path when there is no remote. Run `wt config show` inside the repo to see the identifier for the current project; it appears in the `PROJECT CONFIG` section as `Identifier: …`. Scalar values (like `worktree-path`) replace the global value; everything else (hooks, aliases, etc.) appends, global first. See [how the layers rank](@/config.md#precedence). ```toml [projects."github.com/user/repo"] worktree-path = ".worktrees/{{ branch | sanitize }}" list.full = true merge.squash = false remove.delete-branch = false pre-start.env = "cp .env.example .env" step.copy-ignored.exclude = [".repo-local-cache/"] aliases.deploy = "make deploy BRANCH={{ branch }}" ``` #### Matching several repositories with one entry A key containing `*` matches any run of characters, `/` included, so one entry covers a whole host or namespace β€” including nested groups. `*` is the only wildcard; every other character, `.` among them, is literal. ```toml # Every repository on a self-hosted forge whose hostname carries no brand [projects."git.company.example/*"] forge.platform = "gitlab" # Everything under one namespace shares a layout [projects."git.company.example/platform/*"] worktree-path = ".worktrees/{{ branch | sanitize }}" ``` Every matching entry applies, least- to most-specific, following the rule above: a more specific entry β€” `git.company.example/platform/*` over `git.company.example/*` β€” wins where both set the same setting, while hooks and aliases from every matching entry all run, least-specific first. A literal key is the most specific of all; specificity is the count of non-`*` characters in the key. End a host-wide key with `/*` β€” a bare `git.company.example*` also covers hosts whose names merely start with that string. `approved-commands` matches the same way, so a pattern entry approves its commands for every repository it covers. Only a key written by hand is ever a pattern: `wt config approvals add` and the interactive prompt record under the exact identifier, and `wt config approvals clear` removes only that exact entry, leaving a pattern other repositories share intact. #### Forge platform and hostname `forge` names the forge for the matched repositories β€” the user-level counterpart of the project config's [forge platform](@/config.md#forge-platform) block, for a self-hosted host whose name carries no `github`, `gitlab`, or `gitea` for detection to read. ```toml [projects."git.company.example/*"] forge.platform = "gitlab" # or "github", "gitea" (experimental), "azure-devops" (experimental) forge.hostname = "api.git.company.example" # API host, when the remote's own host isn't it ``` Both fields describe the host rather than the repository, which is why a pattern keyed to a hostname suits them, and why an SSH alias resolved through `~/.ssh/config` β€” where the name in the remote URL is local to one machine β€” belongs here rather than in a repository's committed config. A repository's own `[forge]` block still wins over any entry here, field by field: a repository that sets only `platform` still takes a matching entry's `hostname`. Hooks support all three [hook forms](@/hook.md#hook-forms). A table runs multiple commands concurrently; an array-of-tables pipeline runs steps in sequence. The dotted-key examples below are equivalent to the table forms β€” TOML treats `projects."github.com/user/repo".post-start.server = "..."` and a `[projects."github.com/user/repo".post-start]` table the same way: ```toml # Single command [projects."github.com/user/repo"] post-start = "mise trust" # Multiple commands, running concurrently [projects."github.com/user/repo".post-start] mise = "mise trust" server = "npm run dev" # Pipeline: steps run in sequence [[projects."github.com/user/repo".post-start]] install = "npm ci" [[projects."github.com/user/repo".post-start]] build = "npm run build" server = "npm run dev" ``` ### Custom prompt templates Templates use [minijinja](https://docs.rs/minijinja/) syntax. #### Commit template Available variables: - `{{ git_diff }}`, `{{ git_diff_stat }}` β€” diff content - `{{ branch }}`, `{{ repo }}` β€” context - `{{ recent_commits }}` β€” recent commit messages - `{{ user_guidance }}`, `{{ project_guidance }}` β€” rendered append fragments (see [Appending to the prompt](@/config.md#appending-to-the-prompt)) Default template: ```toml [commit.generation] template = """ Write a commit message for the staged changes below. - Subject line under 50 chars - For material changes, add a blank line then a body paragraph explaining the change - Output only the commit message, no quotes or code blocks {% if user_guidance %} {{ user_guidance }} {% endif %}{% if project_guidance %} {{ project_guidance }} {% endif %} {{ git_diff_stat }} {{ git_diff }} Branch: {{ branch }} {% if recent_commits %} {% for commit in recent_commits %}- {{ commit }} {% endfor %}{% endif %} """ ``` #### Squash template Available variables (in addition to commit template variables): - `{{ commit_details }}` β€” list of commits being squashed; each renders as its subject and exposes `.subject` / `.body` - `{{ target_branch }}` β€” merge target branch Default template: ```toml [commit.generation] squash-template = """ Write a commit message for the combined effect of these commits. - Subject line under 50 chars - For material changes, add a blank line then a body paragraph explaining the change - Output only the commit message, no quotes or code blocks {% if user_guidance %} {{ user_guidance }} {% endif %}{% if project_guidance %} {{ project_guidance }} {% endif %} {% for detail in commit_details %}- {{ detail.subject }} {% endfor %} {{ git_diff_stat }} {{ git_diff }} """ ``` #### Appending to the prompt `template-append` adds personal conventions to the commit and squash prompts without restating the whole template: ```toml [commit.generation] template-append = """ - Explain the rationale in the body, not just the change """ ``` How the fragment renders, and the project-config counterpart: [the LLM commits guide](@/llm-commits.md#appending-to-the-prompt). ## Hooks See [`wt hook`](@/hook.md) for hook types, execution order, template variables, and examples. User hooks apply to all projects; [project hooks](@/config.md#project-configuration) apply only to that repository. # Project Configuration Project configuration lets teams share repository-specific settings β€” hooks, dev server URLs, and other defaults. The file lives in `.config/wt.toml` and is typically checked into version control. To create a starter file with commented-out examples, run `wt config create --project`. ## Hooks Project hooks apply to this repository only. See [`wt hook`](@/hook.md) for hook types, execution order, and examples. ```toml pre-start = "npm ci" post-start = "npm run dev" pre-merge = "npm test" ``` ## Dev server URL URL column in `wt list` (dimmed when port not listening): ```toml [list] url = "http://localhost:{{ branch | hash_port }}" ``` ## Forge platform The forge is read from the remote's hostname: any host carrying `github`, `gitlab`, or `gitea` anywhere in it, plus the Azure DevOps service domains. Name the forge explicitly for a host carrying none of those, such as a Forgejo instance at `forge.example.com`: ```toml [forge] platform = "github" # or "gitlab", "gitea" (experimental), "azure-devops" (experimental) hostname = "github.example.com" # Example: API host (GHE / self-hosted GitLab) ``` When many repositories share one self-hosted host, name it once in user config with a [pattern-keyed `[projects]` entry](@/config.md#user-project-specific-settings) instead of repeating this block in each repo. A repository's own `[forge]` still wins, field by field. ## Commit-message append `template-append` adds project-wide conventions to the LLM commit and squash prompts, shared so every teammate's LLM sees the same style guide: ```toml [commit.generation] template-append = """ - Use conventional commits (feat:, fix:, docs:, …) - Reference the relevant issue ID in the body """ ``` The first time the fragment is used (and whenever it changes), `wt` prompts the user to approve it β€” the same one-shot gate as project-defined hooks. Only `template-append` is honored from the project file; the LLM command and the main prompt template stay in [user config](@/config.md), since they describe per-developer environment (which CLI is installed, which agent the developer prefers). How the fragment renders: [the LLM commits guide](@/llm-commits.md#appending-to-the-prompt). ## Copy-ignored excludes Additional excludes for `wt step copy-ignored`: ```toml [step.copy-ignored] exclude = [".cache/", ".turbo/"] ``` Built-in excludes (VCS metadata and tool-state directories) always apply; [the `wt step copy-ignored` docs](@/step.md#wt-step-copy-ignored) list them. User config and project config exclusions are combined. ## Aliases Command templates that run as `wt `. See the [Extending Worktrunk guide](@/extending.md#aliases) for usage and flags. ```toml [aliases] deploy = "make deploy BRANCH={{ branch }}" url = "echo http://localhost:{{ branch | hash_port }}" ``` Aliases defined here are shared with teammates. For personal aliases, use the [user config](@/config.md#aliases) `[aliases]` section instead. # Shell Integration Worktrunk needs shell integration to change directories when switching worktrees. Install with: {{ terminal(cmd="wt config shell install") }} For manual setup, see `wt config shell init --help`. Without shell integration, `wt switch` prints the target directory but cannot `cd` into it. ### First-run prompts On first run without shell integration, Worktrunk offers to install it. On first commit without LLM configuration, it offers to configure a detected tool (`claude`, `codex`). Declining sets `skip-shell-integration-prompt` or `skip-commit-generation-prompt` automatically. # Other ## Environment variables All user config options can be overridden with environment variables using the `WORKTRUNK_` prefix. ### Naming convention Config keys use kebab-case (`worktree-path`), while env vars use SCREAMING_SNAKE_CASE (`WORKTRUNK_WORKTREE_PATH`). The conversion happens automatically. For nested config sections, use double underscores to separate levels: | Config | Environment Variable | |--------|---------------------| | `worktree-path` | `WORKTRUNK_WORKTREE_PATH` | | `commit.generation.command` | `WORKTRUNK_COMMIT__GENERATION__COMMAND` | | `commit.stage` | `WORKTRUNK_COMMIT__STAGE` | ### Example: CI/testing override Override the LLM command in CI to use a mock: {{ terminal(cmd="WORKTRUNK_COMMIT__GENERATION__COMMAND=__WT_QUOT__echo 'test: automated commit'__WT_QUOT__ wt merge") }} ### Other environment variables | Variable | Purpose | |----------|---------| | `WORKTRUNK_BIN` | Override binary path for shell wrappers; useful for testing dev builds | | `WORKTRUNK_CONFIG_PATH` | Override user config file location | | `WORKTRUNK_SYSTEM_CONFIG_PATH` | Override system config file location | | `WORKTRUNK_PROJECT_CONFIG_PATH` | Override project config file location (defaults to `.config/wt.toml`); relative paths resolve from the worktree root | | `XDG_CONFIG_DIRS` | Colon-separated system config directories (default: `/etc/xdg`) | | `WORKTRUNK_DIRECTIVE_CD_FILE` | Internal: set by shell wrappers. wt writes a raw path; the wrapper `cd`s to it | | `WORKTRUNK_DIRECTIVE_EXEC_FILE` | Internal: set by shell wrappers. wt writes shell commands; the wrapper sources the file | | `WORKTRUNK_SHELL_CWD` | Internal: set by wt on alias and hook bodies, so a nested `wt` preserves the user's subdirectory | | `WORKTRUNK_SHELL` | Internal: set by shell wrappers to indicate shell type (e.g., `powershell`) | | `WORKTRUNK_COMPLETE_NAME` | Internal: set by shell wrappers to the command name completions register under (defaults to the binary name) | | `WORKTRUNK_MAX_CONCURRENT_COMMANDS` | Max parallel git commands (default: 32). Lower if hitting file descriptor limits. | | `WORKTRUNK_VERBOSE` | Verbosity level (`0`/`1`/`2`), like `-v`/`-vv` but applied everywhere β€” including shell completion, which no flag can reach | | `RUST_LOG` | Logging directive (e.g. `worktrunk=debug`); overrides the verbosity baseline for what reaches stderr | | `NO_COLOR` | Disable colored output ([standard](https://no-color.org/)) | | `CLICOLOR_FORCE` | Force colored output even when not a TTY | ## Inline config overrides (`--config-set`) `--config-set ` overrides any user config key for a single invocation. The value is a TOML fragment, so arrays and tables work directly; the flag is global (works before or after the subcommand), repeatable, and a later `--config-set` replaces an earlier one for the same key. {{ terminal(cmd="wt --config-set list.full=true list|||wt step copy-ignored --config-set 'step.copy-ignored.exclude=[__WT_QUOT__target__WT_QUOT__, __WT_QUOT__dist__WT_QUOT__]'") }} This composes with aliases β€” an alias body can invoke `wt --config-set … ` to render a named view without changing the saved config. ## Precedence Sources closer to the invocation rank higher (user config above system config), and within a config file a [project entry](@/config.md#user-project-specific-settings) outranks the global key of the same name. So `worktree-path` comes from the first of these that sets it: 1. `--config-set 'worktree-path = …'` 2. `WORKTRUNK_WORKTREE_PATH` 3. `[projects."github.com/owner/repo"]` in the config file 4. global `worktree-path` in the config file A `--config-set` that names a project entry is both the highest layer and the most specific key, so it beats the same flag's global key: {{ terminal(cmd="wt --config-set 'projects.__WT_QUOT__github.com/owner/repo__WT_QUOT__.worktree-path = __WT_QUOT__/tmp/scratch__WT_QUOT__' switch --create feature") }} Hooks, aliases and `step.copy-ignored.exclude` accumulate rather than replace, so an env-set hook and a project's hook both run. ## Command reference {% terminal() %} wt config - Manage user & project configs Includes shell integration, hooks, and saved state. Usage: **wt config** [OPTIONS] <COMMAND> **Commands:** **shell** Shell integration setup **create** Create configuration file **show** Show configuration files & locations **update** Update deprecated config settings **approvals** Manage command approvals **alias** Inspect and preview aliases **plugins** Plugin management **state** Manage internal data and cache **Options:** **-h**, **--help** Print help (see a summary with '-h') **Global Options:** **-C** <path> Working directory for this command **--config** <path> User config file path **--config-set** <toml> Override config with inline TOML, e.g. --config-set list.full=true (repeatable) **-v**, **--verbose**... Verbose output (-v: info logs + hook/alias template variables on stderr; -vv: also debug logs and raw subprocess output written to .git/wt/logs/). Set WORKTRUNK_VERBOSE=0|1|2 to apply the same level everywhere β€” including shell completion, which no flag can reach **-y**, **--yes** Skip approval prompts {% end %} # Subcommands ## wt config show Show configuration files & locations. Shows location and contents of user config (`~/.config/worktrunk/config.toml`) and project config (`.config/wt.toml`). Also shows system config if present. If a config file doesn't exist, shows defaults that would be used. ### Full diagnostics Use `--full` to run diagnostic checks: {{ terminal(cmd="wt config show --full") }} This tests: - **CI tool status** β€” Whether `gh` (GitHub) or `glab` (GitLab) is installed and authenticated - **Commit generation** β€” Whether the LLM command can generate commit messages - **Version check** β€” Whether a newer version is available on GitHub ### Command reference {% terminal() %} wt config show - Show configuration files & locations Usage: **wt config show** [OPTIONS] **Options:** **--full** Run diagnostic checks (CI tools, commit generation, version) **-h**, **--help** Print help (see a summary with '-h') **Output:** **--format** <FORMAT> Output format [default: text] [possible values: text, json] **Global Options:** **-C** <path> Working directory for this command **--config** <path> User config file path **--config-set** <toml> Override config with inline TOML, e.g. --config-set list.full=true (repeatable) **-v**, **--verbose**... Verbose output (-v: info logs + hook/alias template variables on stderr; -vv: also debug logs and raw subprocess output written to .git/wt/logs/). Set WORKTRUNK_VERBOSE=0|1|2 to apply the same level everywhere β€” including shell completion, which no flag can reach **-y**, **--yes** Skip approval prompts {% end %} ## wt config approvals Manage command approvals. Project hooks and project aliases prompt for approval on first run to prevent untrusted projects from running arbitrary commands. Approvals from both flows are stored together. ### Examples List commands and their approval status for current project: {{ terminal(cmd="wt config approvals list") }} Pre-approve all hook and alias commands for current project: {{ terminal(cmd="wt config approvals add") }} Pre-approve without prompting, for a container or CI job: {{ terminal(cmd="wt config approvals add --yes") }} Clear approvals for current project: {{ terminal(cmd="wt config approvals clear") }} Clear only approvals for commands no longer in the project config: {{ terminal(cmd="wt config approvals clear --stale") }} Clear global approvals: {{ terminal(cmd="wt config approvals clear --global") }} Check whether an unattended run would stop for approval: {{ terminal(cmd="wt config approvals list --format=json | jq -r .state") }} ### How approvals work Approved commands are saved to `~/.config/worktrunk/approvals.toml`. Re-approval is required when the command template changes or the project moves. `--yes` bypasses the prompt, and what it leaves behind depends on the command it is passed to. On a command that runs project commands it grants consent for that run alone and records nothing, so the next run asks again. On `wt config approvals add` the record is the whole point, so the approvals are written β€” which is how an unattended environment pre-approves a project it has just cloned. ### Reading approval state `wt config approvals list` reads the state without prompting or writing it, so an orchestrator can find out whether a non-interactive run will stop for approval before scheduling one. `--format=json` emits: ```json { "state": "approval_required", "commands": [ {"phase": "post-start", "name": "dev", "template": "npm run dev", "approved": false}, {"phase": "pre-merge", "template": "cargo test", "approved": true} ], "stale": ["some removed command"] } ``` `state` is `no_commands` (the project declares none), `approval_required` (at least one is unapproved), or `approved`. `name` is absent for an unnamed command and for the commit-template fragment. `stale` is separate rather than a fourth `state`, because it co-occurs with all three: these are approvals recorded earlier whose command has since been edited or removed from the project config. They are what `--yes` would silently re-approve, so an orchestrator preserving the approval model reads them before choosing that flag. ### Command reference {% terminal() %} wt config approvals - Manage command approvals Usage: **wt config approvals** [OPTIONS] <COMMAND> **Commands:** **list** List project commands and their approval status **add** Store approvals in approvals.toml **clear** Clear approved commands from approvals.toml **Options:** **-h**, **--help** Print help (see a summary with '-h') **Global Options:** **-C** <path> Working directory for this command **--config** <path> User config file path **--config-set** <toml> Override config with inline TOML, e.g. --config-set list.full=true (repeatable) **-v**, **--verbose**... Verbose output (-v: info logs + hook/alias template variables on stderr; -vv: also debug logs and raw subprocess output written to .git/wt/logs/). Set WORKTRUNK_VERBOSE=0|1|2 to apply the same level everywhere β€” including shell completion, which no flag can reach **-y**, **--yes** Skip approval prompts {% end %} ## wt config alias Inspect and preview aliases. Aliases are command templates configured in user (`~/.config/worktrunk/config.toml`) or project (`.config/wt.toml`) config and run as `wt `. See the [Extending Worktrunk guide](@/extending.md#aliases) for the configuration format. ### Examples Show every configured alias's template: {{ terminal(cmd="wt config alias show") }} Show the template for `deploy`: {{ terminal(cmd="wt config alias show deploy") }} Preview an invocation without running it: {{ terminal(cmd="wt config alias dry-run deploy|||wt config alias dry-run deploy -- --env=staging") }} ### Command reference {% terminal() %} wt config alias - Inspect and preview aliases Usage: **wt config alias** [OPTIONS] <COMMAND> **Commands:** **show** Show an alias's template, or all aliases' templates **dry-run** Preview an alias invocation with template expansion **Options:** **-h**, **--help** Print help (see a summary with '-h') **Global Options:** **-C** <path> Working directory for this command **--config** <path> User config file path **--config-set** <toml> Override config with inline TOML, e.g. --config-set list.full=true (repeatable) **-v**, **--verbose**... Verbose output (-v: info logs + hook/alias template variables on stderr; -vv: also debug logs and raw subprocess output written to .git/wt/logs/). Set WORKTRUNK_VERBOSE=0|1|2 to apply the same level everywhere β€” including shell completion, which no flag can reach **-y**, **--yes** Skip approval prompts {% end %} ## wt config state Manage internal data and cache. State is stored in `.git/` (config entries and log files), separate from configuration files. ### Keys - **cache**: [Regenerable caches β€” CI status, summaries, git commands, hints, and the `wt switch -` target](@/config.md#wt-config-state-cache) - **default-branch**: [The repository's default branch (`main`, `master`, etc.)](@/config.md#wt-config-state-default-branch) - **marker**: [Custom status marker for a branch (shown in `wt list`)](@/config.md#wt-config-state-marker) - **vars**: [Custom variables per branch](@/config.md#wt-config-state-vars) - **logs**: [Operation and debug logs](@/config.md#wt-config-state-logs) ### Examples Get the default branch: {{ terminal(cmd="wt config state default-branch") }} Set the default branch manually: {{ terminal(cmd="wt config state default-branch set main") }} Set a marker for current branch: {{ terminal(cmd="wt config state marker set 🚧") }} Store arbitrary data: {{ terminal(cmd="wt config state vars set env=staging") }} Drop the regenerable caches: {{ terminal(cmd="wt config state cache clear") }} Show all stored state: {{ terminal(cmd="wt config state get") }} Clear all stored state: {{ terminal(cmd="wt config state clear") }} ### Command reference {% terminal() %} wt config state - Manage internal data and cache Usage: **wt config state** [OPTIONS] <COMMAND> **Commands:** **get** Get all stored state **clear** Clear all stored state **cache** Regenerable caches **default-branch** Default branch detection and override **logs** Operation and debug logs **marker** Branch markers **vars** [experimental] Custom variables per branch **Options:** **-h**, **--help** Print help (see a summary with '-h') **Global Options:** **-C** <path> Working directory for this command **--config** <path> User config file path **--config-set** <toml> Override config with inline TOML, e.g. --config-set list.full=true (repeatable) **-v**, **--verbose**... Verbose output (-v: info logs + hook/alias template variables on stderr; -vv: also debug logs and raw subprocess output written to .git/wt/logs/). Set WORKTRUNK_VERBOSE=0|1|2 to apply the same level everywhere β€” including shell completion, which no flag can reach **-y**, **--yes** Skip approval prompts {% end %} ## wt config state cache Regenerable caches. View or drop worktrunk's regenerable caches in one place. Everything here is rebuilt on demand β€” clearing only forces recomputation, never data loss. ### What's cached - **CI status** β€” GitHub/GitLab CI per branch (30–60s TTL), shown in [`wt list`](@/list.md#ci-status), plus the largest PR/MR number seen (sizes the CI column) - **Summaries** β€” LLM-generated branch summaries (`wt list --full`, `wt switch` preview) - **Git commands** β€” SHA-keyed disk caches: merge-tree, ancestry, diff-stats, and `wt switch` preview renders - **Hints** β€” one-time hints already shown in this repo - **Previous branch** β€” the `wt switch -` target, re-recorded on the next switch `cache clear` drops all of the above with no prompt. It re-shows one-time hints and forgets the `wt switch -` target until the next switch β€” both repopulate on their own. Without a subcommand, runs `get`. ### Examples Show cache contents: {{ terminal(cmd="wt config state cache") }} Drop all caches: {{ terminal(cmd="wt config state cache clear") }} ### Command reference {% terminal() %} wt config state cache - Regenerable caches Usage: **wt config state cache** [OPTIONS] [COMMAND] **Commands:** **get** Show cache contents **clear** Drop all caches **Options:** **-h**, **--help** Print help (see a summary with '-h') **Output:** **--format** <FORMAT> Output format (text, json) [default: text] **Global Options:** **-C** <path> Working directory for this command **--config** <path> User config file path **--config-set** <toml> Override config with inline TOML, e.g. --config-set list.full=true (repeatable) **-v**, **--verbose**... Verbose output (-v: info logs + hook/alias template variables on stderr; -vv: also debug logs and raw subprocess output written to .git/wt/logs/). Set WORKTRUNK_VERBOSE=0|1|2 to apply the same level everywhere β€” including shell completion, which no flag can reach **-y**, **--yes** Skip approval prompts {% end %} ## wt config state default-branch Default branch detection and override. Useful in scripts to avoid hardcoding `main` or `master`: {{ terminal(cmd="git rebase $(wt config state default-branch)") }} In a hook or alias template, prefer the `{{ default_branch }}` [template variable](@/hook.md#template-variables); `$(wt config state default-branch)` is for plain shell scripts. Without a subcommand, runs `get`. Use `set` to override, or `clear` then `get` to re-detect. `default-branch get` resolves the value and caches it on a miss; the aggregate `wt config state get` only reports the cache (read-only), so it can show `(none)` until something populates it. ### Detection Worktrunk detects the default branch automatically: 1. **Worktrunk cache** β€” Checks `git config worktrunk.default-branch` 2. **Git cache** β€” Detects primary remote and checks its HEAD (e.g., `origin/HEAD`) 3. **Remote query** β€” If not cached, queries `git ls-remote` β€” typically 100ms–2s, abandoned after 10s 4. **Local inference** β€” If no remote, or the query was abandoned, infers from local branches Once detected, the result is cached in `worktrunk.default-branch` for fast access. The cache isn't re-validated on every command, so a later change to `origin/HEAD` β€” a renamed default branch followed by `git remote set-head origin -a` β€” isn't picked up automatically. `wt config state` flags the drift when the cached value differs from the remote's local HEAD; `set` adopts the new branch and `clear` re-detects. An abandoned remote query is the one case that isn't cached: the branch it inferred locally answers that command, but a value guessed while the remote was unreachable would otherwise become permanent, so the next command queries again. The local inference fallback uses these heuristics in order: - If only one local branch exists, uses it - For bare repos or empty repos, checks `symbolic-ref HEAD` - Checks `git config init.defaultBranch` - Looks for common names: `main`, `master`, `develop`, `trunk` If none of these match, detection fails; set it explicitly with `wt config state default-branch set BRANCH`. ### Command reference {% terminal() %} wt config state default-branch - Default branch detection and override Usage: **wt config state default-branch** [OPTIONS] [COMMAND] **Commands:** **get** Get the default branch **set** Set the default branch **clear** Clear the default branch cache **Options:** **-h**, **--help** Print help (see a summary with '-h') **Global Options:** **-C** <path> Working directory for this command **--config** <path> User config file path **--config-set** <toml> Override config with inline TOML, e.g. --config-set list.full=true (repeatable) **-v**, **--verbose**... Verbose output (-v: info logs + hook/alias template variables on stderr; -vv: also debug logs and raw subprocess output written to .git/wt/logs/). Set WORKTRUNK_VERBOSE=0|1|2 to apply the same level everywhere β€” including shell completion, which no flag can reach **-y**, **--yes** Skip approval prompts {% end %} ## wt config state logs Operation and debug logs. View and manage log files β€” hook output, command audit trail, and debug diagnostics. ### What's logged Three kinds of logs live in `.git/wt/logs/`: #### Command log (`commands.jsonl`) All hook executions and LLM commands are recorded automatically β€” one JSON object per line. Rotates to `commands.jsonl.old` at 1MB (~2MB total). Fields: | Field | Description | |-------|-------------| | `ts` | ISO 8601 timestamp | | `wt` | The `wt` command that triggered this (e.g., `wt hook pre-merge --yes`) | | `label` | What ran (e.g., `pre-merge user:lint`, `commit.generation`) | | `cmd` | Shell command executed | | `exit` | Exit code (`null` for background commands) | | `dur_ms` | Duration in milliseconds (`null` for background commands) | The command log appends entries and is not branch-specific β€” it records all activity across all worktrees. #### Hook output logs Hook output lives in per-branch subtrees under `.git/wt/logs/{branch}/`: | Operation | Log path | |-----------|----------| | Background hooks | `{branch}/{source}/{hook-type}/{name}.log` | | Background removal | `{branch}/internal/remove.log` | All `post-*` hooks (post-start, post-switch, post-commit, post-merge) run in the background and produce log files. Source is `user` or `project`. Branch and hook names are sanitized for filesystem safety (invalid characters β†’ `-`; short collision-avoidance hash appended). Same operation on same branch overwrites the previous log. Removing a branch clears its subtree; orphans from deleted branches can be swept with `wt config state logs clear`. #### Diagnostic files | File | Created when | |------|-------------| | `trace.log` | Running with `-vv` | | `trace.jsonl` | Running with `-vv` | | `subprocess.log` | Running with `-vv` | | `diagnostic.md` | Running with `-vv` | `trace.log` is the human-readable trace at `-vv` β€” each command's start (`$ …`) and completion (`βœ“`/`βœ— … 12.3ms`), in-process spans, milestones, and bounded subprocess previews. `trace.jsonl` is the same event stream as one JSON object per line, for machines (`jq`, chrome://tracing); `wt config state logs profile` reads it to summarize a performance report (where time went, parallelism, redundant commands). `subprocess.log` holds the raw uncapped subprocess stdout/stderr bodies. `diagnostic.md` is a markdown bug-report bundle that leads with that same performance profile and inlines `trace.log`; `wt` prints a `gh gist create` command pointing at it. All four are overwritten on each `-vv` run. ### Location All logs are stored in `.git/wt/logs/` (in the main worktree's git directory). All worktrees write to the same directory. Top-level files are shared logs (command audit + diagnostics); top-level directories are per-branch log trees. ### Structured output `wt config state logs --format=json` emits three arrays β€” `command_log`, `hook_output`, `diagnostic`. Each entry carries a `file` (relative), `path` (absolute), `size`, and `modified_at` (unix seconds). Hook-output entries additionally expose `branch`, `source` (`user` / `project` / `internal`), `hook_type` (the `post-*` kind, or `null` for internal ops), and `name`. Filter with `jq` to pick out a specific entry. ### Examples List all log files: {{ terminal(cmd="wt config state logs") }} Query the command log: {{ terminal(cmd="tail -5 .git/wt/logs/commands.jsonl | jq .") }} Path to one hook log (e.g. the `post-start` `server` hook for the current branch): {{ terminal(cmd="wt config state logs --format=json | jq -r '.hook_output[] | select(.source == __WT_QUOT__user__WT_QUOT__ and .hook_type == __WT_QUOT__post-start__WT_QUOT__ and (.name | startswith(__WT_QUOT__server__WT_QUOT__))) | .path'") }} Logs for a specific branch: {{ terminal(cmd="wt config state logs --format=json | jq '.hook_output[] | select(.branch | startswith(__WT_QUOT__feature__WT_QUOT__))'") }} Clear all logs: {{ terminal(cmd="wt config state logs clear") }} ### Command reference {% terminal() %} wt config state logs - Operation and debug logs Usage: **wt config state logs** [OPTIONS] [COMMAND] **Commands:** **get** List all log file paths **profile** Performance profile from a trace **clear** Clear all log files **Options:** **-h**, **--help** Print help (see a summary with '-h') **Output:** **--format** <FORMAT> Output format (text, json) [default: text] **Global Options:** **-C** <path> Working directory for this command **--config** <path> User config file path **--config-set** <toml> Override config with inline TOML, e.g. --config-set list.full=true (repeatable) **-v**, **--verbose**... Verbose output (-v: info logs + hook/alias template variables on stderr; -vv: also debug logs and raw subprocess output written to .git/wt/logs/). Set WORKTRUNK_VERBOSE=0|1|2 to apply the same level everywhere β€” including shell completion, which no flag can reach **-y**, **--yes** Skip approval prompts {% end %} ## wt config state ci-status CI status cache. **Deprecated** β€” the CI status cache is now part of [`wt config state cache`](@/config.md#wt-config-state-cache). This subcommand still works but prints a deprecation notice. Status values, display symbols, and fetch behavior: [`wt list` CI status](@/list.md#ci-status). Without a subcommand, runs `get` for the current branch. Use `clear` to reset cache for a branch or `clear --all` to reset all. ### Command reference {% terminal() %} wt config state ci-status - CI status cache Usage: **wt config state ci-status** [OPTIONS] [COMMAND] **Commands:** **get** Get CI status for a branch **clear** Clear CI status cache **Options:** **-h**, **--help** Print help (see a summary with '-h') **Output:** **--format** <FORMAT> Output format (text, json) [default: text] **Global Options:** **-C** <path> Working directory for this command **--config** <path> User config file path **--config-set** <toml> Override config with inline TOML, e.g. --config-set list.full=true (repeatable) **-v**, **--verbose**... Verbose output (-v: info logs + hook/alias template variables on stderr; -vv: also debug logs and raw subprocess output written to .git/wt/logs/). Set WORKTRUNK_VERBOSE=0|1|2 to apply the same level everywhere β€” including shell completion, which no flag can reach **-y**, **--yes** Skip approval prompts {% end %} ## wt config state marker Branch markers. Custom status text or emoji shown in the `wt list` Status column. ### Display Markers appear at the end of the Status column, after git symbols: {% terminal(cmd="wt list") %} **Branch** **Status** **HEADΒ±** **main↕** **main…±** **Remoteβ‡…** **Commit** **Age** **Message** @ main ^⇑ ⇑1 33323bc 1d Initial commit + feature-api ↑ πŸ€– ↑1 +1 70343f0 1d Add REST API endp… + review-ui ? ↑ πŸ’¬ ↑1 +1 a585d6e 1d Add dashboard com… + wip-docs ? – 33323bc 1d Initial commit β—‹ Showing 4 worktrees, 2 with changes, 2 ahead, 1 column hidden {% end %} ### Use cases - **Work status** β€” `🚧` WIP, `βœ…` ready for review, `πŸ”₯` urgent - **Agent tracking** β€” The [Claude Code](@/claude-code.md) plugin sets markers automatically - **Notes** β€” Any short text: `"blocked"`, `"needs tests"` ### Storage Stored in git config as `worktrunk.state..marker`. Set directly with: {{ terminal(cmd="git config worktrunk.state.feature.marker '{__WT_QUOT__marker__WT_QUOT__:__WT_QUOT__🚧__WT_QUOT__,__WT_QUOT__set_at__WT_QUOT__:0}'") }} Without a subcommand, runs `get` for the current branch. For `--branch`, use `get --branch=NAME`. ### Command reference {% terminal() %} wt config state marker - Branch markers Usage: **wt config state marker** [OPTIONS] [COMMAND] **Commands:** **get** Get marker for a branch **set** Set marker for a branch **clear** Clear marker for a branch **Options:** **-h**, **--help** Print help (see a summary with '-h') **Output:** **--format** <FORMAT> Output format (text, json) [default: text] **Global Options:** **-C** <path> Working directory for this command **--config** <path> User config file path **--config-set** <toml> Override config with inline TOML, e.g. --config-set list.full=true (repeatable) **-v**, **--verbose**... Verbose output (-v: info logs + hook/alias template variables on stderr; -vv: also debug logs and raw subprocess output written to .git/wt/logs/). Set WORKTRUNK_VERBOSE=0|1|2 to apply the same level everywhere β€” including shell completion, which no flag can reach **-y**, **--yes** Skip approval prompts {% end %} ## wt config state vars Custom variables per branch. Store custom variables per branch. Values are stored as-is β€” plain strings or JSON. ### Examples Set and get values: {{ terminal(cmd="wt config state vars set env=staging|||wt config state vars get env") }} Store JSON: {{ terminal(cmd="wt config state vars set config='{__WT_QUOT__port__WT_QUOT__: 3000, __WT_QUOT__debug__WT_QUOT__: true}'") }} List all keys: {{ terminal(cmd="wt config state vars list") }} Operate on a different branch: {{ terminal(cmd="wt config state vars set env=production --branch=main") }} ### Template access Variables are available in [hook templates](@/hook.md#template-variables) as `{{ vars. }}`. Use the `default` filter for keys that may not be set: ```toml [post-start] dev = "ENV={{ vars.env | default('development') }} npm start -- --port {{ vars.port | default('3000') }}" ``` JSON object and array values support dot access: {{ terminal(cmd="wt config state vars set config='{__WT_QUOT__port__WT_QUOT__: 3000, __WT_QUOT__debug__WT_QUOT__: true}'") }} ```toml [post-start] dev = "npm start -- --port {{ vars.config.port }}" ``` ### Storage format Stored in git config as `worktrunk.state..vars.`. Keys must contain only letters, digits and hyphens β€” dots conflict with git config's section separator, underscores with its variable name format. ### Command reference {% terminal() %} wt config state vars - [experimental] Custom variables per branch Usage: **wt config state vars** [OPTIONS] <COMMAND> **Commands:** **get** Get a value **list** List all keys **set** Set a value **clear** Clear a key or all keys **Options:** **-h**, **--help** Print help (see a summary with '-h') **Global Options:** **-C** <path> Working directory for this command **--config** <path> User config file path **--config-set** <toml> Override config with inline TOML, e.g. --config-set list.full=true (repeatable) **-v**, **--verbose**... Verbose output (-v: info logs + hook/alias template variables on stderr; -vv: also debug logs and raw subprocess output written to .git/wt/logs/). Set WORKTRUNK_VERBOSE=0|1|2 to apply the same level everywhere β€” including shell completion, which no flag can reach **-y**, **--yes** Skip approval prompts {% end %} --- ## File: docs/content/extending.md +++ title = "Extending Worktrunk" description = "Three ways to add custom behavior: hooks for lifecycle automation, aliases for reusable commands, and custom subcommands for standalone tools." weight = 21 [extra] group = "Reference" +++ Worktrunk has three extension mechanisms. **[Hooks](#hooks)** are shell commands that run automatically at lifecycle events (switching, starting, committing, merging, removing). Defined in TOML. **[Aliases](#aliases)** are reusable shell commands invoked as `wt `. Defined in TOML. **[Custom subcommands](#custom-subcommands)** are standalone executables invoked as `wt `. Drop `wt-foo` on `PATH` and it becomes `wt foo`. | | Hooks | Aliases | Custom subcommands | |---|---|---|---| | **Trigger** | Automatic (lifecycle events) | Manual (`wt `) | Manual (`wt `) | | **Defined in** | TOML config | TOML config | Any executable on `PATH` | | **Template variables** | Yes | Yes | No | | **Shareable via repo** | `.config/wt.toml` | `.config/wt.toml` | Distribute the binary | | **Language** | Shell commands | Shell commands | Any | Hooks and aliases live in the same TOML config and share the [template engine](@/hook.md#template-variables). User config is trusted; project config requires approval on first run. When both define the same name, both run (user first). ## Hooks Ten hooks cover five lifecycle events β€” switch, start, commit, merge, remove β€” each with a blocking `pre-` variant (failure aborts the operation) and a background `post-` variant. [`wt hook`](@/hook.md#hook-types) maps each hook to its timing and typical uses. ```toml [pre-start] deps = "npm ci" [post-start] server = "npm run dev -- --port {{ branch | hash_port }}" [pre-merge] test = "npm test" ``` See [`wt hook`](@/hook.md) for the full reference and built-in recipes (dev server per worktree, database per worktree, progressive validation). [Tips & Patterns](@/tips-patterns.md) has more. ## Aliases Aliases are configured under `[aliases]`: ```toml [aliases] deploy = "fly deploy --config=fly.{{ env }}.toml --app=myapp-{{ branch }}" open = "open http://localhost:{{ branch | hash_port }}" since-main = "git log --oneline {{ default_branch }}..HEAD" ``` {{ terminal(cmd="wt deploy --env=staging|||wt open") }} `wt ` resolves to a built-in first, then an alias, then a [custom subcommand](#custom-subcommands). ### Templates Aliases use the same [template engine as hooks](@/hook.md#template-variables): variables, [filters](@/hook.md#worktrunk-filters), [functions](@/hook.md#worktrunk-functions), and [`--KEY=VALUE` smart routing](@/hook.md#passing-values) (bind if the template references `KEY`, else forward to `{{ args }}`). For example, `wt deploy --env=staging` sets `{{ env }}`. Alias templates add `{{ args }}` for positional CLI arguments. Operation-context variables (`target`, `base`, `pr_number`) aren't auto-populated, but can still be bound with `--KEY=VALUE`. ### Positional arguments `{{ args }}` renders as a space-joined, shell-escaped string, ready to splice into a command: ```toml [aliases] s = "wt switch {{ args }}" ``` {{ terminal(cmd="wt s some-branch|||wt s feature/api|||wt s 'has a space'") }} For indexing (`{{ args[0] }}`), looping, and counting, see [Passing values](@/hook.md#passing-values). Tokens after `--` forward unconditionally, bypassing any binding. Writing `wt deploy --Β --branch=foo` forwards the literal `--branch=foo` to `{{ args }}` even though the template references `{{ branch }}`. An alias that forwards `{{ args }}` to a `wt` command β€” like `co = "wt switch {{ args }}"` or `cm = "wt step commit {{ args }}"` β€” inherits that command's argument and flag completion, so `wt co ` completes branches the same way `wt switch ` does. ### Inspecting and previewing - `wt config alias show ` prints the template. - `wt config alias dry-run [-- args...]` prints the rendered command. {{ terminal(cmd="wt config alias show deploy|||wt config alias dry-run deploy|||wt config alias dry-run deploy -- --env=staging") }} ### Multi-step pipelines `[[aliases.NAME]]` defines a pipeline using the [same `[[block]]` semantics as hooks](@/hook.md#hook-forms): blocks run in order, keys within a block run concurrently, and a step failure aborts the remainder. ```toml [[aliases.release]] test = "cargo test" [[aliases.release]] build = "cargo build --release" package = "cargo package --no-verify" [[aliases.release]] publish = "cargo publish {{ args }}" ``` Every step sees the same `{{ args }}` and bound variables. `wt release -- --dry-run` forwards `--dry-run` to `publish` without affecting earlier steps. ### Changing directory `wt switch`, `wt merge` (when it leaves the removed source), and `wt remove` of the current worktree change the parent shell's directory even when invoked from an alias; the Worktrunk shell integration propagates the change through. Other shell state doesn't persist: the alias runs in a subshell, so `cd`, `export`, and similar commands only affect that subshell. ### Deferring expansion to a nested `wt` command A `wt step for-each` alias that prints the same branch in every worktree is rendering `{{ branch }}` too early. An alias body renders once at dispatch, in the invoking worktree, so a bare `{{ branch }}` is baked to that worktree's branch before for-each iterates. (`wt config alias dry-run ` shows the rendered body, with the value already baked in.) `{% raw %}…{% endraw %}` defers the variable: it survives the dispatch render as a literal `{{ branch }}`, and for-each expands it per worktree. One catch for `for-each`: the deferred `{{ branch }}` has spaces, so the alias body's `sh -c` splits it into `{{`, `branch`, `}}` before for-each sees it (`Failed to expand for-each argument: syntax error`). Give for-each its own `sh -c '…'` to keep the value one token: ```toml [aliases] show-branches = "wt step for-each -- sh -c 'echo {% raw %}{{ branch }}{% endraw %}'" ``` `wt show-branches` prints each worktree's own branch. `wt switch --execute` defers the same way, without the extra wrapper: its `--execute '…'` argument is already a single quoted string, so only `{% raw %}` is needed. Here `{{ worktree_path }}` expands against the worktree being created, not the one the alias ran from: ```toml [aliases] echo-target = "wt switch {{ args }} --no-cd --execute 'echo {% raw %}{{ worktree_path }}{% endraw %}'" ``` A repo-level variable like `{{ default_branch }}` needs no deferral: it is identical in every worktree, so a bare `{{ default_branch }}` is already correct everywhere. ### Recipe: rebase every worktree onto its upstream ```toml [aliases] up = ''' git fetch --all --prune && wt step for-each -- sh -c ' git rev-parse --verify -q @{u} >/dev/null || exit 0 g=$(git rev-parse --git-dir) test -d "$g/rebase-merge" -o -d "$g/rebase-apply" && exit 0 git update-index --refresh -q >/dev/null || true git rebase @{u} --no-autostash || git rebase --abort '''' ``` `wt up` fetches all remotes, then iterates every worktree: skip if no upstream, skip if mid-rebase, refresh the index to drop stale stat entries, then rebase and auto-abort on conflict. It rebases onto git-native `@{u}` rather than a `{{ … }}` template, so git resolves each worktree's own upstream and there is nothing to defer. ### Recipe: move or copy in-progress changes to a new worktree `wt switch --create` lands you in a clean worktree. To carry staged, unstaged, and untracked changes along, pair it with `git stash`: ```toml # .config/wt.toml [aliases] move-changes = ''' if git diff --quiet HEAD && test -z "$(git ls-files --others --exclude-standard)"; then wt switch --create {{ to }} --execute="{{ args }}" else git stash push --include-untracked --quiet wt switch --create {{ to }} --execute="git stash pop --index; {{ args }}" fi ''' ``` Run with `wt move-changes --to=feature-xyz`. The guard skips the stash when nothing is in flight; otherwise `git stash push` captures everything and `--execute` pops it in the new worktree with the staged/unstaged split intact. Anything after `--` runs in the new worktree after pop. For example, `wt move-changes --to=feature-xyz -- claude` opens Claude there. To copy instead of move, add `git stash apply --index --quiet` right after the push. ### Recipe: tail a specific hook log `wt config state logs --format=json` emits structured entries (`branch`, `source`, `hook_type`, `name`, `path`). Pipe through `jq` to resolve one entry, then wrap in an alias for quick access: ```toml [aliases] hook-log = ''' tail -f "$(wt config state logs --format=json | jq -r --arg name "{{ name | sanitize_hash }}" --arg kind "{{ kind }}" ' .hook_output[] | select(.branch == "{{ branch | sanitize_hash }}" and .hook_type == $kind and .name == $name) | .path ' | head -1)" ''' ``` Run with `wt hook-log --kind=post-start --name=server` to tail the log for the `server` hook on the current branch. `--kind` picks the hook type; the branch is pulled from the current worktree via `{{ branch }}`. `sanitize_hash` rewrites `branch` and `name` to filesystem-safe forms with a hash suffix that keeps distinct originals unique (the same transformation Worktrunk applies on disk), so the alias resolves the right log even when either contains characters like `/`. ## Custom subcommands Any executable named `wt-` on `PATH` becomes available as `wt `, the same pattern git uses for `git-foo`. Built-in commands and [aliases](#aliases) take precedence. {{ terminal(cmd="wt sync origin # runs: wt-sync origin|||wt -C /tmp/repo sync # -C is forwarded as the child's working directory") }} Arguments pass through verbatim, stdio is inherited, and the child's exit code propagates unchanged. ### Examples - [`worktrunk-sync`](https://github.com/pablospe/worktrunk-sync): rebases stacked worktree branches in the dependency order inferred from git history. Install with `cargo install worktrunk-sync`, then run as `wt sync`. - [`workz`](https://github.com/rohansx/workz): provisions the current worktree with a collision-free port range plus its own database and Docker Compose project, merged into `.env.local`, so parallel worktrees don't clash. Install with `cargo install workz`, drop its [`wt-workz`](https://github.com/rohansx/workz/blob/main/examples/wt-workz) adapter on `PATH`, then run as `wt workz`. ## Reference: hooks vs. aliases Aside from the differences below, hooks and aliases behave the same. Interface differences | Axis | Hooks | Aliases | |------|-------|---------| | Invocation | `wt hook [args...]` (nested under the `hook` built-in) | `wt [args...]` (top-level) | | Bare positionals | Filter names (`wt hook pre-merge test build` runs only `test` and `build`) | Forwarded to `{{ args }}` | | Reach `{{ args }}` from positionals | Must use `--` (`wt hook pre-merge -- extra`) | Any bare positional lands there | | Approval skip flag | Post-subcommand `--yes` / `-y` supported (`wt hook pre-merge --yes`) | Only the global form (`wt -y `); post-alias `--yes` falls through to `{{ args }}` | | Source discrimination | `user:` / `project:` / `user:name` / `project:name` filter syntax | Run user first, then project; no filter syntax | | Force-bind escape | `--var KEY=VALUE` (deprecated in favor of `--KEY=VALUE`, but still force-binds) | None; smart routing is the only path | | `--help` | `wt hook --help` lists hook types; `wt hook --help` shows flags and arguments for that type | The template body is the documentation: `wt --help` redirects to `wt config alias show` / `dry-run`. `wt --help` and `wt step --help` list configured aliases alongside built-in commands | | Inspection | `wt hook show [type] [--expanded]` | `wt config alias show ` / `wt config alias dry-run ` | | Stdin | All template variables as JSON (parse with `json.load(sys.stdin)`) | Inherits parent stdin (pipes pass through; interactive TUIs like `wt switch` keep the tty) | | Template-context extras | `hook_type`, `hook_name`, per-type operation vars (`base`, `target`, `pr_number`, …) | `args` on top of the shared base variables | --- ## File: docs/content/faq.md +++ title = "FAQ" description = "Common questions about Worktrunk: comparison to git worktree and branch switching, bare repos, TUI support, and more." weight = 25 [extra] group = "Reference" +++ ## How does Worktrunk compare to alternatives? ### vs. branch switching Branch switching uses one directory: uncommitted changes from one agent get mixed with the next agent's work, or block switching entirely. Worktrees give each agent its own directory with independent files and index. ### vs. Plain `git worktree` Git's built-in worktree commands work but require manual lifecycle management: {% terminal() %} # Plain git worktree workflow git worktree add -b feature-branch ../myapp-feature main cd ../myapp-feature # ...work, commit, push... cd ../myapp git merge feature-branch git worktree remove ../myapp-feature git branch -d feature-branch {% end %} Worktrunk automates the full lifecycle: {% terminal() %} wt switch --create feature-branch # Creates worktree, runs setup hooks # ...work... wt merge # Merges into default branch, cleans up {% end %} No cd back to main β€” `wt merge` runs from the feature worktree and merges into the target, like GitHub's merge button. What `git worktree` doesn't provide: - Consistent directory naming and cleanup validation - Project-specific automation (install dependencies, start services) - Unified status across all worktrees (commits, CI, conflicts, changes) ### vs. git-machete / git-town Different scopes: - **git-machete**: Branch stack management in a single directory - **git-town**: Git workflow automation in a single directory - **worktrunk**: Multi-worktree management with hooks and status aggregation These tools can be used togetherβ€”run git-machete or git-town inside individual worktrees. ### vs. Git TUIs (lazygit, gh-dash, etc.) Git TUIs operate on a single repository. Worktrunk manages multiple worktrees, runs automation hooks, and aggregates status across branches. TUIs work inside each worktree directory. ## Does Worktrunk support stacked branches? Not natively β€” stacked-branch workflows are a large design space, so Worktrunk treats them as an extension rather than a built-in. [`worktrunk-sync`](https://github.com/pablospe/worktrunk-sync) is a community tool that auto-detects the branch dependency tree from git history and rebases each branch onto its parent in topological order. Install with `cargo install worktrunk-sync` and run as `wt sync` (via [custom subcommands](@/extending.md#custom-subcommands)). ## How do I move uncommitted changes to a new worktree? Stash the changes, create the worktree, then pop: {% terminal() %} git stash push -u # -u also stashes untracked files wt switch --create feature # new branch off the default branch git stash pop # changes reappear in the new worktree {% end %} The stash lives in the shared `.git` directory, so it's reachable from the new worktree. The original branch is left clean. `wt switch --create` bases the new branch on the default branch. To base it on the current commit instead, pass `--base=@` (needed when the current branch has commits beyond the default branch). ## There's an issue with my shell setup If shell integration isn't working (auto-cd not happening, completions missing, `wt` not found as a function), the fastest path to a fix is using Claude Code with the Worktrunk plugin: 1. Install the [Worktrunk plugin](@/claude-code.md) in Claude Code 2. Ask Claude to debug the Worktrunk shell integration Claude will run `wt config show`, inspect the shell config files, and identify the issue. If Claude can't fix it, please [open an issue](https://github.com/max-sixty/worktrunk/issues/new?title=Shell%20setup%20issue&body=%23%23%20Shell%20and%20OS%0A%0A-%20Shell%3A%20%0A-%20OS%3A%20%0A%0A%23%23%20Output%20of%20%60wt%20config%20show%60%0A%0A%60%60%60%0A%0A%60%60%60%0A%0A%23%23%20What%20Claude%20found%20%28if%20available%29%0A%0A) with the output of `wt config show`, the shell (bash/zsh/fish), and OS. (And even if it fixes the problem, feel free to open an issue: non-standard success cases are useful for ensuring Worktrunk is easy to set up for others.) ## What does `-v` / `-vv` do? Three verbosity levels. Each is a superset of the previous one. | Level | Stderr | Files (`.git/wt/logs/`) | Use case | |-------|--------|-------------------------|----------| | (none) | Warnings only | β€” | Normal use | | `-v` | + Info: hook output, alias template variable resolution | β€” | Debugging hooks/aliases | | `-vv` | Same as `-v` | + `trace.log`, `trace.jsonl`, `subprocess.log`, `diagnostic.md` | Filing a bug | At `-vv`, debug-level records (command lines, in-process spans, bounded subprocess preview) route to `trace.log` instead of stderr β€” so the terminal stays readable while the deep trace lands on disk. A one-line pointer on stderr shows where the files went. The `-vv` files have distinct audiences: `trace.log` is the human trace (bounded, gistable), `trace.jsonl` the same records for machines, `subprocess.log` the raw uncapped subprocess output, and `diagnostic.md` a bug-report bundle. Each is described in [`wt config state logs`](@/config.md#wt-config-state-logs). `RUST_LOG` overrides the flag baseline when set (`RUST_LOG=debug wt -v` lifts `-v` to debug-on-stderr). The flags only reach a command you type; shell completion runs as its own process with nowhere to pass one. Set `WORKTRUNK_VERBOSE=0|1|2` to apply the level to *every* invocation, completion included β€” it's the env-var equivalent of `-v`/`-vv`, so level 2 writes the same `trace.log`/`trace.jsonl`/`subprocess.log`/`diagnostic.md` files. An explicit `-v`/`-vv` on a command raises the level further but never lowers this baseline. To profile a slow tab-completion, run it the way your shell does β€” e.g. `WORKTRUNK_VERBOSE=2 COMPLETE=fish wt -- wt switch ''` β€” then render the result with `wt config state logs profile`. ## What files does Worktrunk create? ### 1. Worktree directories Created by `wt switch ` when switching to a branch that doesn't have a worktree. Use `wt switch --create ` to create a new branch. Default location is `../.` (sibling to main repo), configurable via `worktree-path` in user config. **To remove:** `wt remove ` removes the worktree directory and deletes the branch. ### 2. Config files | File | Created by | Purpose | |------|------------|---------| | `~/.config/worktrunk/config.toml` | `wt config create` | User preferences | | `~/.config/worktrunk/approvals.toml` | Approving project commands | Approved hook and alias commands | | `.config/wt.toml` | `wt config create --project` | Project hooks (checked into repo) | User config location: `$XDG_CONFIG_HOME/worktrunk/` (or `~/.config/worktrunk/`) on Linux/macOS, `%APPDATA%\worktrunk\` on Windows. **To remove:** Delete directly. User config: `rm ~/.config/worktrunk/config.toml`. Project config: `rm .config/wt.toml` (and commit). ### 3. Shell integration Created by `wt config shell install`: - **Bash**: adds line to `~/.bashrc` - **Zsh**: adds line to `~/.zshrc` (or `$ZDOTDIR/.zshrc`) - **Fish**: creates `~/.config/fish/functions/wt.fish` and `~/.config/fish/completions/wt.fish` - **Nushell** : creates `wt.nu` in Nushell's user vendor-autoload directory β€” the last entry of `$nu.vendor-autoload-dirs`, under `$nu.data-dir` (typically `~/.local/share/nushell/vendor/autoload` on Linux, `~/Library/Application Support/nushell/vendor/autoload` on macOS) - **PowerShell** (Windows): creates both profile files if they don't exist: - `Documents/PowerShell/Microsoft.PowerShell_profile.ps1` (PowerShell 7+) - `Documents/WindowsPowerShell/Microsoft.PowerShell_profile.ps1` (Windows PowerShell 5.1) Fish and Nushell wrappers live at a path named after the command, so install writes that file whole, replacing an existing `functions/wt.fish`, `completions/wt.fish`, or `wt.nu`. Bash, zsh, and PowerShell rc files hold the rest of a shell's setup, so install only appends a line to those. **PowerShell detection on Windows:** When running from cmd.exe or PowerShell, both PowerShell profile files are created automatically. When running from Git Bash or MSYS2, PowerShell is skipped (use `wt config shell install powershell` to create the profiles explicitly). **To remove:** `wt config shell uninstall`. ### 4. Metadata in `.git/` (automatic) Worktrunk stores small amounts of cache and log data in the repository's `.git/` directory: | Location | Purpose | Created by | |----------|---------|------------| | `git config worktrunk.*` | Cached default branch, switch history, branch markers, custom variables | Various commands | | `.git/wt/cache/{kind}/*.json` | Cached CI status, the largest PR/MR number seen (sizes the `wt list` CI column), and git command results (merge-tree, integration probes, diff stats, ancestry checks, ahead/behind counts, merge bases) | `wt list`, `wt merge`, `wt remove` | | `.git/wt/cache/summary/{branch}/{hash}.json` | Cached LLM branch summaries, content-addressed by diff hash | `wt list --full`, `wt switch` (when `[list] summary = true`) | | `.git/wt/logs/{branch}/**/*.log` | Background hook output (nested per branch) | Hooks, background `wt remove` | | `.git/wt/logs/commands.jsonl` | Command audit log (~2MB max) | Hooks, LLM commands | | `.git/wt/logs/trace.log` | Human debug trace for issue reporting | Running with `-vv` | | `.git/wt/logs/trace.jsonl` | Machine trace (one JSON object per record) | Running with `-vv` | | `.git/wt/logs/subprocess.log` | Raw uncapped subprocess stdout/stderr (may be multi-MB) | Running with `-vv` | | `.git/wt/logs/diagnostic.md` | Diagnostic report for issue reporting (leads with the performance profile) | Running with `-vv` | | `.git/wt/trash/-` | Staged worktree contents pending background deletion | `wt remove` | None of this is tracked by git or pushed to remotes. **To remove:** `wt config state clear` removes all worktrunk data β€” config keys, caches, markers, hints, variables, logs, and stale trash. ### What Worktrunk does NOT create - No files outside `.git/`, config directories, or worktree directories - No global git hooks - No modifications to `~/.gitconfig` - No long-running background processes or daemons ## What can Worktrunk delete? Worktrunk can delete **worktrees** and **branches**. Both have safeguards. ### Worktree removal `wt remove` mirrors `git worktree remove`: it refuses to remove worktrees with uncommitted changes (staged, modified, or untracked files). The `--force` flag removes the worktree anyway, discarding all of those changes. Removal also refuses, `--force` included, when the directory at a registered path no longer holds the worktree registered there β€” a clone made there after the worktree was deleted, say, or another worktree of the same repository moved onto the path. `--force` waives uncommitted changes, not the check for what the directory holds, and `git worktree remove` refuses the same cases. To protect a worktree from removal entirely (say it holds a local database), lock it: {{ terminal(cmd="git worktree lock ../myproject.feature --reason __WT_QUOT__Contains local database__WT_QUOT__") }} Locked worktrees show `⊞` in `wt list`. Neither `git worktree remove` nor `wt remove` (even with `--force`) will delete them. Unlock with `git worktree unlock`. ### Branch deletion By default, `wt remove` only deletes branches whose content is already in the default branch. Branches showing `_` (same commit) or `βŠ‚` (integrated) in `wt list` are safe to delete. For the full algorithm, see [Branch cleanup](@/remove.md#branch-cleanup) β€” it handles squash-merge and rebase workflows where commit history differs but file changes match. Use `-D` to force-delete branches with unmerged changes. Use `--no-delete-branch` to keep the branch regardless of status. A branch checked out in a second worktree is retained regardless, `-D` included. Deleting it would leave that worktree unable to resolve `HEAD`; only `git worktree add --force` produces that state. ### Other cleanup - `wt merge` / `wt step push` β€” the target branch's checked-out worktree is updated to the merged commits, so a file those commits delete disappears from it, and an ignored file at a path they track is overwritten β€” the same result a `git merge` run in that worktree would produce. Uncommitted changes at paths the merge doesn't touch stay in place, staged or not; one at a path it does touch refuses the merge upfront, naming the file - `wt remove` β€” besides the target worktree, two cleanup mechanisms run. The removed worktree's own `git fsmonitor--daemon` (git's per-worktree filesystem watcher under `core.fsmonitor=true`, which would leak once its worktree is gone) is sent `git fsmonitor--daemon stop`, then force-terminated (`SIGTERM`, then `SIGKILL`) via the PID resolved from its IPC socket if it didn't exit. A background sweep then deletes `.git/wt/trash/` entries older than 24 hours (directories orphaned when a previous background removal was interrupted) and terminates fsmonitor daemons whose worktree no longer exists (orphans from `git worktree remove`, `rm -rf`, or a crashed `wt`) - `wt config state clear` β€” removes all worktrunk data from `.git/` (config keys, caches, markers, hints, variables, logs, stale trash) - `wt config shell install` β€” when migrating an integration to a new location, removes the file left at the old one: fish `conf.d/wt.fish` (now `functions/wt.fish`) and nushell wrappers stranded under `/vendor/autoload` (now `/vendor/autoload`). The old path is where worktrunk's own wrapper lived and is named after the command being installed, so it's taken back whole without reading it β€” a `conf.d/wt.fish` left in place would be sourced at startup and shadow the new wrapper anyway. Only that exact filename is touched, and each removal is printed - `wt config shell uninstall` β€” removes integration lines from bash/zsh/PowerShell rc files, and deletes worktrunk's wrapper and completion files (fish `functions/`, `conf.d/`, and `completions/`; nushell `vendor/autoload`). Uninstall takes no command name, so it lists those directories and recognizes files by worktrunk's own content markers, whatever binary name they were installed under; files without the markers are left alone. An rc file belongs to the user, so a line qualifies only where it runs the init command: one that merely mentions it, inside a comment, an `echo`, or an alias body, stays. Every line uninstall does take is printed, before removal and again after See [What files does Worktrunk create?](#what-files-does-worktrunk-create) for details. ## What commands does Worktrunk execute? Worktrunk runs `git` commands internally and optionally runs `gh` (GitHub) or `glab` (GitLab) for CI status. Beyond that, user-defined commands execute in four contexts: 1. **User hooks** (`~/.config/worktrunk/config.toml`) β€” Personal automation for all repositories 2. **Project hooks** (`.config/wt.toml`) β€” Repository-specific automation 3. **LLM commands** (`~/.config/worktrunk/config.toml`) β€” Commit message generation and [branch summaries](@/llm-commits.md#branch-summaries) 4. **--execute flag** β€” Explicitly provided commands User hooks and user aliases don't require approval (you defined them). Commands from project hooks and project aliases require approval on first run. Approved commands are saved to the approvals file (`approvals.toml`). If a command changes, Worktrunk requires new approval. ### Example approval prompt {% terminal() %} β–² **repo** needs approval to execute **3** commands: β—‹ pre-start **install**: npm ci β—‹ pre-start **build**: cargo build --release β—‹ pre-start **env**: echo 'PORT={{ branch | hash_port }}' > .env.local ❯ Allow and remember? **[y/N]** {% end %} Use `--yes` to bypass prompts (useful for CI/automation). ### Command log All hook executions and LLM commands are recorded in `.git/wt/logs/commands.jsonl` β€” one JSON object per line. Fields: `ts` (timestamp), `wt` (the wt command that triggered it), `label` (what ran, e.g., `pre-merge user:lint`), `cmd` (shell command), `exit` (exit code, `null` for background), `dur_ms` (duration, `null` for background). The file rotates to `commands.jsonl.old` at 1MB, bounding storage to ~2MB. View the log with `wt config state logs get`, or query directly: {% terminal() %} # Recent commands tail -5 .git/wt/logs/commands.jsonl | jq . # Failed commands jq 'select(.exit != 0 and .exit != null)' .git/wt/logs/commands.jsonl {% end %} Clear with `wt config state logs clear`. ## Does Worktrunk work on Windows? Yes. Core commands, shell integration, and tab completion work in both Git Bash and PowerShell. See [installation](@/worktrunk.md#install) for setup details, including avoiding the Windows Terminal `wt` conflict. **Git for Windows required** β€” Hooks use bash syntax and execute via Git Bash, so [Git for Windows](https://gitforwindows.org/) must be installed even when PowerShell is the interactive shell. The `wt switch` interactive picker runs on Windows too, on [skim](https://github.com/skim-rs/skim)'s crossterm backend. ## How does Worktrunk determine the default branch? Worktrunk checks the local git cache first, queries the remote if needed, and falls back to local inference when no remote exists. If the remote's default branch has changed (e.g., renamed from master to main), clear the cache with `wt config state default-branch clear`. For full details on the detection mechanism, see `wt config state default-branch --help`. ## My `for-each` or `--execute` alias prints the same value in every worktree An alias body renders once at dispatch, in the invoking worktree's context, so a per-worktree variable like `{{ branch }}` is baked to that one worktree's value before the nested `wt` command iterates. Every worktree then sees the same value. Confirm it with `wt config alias dry-run `: if the value is already substituted (e.g. `… echo branch=main`), it was baked at dispatch. To defer a variable to the nested command, wrap it as `{% raw %}{{ branch }}{% endraw %}`; for `wt step for-each`, also keep it inside a quoted `sh -c '…'` so the alias's shell doesn't word-split it. See [deferring expansion in an alias](@/extending.md#deferring-expansion-to-a-nested-wt-command). A repo-level variable like `{{ default_branch }}` is unaffected β€” it is identical in every worktree. ## Installation fails with C compilation errors Errors related to tree-sitter or C compilation (C99 mode, `le16toh` undefined) can be avoided by installing without syntax highlighting: {{ terminal(cmd="cargo install worktrunk --no-default-features --features cli") }} This disables bash syntax highlighting in command output but keeps all core functionality. The syntax highlighting feature requires C99 compiler support and can fail on older systems or minimal Docker images. ## Running tests (for contributors) ### Quick tests {{ terminal(cmd="cargo test") }} ### Full integration tests Shell integration tests require bash, zsh, fish, nushell, and pwsh, plus `jq`: {{ terminal(cmd="cargo test --test integration --features shell-integration-tests") }} ## How can I contribute? - Star the repo - Try it out and [open an issue](https://github.com/max-sixty/worktrunk/issues) with feedback β€” even small annoyances - What worktree friction does Worktrunk not yet solve? [Tell us](https://github.com/max-sixty/worktrunk/issues) - Send to a friend - Post about it on [X](https://twitter.com/intent/tweet?text=Worktrunk%20%E2%80%94%20CLI%20for%20git%20worktree%20management&url=https%3A%2F%2Fworktrunk.dev), [Reddit](https://www.reddit.com/submit?url=https%3A%2F%2Fworktrunk.dev&title=Worktrunk%20%E2%80%94%20CLI%20for%20git%20worktree%20management), or [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fworktrunk.dev) --- ## File: docs/content/hook.md +++ title = "wt hook" description = "Run configured hooks." weight = 17 [extra] group = "Commands" +++ Run configured hooks. Hooks are shell commands that run at key points in the worktree lifecycle β€” automatically during `wt switch`, `wt merge`, & `wt remove`, or on demand via `wt hook `. Both user and project hooks are supported. # Hook Types | Event | `pre-` β€” blocking | `post-` β€” background | |-------|-------------------|---------------------| | **switch** | `pre-switch` | `post-switch` | | **create** | `pre-start` | `post-start` | | **commit** | `pre-commit` | `post-commit` | | **merge** | `pre-merge` | `post-merge` | | **remove** | `pre-remove` | `post-remove` | `pre-*` hooks block β€” failure aborts the operation. `post-*` hooks run in the background with output logged (use [`wt config state logs`](@/config.md#wt-config-state-logs) to find and manage log files). Use `-v` to see the template variables for background hooks; `wt hook --dry-run` previews the commands. The most common creation hook is `post-start` β€” it runs background tasks (dev servers, file copying, builds) without blocking worktree creation. Prefer `post-start` over `pre-start` unless a later step needs the work completed first. | Hook | Purpose | |------|---------| | `pre-switch` | Runs in the source worktree before switching β€” creating, switching to existing, or staying on current | | `post-switch` | Triggers on all switch results: creating, switching to existing, or staying on current | | `pre-start` | Runs once when a new worktree is created, blocking `post-start`/`--execute` until complete: dependency install, env file generation | | `post-start` | Runs once when a new worktree is created, in the background: dev servers, long builds, file watchers, copying caches | | `pre-commit` | Formatters, linters, type checking β€” runs during `wt merge` before the squash commit | | `post-commit` | CI triggers, notifications, background linting | | `pre-merge` | Tests, security scans, build verification β€” runs after rebase, before merge to target | | `post-merge` | Deployment, notifications, installing updated binaries. Runs in the target branch worktree if it exists, otherwise the primary worktree | | `pre-remove` | Cleanup before worktree deletion: saving test artifacts, backing up state. Runs in the worktree being removed | | `post-remove` | Stopping dev servers, removing containers, notifying external systems. Template variables reference the removed worktree | During `wt merge`, hooks run in this order: pre-commit β†’ post-commit β†’ pre-merge β†’ pre-remove β†’ post-remove + post-merge. See [`wt merge`](@/merge.md#pipeline) for the complete pipeline. # Security Project commands require approval on first run: {% terminal() %} β–² **repo** needs approval to execute **3** commands: β—‹ pre-start **install**: npm ci β—‹ pre-start **build**: cargo build --release β—‹ pre-start **env**: echo 'PORT={{ branch | hash_port }}' > .env.local ❯ Allow and remember? **[y/N]** {% end %} - Approvals are saved to `~/.config/worktrunk/approvals.toml` - If a command changes, new approval is required - Declining skips every project command for that operation β€” including any already approved β€” and continues without them; saved approvals are unaffected - Use `--yes` to bypass prompts β€” useful for CI and automation - Use `--no-hooks` to skip hooks Manage approvals with `wt config approvals add` and `wt config approvals clear`. # Configuration Hooks can be defined in project config (`.config/wt.toml`) or user config (`~/.config/worktrunk/config.toml`). Both use the same format. The project config is read from the worktree the command ran in. ## Hook forms Hooks take one of three forms, determined by their TOML shape. A string is a single command: ```toml pre-start = "npm install" ``` A table is multiple commands that run concurrently: ```toml [post-start] server = "npm run dev" watch = "npm run watch" ``` A pipeline is a sequence of `[[hook]]` blocks run in order. Each block is one step; multiple keys within a block run concurrently. A failing step aborts the rest of the pipeline: ```toml [[post-start]] install = "npm ci" [[post-start]] build = "npm run build" server = "npm run dev" ``` Here `install` runs first, then `build` and `server` run together. Templates are syntax-checked before the pipeline starts and rendered as each step runs, so a step can store [per-branch vars](@/config.md#wt-config-state-vars) that later steps read via `{{ vars. }}`. Because an earlier step can still change those values, a preview leaves them alone: `wt hook --dry-run` and `wt hook show --expanded` render `{{ vars. }}` as itself while every other variable expands. Most hooks don't need `[[hook]]` blocks. Reach for them when there's a dependency chain β€” typically setup that must complete before later steps, like installing dependencies before running a build and dev server concurrently. ## Project vs user hooks | Aspect | Project hooks | User hooks | |--------|--------------|------------| | Location | `.config/wt.toml` | `~/.config/worktrunk/config.toml` | | Scope | Single repository | All repositories (or [per-project](@/config.md#user-project-specific-settings)) | | Approval | Required | Not required | | Execution order | After user hooks | First | Skip all hooks with `--no-hooks`. To run a specific hook when user and project both define the same name, use `user:name` or `project:name` syntax. ## Template variables Hooks can use template variables that expand at runtime: | Kind | Variable | Description | |------|----------|-------------| | active | `{{ branch }}` | Branch name | | | `{{ worktree_path }}` | Worktree path | | | `{{ worktree_name }}` | Worktree directory name | | | `{{ commit }}` | Branch HEAD SHA | | | `{{ short_commit }}` | Branch HEAD SHA, abbreviated per `core.abbrev` | | | `{{ upstream }}` | Branch upstream (if tracking a remote) | | operation | `{{ base }}` | Base branch name (switch/create only) | | | `{{ base_worktree_path }}` | Base worktree path | | | `{{ target }}` | Target branch name | | | `{{ target_worktree_path }}` | Target worktree path (when target has a worktree) | | | `{{ pr_number }}` | PR/MR number (post-switch, pre-start, post-start; when creating via `pr:N` / `mr:N`) | | | `{{ pr_url }}` | PR/MR web URL (post-switch, pre-start, post-start; when creating via `pr:N` / `mr:N`) | | repo | `{{ repo }}` | Repository directory name | | | `{{ repo_path }}` | Absolute path to repository root | | | `{{ owner }}` | Primary remote owner path (may include subgroups) | | | `{{ remote_repo }}` | Repository name from the primary remote URL, without `.git` | | | `{{ primary_worktree_path }}` | Primary worktree path | | | `{{ default_branch }}` | Default branch name | | | `{{ remote }}` | Primary remote name | | | `{{ remote_url }}` | Remote URL | | exec | `{{ cwd }}` | Directory where the hook command runs | | | `{{ hook_type }}` | Hook type being run (e.g. `pre-start`, `pre-merge`) | | | `{{ hook_name }}` | Hook command name (if named) | | | `{{ args }}` | Tokens forwarded from the CLI β€” see [Running Hooks Manually](#running-hooks-manually) | | user | `{{ vars. }}` | Per-branch variables from [`wt config state vars`](@/config.md#wt-config-state-vars) | The `repo` variables (`repo`, `repo_path`, `owner`, `remote_repo`, `primary_worktree_path`, `default_branch`, `remote`, `remote_url`) are constant across the whole repository β€” `default_branch` is the same in every worktree. The `active` variables (`branch`, `worktree_path`, `worktree_name`, `commit`, `short_commit`, `upstream`) vary per worktree. Bare variables (`branch`, `worktree_path`, `commit`) refer to the branch the operation acts on: the destination for switch/create, the source for merge/remove. `base` and `target` give the other side: | Operation | Bare vars | `base` | `target` | |-----------|-----------|--------|----------| | switch/create | destination | where you came from | = bare vars | | commit (during merge/squash) | worktree being squashed | = bare vars | integration target | | merge | feature being merged | = bare vars | merge target | | remove | branch being removed | = bare vars | where you end up | All hooks share the same perspective β€” `{{ branch | hash_port }}` produces the same port in `post-start` and `post-remove`. `cwd` is the worktree root where the hook command runs. It equals `worktree_path` except in three cases: - `pre-switch`: hook runs in the source worktree; `worktree_path` is the destination - `post-remove`: the active worktree is gone, so the hook runs in the primary worktree - `post-merge` with removal: the active worktree is gone, so the hook runs in the target worktree Undefined variables error β€” use conditionals or defaults for optional behavior: ```toml [pre-start] # Rebase onto upstream if tracking a remote branch (e.g., wt switch --create feature origin/feature) sync = "{% if upstream %}git fetch && git rebase {{ upstream }}{% endif %}" ``` Run any hook-firing command with `-v` to see the resolved variables for the actual invocation β€” each hook prints a `template variables:` block showing every in-scope variable and its value (`(unset)` for conditional vars that didn't populate, like `target_worktree_path` during `wt switch -`). Aliases do the same under `-v`: `wt -v ` prints the alias's in-scope variables before the pipeline runs. Variables use dot access and the `default` filter for missing keys. JSON object/array values are parsed automatically, so `{{ vars.config.port }}` works when the value is `{"port": 3000}`: ```toml [post-start] dev = "ENV={{ vars.env | default('development') }} npm start -- --port {{ vars.config.port | default('3000') }}" ``` ## Worktrunk filters Templates support Jinja2 filters for transforming values: | Filter | Example | Description | |--------|---------|-------------| | `sanitize` | `{{ branch \| sanitize }}` | Replace `/` and `\` with `-` | | `sanitize_db` | `{{ branch \| sanitize_db }}` | Database-safe identifier with hash suffix (`[a-z0-9_]`, max 48 chars) | | `sanitize_hash` | `{{ branch \| sanitize_hash }}` | Filesystem-safe name with hash suffix for uniqueness | | `hash` | `{{ branch \| hash }}` | 3-character base36 digest of the input | | `hash_port` | `{{ branch \| hash_port }}` | Hash to port 10000-19999 | | `dirname` | `{{ repo_path \| dirname }}` | Strip the last path component (`/a/b/c` β†’ `/a/b`) | | `basename` | `{{ repo_path \| basename }}` | Keep only the last path component (`/a/b/c` β†’ `c`) | | `codename(n)` | `{{ branch \| codename(2) }}` | Deterministic friendly words | The `sanitize_db` filter produces database-safe identifiers β€” lowercase alphanumeric and underscores, no leading digits, with a 3-character hash suffix to avoid collisions and reserved words. The `sanitize_hash` filter produces a filesystem-safe name and appends a 3-character hash suffix when sanitization changed the input, so distinct originals never collide β€” already-safe names pass through unchanged. The `codename(n)` filter produces deterministic friendly names from an input string: `codename(1)` returns a noun, `codename(2)` returns `adjective-noun`, and higher counts add more adjectives. The pool is large (~1.26M combinations for `codename(2)`), so it usually stands alone as a worktree leaf: ```toml # Friendly branch-derived worktree names, e.g. myproject.malleable-opah worktree-path = "{{ repo_path }}/../{{ repo }}.{{ branch | codename(2) }}" ``` When you want both a friendly name and the original branch identity in the path, put the branch name in a parent directory: ```toml worktree-path = "{{ repo_path }}/../worktrees/{{ branch | sanitize }}/{{ branch | codename(2) }}" ``` The `hash` filter is the bare 3-character base36 digest, useful for composing your own truncate-with-collision-avoidance recipes when an output budget is tight (e.g., Unix socket paths capped at 107 bytes): ```toml # Truncated branch slug + hash: collisions remain disambiguated even when prefixes match worktree-path = "/tmp/{{ (branch | sanitize)[:20] }}_{{ branch | sanitize | hash }}" ``` The `dirname` and `basename` filters traverse paths. They're useful for bare repos in a hidden directory like `myproject/.git`, where `{{ repo }}` resolves to `.git`: ```toml # Place worktrees as siblings of the bare repo, named `.` worktree-path = "{{ repo_path }}/../{{ repo_path | dirname | basename }}.{{ branch | sanitize }}" ``` The `hash_port` filter is useful for running dev servers on unique ports per worktree: ```toml [post-start] dev = "npm run dev -- --host {{ branch }}.localhost --port {{ branch | hash_port }}" ``` Hash any string, including concatenations: ```toml # Unique port per repo+branch combination dev = "npm run dev --port {{ (repo ~ '-' ~ branch) | hash_port }}" ``` Variables are shell-escaped automatically β€” quotes around `{{ ... }}` are unnecessary and can cause issues with special characters. ## Worktrunk functions Templates also support functions for dynamic lookups: | Function | Example | Description | |----------|---------|-------------| | `worktree_path_of_branch(branch)` | `{{ worktree_path_of_branch("main") }}` | Look up the path of a branch's worktree | The `worktree_path_of_branch` function returns the filesystem path of a worktree given a branch name, or an empty string if no worktree exists for that branch. This is useful for referencing files in other worktrees: ```toml [pre-start] # Copy config from main worktree setup = "cp {{ worktree_path_of_branch('main') }}/config.local {{ worktree_path }}" ``` ## JSON context Hooks receive all template variables as JSON on stdin, enabling complex logic that templates can't express: ```toml [pre-start] setup = "python3 scripts/pre-start-setup.py" ``` ```python import json, sys, subprocess ctx = json.load(sys.stdin) if ctx['branch'].startswith('feature/') and 'backend' in ctx['repo']: subprocess.run(['make', 'seed-db']) ``` ## Copying untracked files One specific command worth calling out: [`wt step copy-ignored`](@/step.md#wt-step-copy-ignored). Git worktrees share the repository but not untracked files, and this copies gitignored files between worktrees: ```toml [post-start] copy = "wt step copy-ignored" ``` # Running Hooks Manually `wt hook ` runs hooks on demand β€” useful for testing during development, running in CI pipelines, or re-running after a failure. {{ terminal(cmd="wt hook pre-merge # Run all pre-merge hooks|||wt hook pre-merge test # Run hooks named __WT_QUOT__test__WT_QUOT__ from both sources|||wt hook pre-merge test build # Run hooks named __WT_QUOT__test__WT_QUOT__ and __WT_QUOT__build__WT_QUOT__|||wt hook pre-merge user: # Run all user hooks|||wt hook pre-merge project: # Run all project hooks|||wt hook pre-merge user:test # Run only user's __WT_QUOT__test__WT_QUOT__ hook|||wt hook pre-merge --yes # Skip approval prompts (for CI)|||wt hook pre-start --branch=feature/test # Override a template variable|||wt hook pre-merge -- --extra args # Forward tokens into __WT_OPEN__ args __WT_CLOSE__") }} The `user:` and `project:` prefixes filter by source. Use `user:` or `project:` alone to run all hooks from that source, or `user:name` / `project:name` to run a specific hook. {% terminal(cmd="wt hook pre-merge") %} β—Ž Running pre-merge **project:test** cargo test Finished test [unoptimized + debuginfo] target(s) in 0.12s Running unittests src/lib.rs (target/debug/deps/worktrunk-abc123) running 18 tests test auth::tests::test_jwt_decode ... ok test auth::tests::test_jwt_encode ... ok test auth::tests::test_token_refresh ... ok test auth::tests::test_token_validation ... ok test result: ok. 18 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.08s β—Ž Running pre-merge **project:lint** cargo clippy Checking worktrunk v0.1.0 Finished dev [unoptimized + debuginfo] target(s) in 1.23s {% end %} {% terminal(cmd="wt hook post-start") %} β—Ž Running post-start: project @ ~/acme {% end %} ## Passing values `--KEY=VALUE` binds `KEY` whenever `{{ KEY }}` appears in any command of the hook β€” the same smart-routing rule `wt ` uses. Built-in variables can be overridden: `--branch=foo` sets `{{ branch }}` inside hook templates (the worktree's actual branch doesn't move). Hyphens in keys become underscores: `--my-var=x` sets `{{ my_var }}`. Any `--KEY=VALUE` whose key isn't referenced by a hook template forwards into `{{ args }}` as a literal `--KEY=VALUE` token. Tokens after `--` also forward into `{{ args }}` verbatim. `{{ args }}` renders as a space-joined, shell-escaped string; index with `{{ args[0] }}`, loop with `{% for a in args %}…{% endfor %}`, count with `{{ args | length }}`. The long form `--var KEY=VALUE` is deprecated but still supported. It force-binds regardless of whether any hook template references `KEY` β€” useful when a template only references the key conditionally (e.g. `{% if override %}…{% endif %}`). # Recipes - [Eliminate cold starts](@/tips-patterns.md#eliminate-cold-starts): `wt step copy-ignored` in `post-start` shares build caches and dependencies; use a `[[post-start]]` pipeline when a later hook depends on the copy - [Dev server per worktree](@/tips-patterns.md#dev-server-per-worktree): `wt step tether` in `post-start` runs the dev server and kills its whole process group when the worktree is removed, with optional subdomain routing - [Database per worktree](@/tips-patterns.md#database-per-worktree): a `post-start` pipeline stores container name, port, and connection string as [per-branch vars](@/config.md#wt-config-state-vars) that later hooks reference - [Progressive validation](@/tips-patterns.md#progressive-validation): quick lint/typecheck in `pre-commit`, expensive tests and builds in `pre-merge` - [Target-specific hooks](@/tips-patterns.md#target-specific-hooks): branch on `{{ target }}` in `post-merge` for per-environment deploys ## See also - [`wt merge`](@/merge.md) β€” Runs hooks automatically during merge - [`wt switch`](@/switch.md) β€” Runs pre-start/post-start hooks on `--create` - [`wt config approvals`](@/config.md#wt-config-approvals) β€” Manage approvals - [`wt config state logs`](@/config.md#wt-config-state-logs) β€” Access background hook logs ## Command reference {% terminal() %} wt hook - Run configured hooks Usage: **wt hook** [OPTIONS] <COMMAND> **Commands:** **show** Show configured hooks **pre-switch** Run pre-switch hooks **post-switch** Run post-switch hooks **pre-start** Run pre-start hooks **post-start** Run post-start hooks **pre-commit** Run pre-commit hooks **post-commit** Run post-commit hooks **pre-merge** Run pre-merge hooks **post-merge** Run post-merge hooks **pre-remove** Run pre-remove hooks **post-remove** Run post-remove hooks **Options:** **-h**, **--help** Print help (see a summary with '-h') **Global Options:** **-C** <path> Working directory for this command **--config** <path> User config file path **--config-set** <toml> Override config with inline TOML, e.g. --config-set list.full=true (repeatable) **-v**, **--verbose**... Verbose output (-v: info logs + hook/alias template variables on stderr; -vv: also debug logs and raw subprocess output written to .git/wt/logs/). Set WORKTRUNK_VERBOSE=0|1|2 to apply the same level everywhere β€” including shell completion, which no flag can reach **-y**, **--yes** Skip approval prompts {% end %} --- ## File: docs/content/list.md +++ title = "wt list" description = "List worktrees and their status." weight = 11 [extra] group = "Commands" +++ List worktrees and their status. Shows uncommitted changes, divergence from the default branch and remote, and optional CI status and LLM summaries.
The table renders progressively: branch names, paths, and commit hashes appear immediately, then status, divergence, and other columns fill in as background git operations complete. ## Full mode `--full` adds the two columns that reach off-machine: [CI status](#ci-status) (GitHub/GitLab pipeline pass/fail, over the network) and [LLM-generated summaries](#llm-summaries) of each branch's changes. The `main…±` line diffs are local git, so they show by default. ## Examples List all worktrees: {% terminal(cmd="wt list") %} **Branch** **Status** **HEADΒ±** **main↕** **main…±** **Remoteβ‡…** **Commit** **Age** **Message** @ feature-api + ↕⇑ +54 -5 ↑4 ↓1 +234 -24 ⇑3 6814f02 30m Add API tests ^ main ^β‡… ⇑1 ⇣1 41ee083 4d Merge fix-auth: h… + fix-auth ↕| ↑2 ↓1 +25 -11 | b772e68 5h Add secure token… + fix-typos _| | 41ee083 4d Merge fix-auth: h… β—‹ Showing 4 worktrees, 1 with changes, 2 ahead, 1 column hidden {% end %} Include CI status and LLM summaries: {% terminal(cmd="wt list --full") %} **Branch** **Status** **HEADΒ±** **main↕** **main…±** **Summary** **Remoteβ‡…** **CI** **Commit** @ feature-api + ↕⇑ +54 -5 ↑4 ↓1 +234 -24 Refactor API to REST architecture with middleware ⇑3 #412 6814f02 ^ main ^β‡… ⇑1 ⇣1 # 41ee083 + fix-auth ↕| ↑2 ↓1 +25 -11 Harden auth with constant-time token validation | #408 b772e68 + fix-typos _| | #410 41ee083 β—‹ Showing 4 worktrees, 1 with changes, 2 ahead, 3 columns hidden {% end %} Include branches that don't have worktrees: {% terminal(cmd="wt list --branches --full") %} **Branch** **Status** **HEADΒ±** **main↕** **main…±** **Summary** **Remoteβ‡…** **CI** **Commit** @ feature-api + ↕⇑ +54 -5 ↑4 ↓1 +234 -24 Refactor API to REST architecture with middleware ⇑3 #412 6814f02 ^ main ^β‡… ⇑1 ⇣1 # 41ee083 + fix-auth ↕| ↑2 ↓1 +25 -11 Harden auth with constant-time token validation | #408 b772e68 + fix-typos _| | #410 41ee083 / exp /↕ ↑2 ↓1 +137 Explore GraphQL schema and resolvers 9637922 / wip /↕ ↑1 ↓1 +33 Start API documentation b40716d β—‹ Showing 4 worktrees, 2 branches, 1 with changes, 4 ahead, 3 columns hidden {% end %} Output as JSON for scripting: {{ terminal(cmd="wt list --format=json") }} ## Columns | Column | Shows | |--------|-------| | Branch | Branch name; a detached worktree has none, so it shows its short hash in dim yellow | | Status | Compact symbols (see below) | | HEADΒ± | Uncommitted changes: +added -deleted lines | | main↕ | Commits ahead/behind default branch | | main…± | Line diffs since the merge-base (three-dot) with the default branch | | Summary | LLM-generated branch summary; requires `--full`, `summary = true`, and [`commit.generation`](@/config.md#commit) | | Remoteβ‡… | Commits ahead/behind tracking branch | | CI | PR/MR number colored by pipeline status; `--full` only | | Path | Worktree directory | | URL | Dev server URL from project config; dimmed if port is not listening | | *(custom)* | User-defined [custom columns](#custom-columns) from `[list.custom-columns]` user config | | Commit | Short hash, abbreviated per `core.abbrev` | | Age | Time since last commit | | Message | Last commit message (truncated) | The `main` header label is used regardless of the default branch's actual name. `main↕` and `main…±` measure against the default branch's upstream tip when the local copy lags it β€” so in a fork whose local `main` trails `origin/main`, a branch reads as ahead of the real mainline, not of a stale local checkout. The `↑`/`↓`/`↕` Status symbols derive from these counts, so they track the upstream tip too. ### Gutter The leftmost column marks each row by physical presence, from most present to least: | Symbol | Meaning | |--------|---------| | `@` | Current worktree | | `^` | Primary worktree (the repo's home worktree) | | `+` | Other worktree | | `/` | Local branch without a worktree (`--branches`) | | `\|` | Remote branch, not present locally until fetched (`--remotes`) | ### CI status The CI column shows the branch's open PR/MR β€” `#3035` on GitHub, Gitea, and Azure DevOps, `!3035` on GitLab β€” colored by pipeline status, or a bare `#` when no number is available (e.g. branch workflows without a PR/MR). One color folds two JSON fields: green/blue/red/yellow/gray are `ci.status`; magenta/cyan are `ci.review_state`. The `Value` column is the matching JSON string from `--format=json`: | Indicator | Value | Meaning | |-----------|-------|---------| | # green | `"passed"` | All checks passed | | # blue | `"running"` | Checks in progress | | # red | `"failed"` | One or more checks failed | | # yellow | `"conflicts"` | Merge conflicts with the target branch | | # gray | `"no-ci"` | No PR/MR, or no checks configured | | ⚠ yellow | `"error"` | CI status could not be fetched (rate limit, network, etc.) | | # magenta | `"changes_requested"` | A reviewer requested changes | | # cyan | `"pending"` | A review is required (e.g. branch protection) but not yet given | | (blank) | `ci` absent | No upstream, or no PR/MR and no branch workflow | The two remaining `ci.review_state` values have no indicator of their own: `"draft"` only dims the cell and `"approved"` leaves the color unchanged. Color precedence resolves the fold: changes-requested (magenta) outranks running checks β€” waiting can't clear it β€” while an outstanding required review (cyan) only recolors an otherwise green or quiet branch. Cool colors mean waiting, warm colors mean act. An approved PR, or one with no review signal at all (no required reviewers and no reviews), keeps its plain `ci.status` color β€” `ci.review_state` is then `"approved"` or absent, respectively. GitLab MR data carries only `"pending"` and `"draft"` β€” no approved or changes-requested signal. CI cells are clickable links to the PR or pipeline page, and appear dimmed for a draft PR/MR (`"draft"`) or when unpushed local changes make the status stale (`ci.stale`). PRs/MRs are checked first, then branch workflows/pipelines for branches with an upstream. Local-only branches show blank; remote-only branches β€” visible with `--remotes` β€” get CI status detection. Results are cached for 30-60 seconds; use `wt config state` to view or clear. ### LLM summaries Reuses the [`commit.generation`](@/config.md#commit) command β€” the same LLM that generates commit messages. Enable with `summary = true` in `[list]` config; requires `--full`. Results are cached until the branch's diff changes. ### Custom columns Each `[list.custom-columns]` entry in user config adds a column: the key is the header, the template renders each row's cell. Templates read two per-branch namespaces β€” `{{ vars.* }}`, stored with [`wt config state vars set`](@/config.md#wt-config-state-vars), and `{{ git.branch.* }}`, the branch's own git config under `branch..*` (a `jira` key you set yourself, or the git-native `description`) β€” useful for tracking what each of many (often agent-driven) branches is for: ```toml [list.custom-columns.Ticket] template = "{{ vars.ticket }}" ``` A column that renders empty for every row is dropped from the table. Templates, widths, and drop priority: [custom columns config](@/config.md#custom-columns). ## Status symbols The Status column packs several subcolumns, left to right, each mapping to a field in `--format=json`. Working-tree flags are independent and co-occur β€” any combination shows at once. The other subcolumns are mutually exclusive: each shows a single symbol, the highest-priority state in top-to-bottom table order, and is blank when nothing applies. ### Working tree Independent flags from `git status`; several can show at once (e.g. `+!?`). Each maps to a boolean in the `working_tree` object: | Symbol | working_tree | Meaning | |--------|--------------|---------| | `+` | `staged` | Staged files | | `!` | `modified` | Modified files (unstaged) | | `?` | `untracked` | Untracked files | `working_tree` also reports `renamed` and `deleted`, which have no dedicated symbol in the column. ### Worktree An in-progress git operation, a worktree-location attribute, or a branch with no worktree. One symbol shows, highest priority first (`✘ > ↻ > ⊟ > ⊞ > βš‘ > /`): | Symbol | JSON | Meaning | |--------|------|---------| | `✘` | `operation_state` `"conflicts"` | Merge conflicts | | `↻` | `operation_state` `"rebase"`, `"merge"`, `"cherry_pick"`, `"revert"`, `"bisect"` | A git operation is in progress; `git status` names it | | `⊟` | `worktree.state` `"prunable"` | Prunable (worktree directory missing) | | `⊞` | `worktree.state` `"locked"` | Locked worktree | | `βš‘` | `worktree.state` `"duplicate_branch"` | Branch checked out in more than one worktree, so `wt` resolves it to whichever git lists first; every worktree on the branch is flagged | | `βš‘` | `worktree.state` `"branch_worktree_mismatch"` | Worktree isn't at the path its branch implies β€” including a detached one, which has no branch to imply a path and so is never at home | | `/` | `kind` `"branch"` | Branch without a worktree (no `worktree` object) | ### Default branch The single highest-priority state describing the branch's relation to the default branch; blank when none applies (a normal up-to-date branch). Each symbol is one `main_state` value: | Symbol | main_state | Meaning | |--------|------------|---------| | `^` | `"is_main"` | The main worktree (the repo's home worktree) | | `βˆ…` | `"orphan"` | No common ancestor with the default branch | | `_` | `"empty"` | Same commit as the default branch, working tree clean β€” safe to remove; row dimmed | | `βŠ‚` | `"integrated"` | Content [integrated](@/remove.md#branch-cleanup) into the default branch or merge target via different history; the matching check is in `integration_reason`; row dimmed | | `βœ—` | `"would_conflict"` | Merging into the default branch would conflict (simulated with `git merge-tree`) and the branch isn't already integrated; with `--full`, the check includes uncommitted changes | | `–` | `"same_commit"` | Same commit as the default branch, but with uncommitted changes | | `↕` | `"diverged"` | Both ahead of and behind the default branch | | `↑` | `"ahead"` | Has commits the default branch doesn't | | `↓` | `"behind"` | Missing commits the default branch has | Rows are dimmed when [safe to delete](@/remove.md#branch-cleanup) β€” `_` (`"empty"`) or `βŠ‚` (`"integrated"`). ### Remote Relation to the tracking branch, derived from the `remote.ahead` / `remote.behind` counts; blank when there is no upstream: | Symbol | remote | Meaning | |--------|--------|---------| | `\|` | `ahead` 0, `behind` 0 | In sync with remote | | `⇑` | `ahead` > 0 | Ahead of remote | | `⇣` | `behind` > 0 | Behind remote | | `β‡…` | `ahead` > 0, `behind` > 0 | Diverged from remote | ### Placeholder symbols These appear across all columns while the table is loading: | Symbol | Meaning | |--------|---------| | `Β·` | Data is loading, or collection timed out / branch too stale | --- ## JSON output `--format=json` emits structured data in one of two schemas while the format migrates: `[list] json-schema = 2` selects the envelope format below, `= 1` the original bare-array format. Unset emits schema 1 with a warning (`wt config update` adopts `= 2`); a future release flips the default to schema 2 and later removes schema 1. ### Schema 2 One envelope object. Items carry independent facts; rendered strings (including the collapsed Status value) live under `display`: ```json { "schema": 2, "repo": { "default_branch": "main", "forge": {"url": "https://github.com/org/repo", "provider": "github", "host": "github.com", "owner": "org", "name": "repo", "remote": "origin"} }, "collected": {"ci": false, "summary": false}, "items": [ { "branch": "feature", "head": {"sha": "05a4a45d…", "short_sha": "05a4a45", "subject": "Add login page", "committed_at": "2025-01-01T08:00:00Z"}, "worktree": {"path": "/home/user/repo.feature", "main": false, "current": true, "previous": false, "detached": false, "branch_mismatch": false, "duplicate_branch": false, "changes": {"staged": false, "modified": true, "untracked": false, "renamed": false, "deleted": false, "conflicted": false, "diff": {"added": 10, "deleted": 2}}}, "default_branch": {"ahead": 3, "behind": 1, "diff": {"added": 50, "deleted": 20}, "orphan": false, "integration": null, "merge_conflicts": false}, "upstream": {"remote": "origin", "branch": "feature", "ahead": 0, "behind": 2}, "display": {"state": "diverged", "symbols": "!↕", "statusline": "feature …"} } ] } ``` How "no value" reads: - **Absent** β€” nothing to report: not applicable (`worktree` on a branch-only row), not requested this run (the envelope's `collected` records what was), or determined-empty (no PR, no lock, not integrated). - **`null`** β€” requested but not determined: a task timed out, the branch was too stale for the expensive checks, or a forge fetch failed. This is the JSON form of the table's `Β·` placeholder. jq treats absent and `null` identically in path expressions, so filters need no null checks; `has()` distinguishes the two when it matters. Item fields: | Field | Description | |-------|-------------| | `branch` | Branch name; null for a detached-HEAD worktree. Remote rows carry the bare name with the remote in `remote` | | `remote` | Remote name, present only on remote-only branch rows | | `head` | `{sha, short_sha, subject, committed_at}`; null for unborn branches. `committed_at` is RFC 3339 UTC | | `worktree` | `{path, main, current, previous, detached, locked, prunable, branch_mismatch, duplicate_branch, operation, changes}`; absent on branch-only rows. `locked`/`prunable` are `{reason}` objects and can co-occur; `operation` is `"rebase"` or `"merge"`; `changes` holds the five working-tree flags plus `conflicted` and `diff {added, deleted}` | | `default_branch` | Relation to the default branch: `{ahead, behind, diff, orphan, integration, merge_conflicts}`; absent on the default branch itself. `integration.reason` is one of `same_commit`, `ancestor`, `no_added_changes`, `trees_match`, `merge_adds_nothing`, `patch_id_match`; a dirty tree skips the checks, leaving `integration` null | | `upstream` | Tracking branch: `{remote, branch, ahead, behind}`; absent when none is configured | | `pr` | Open PR/MR: `{number, url, review, mergeable, repo}`; collected with `--full`. `review` uses the schema 1 `ci.review_state` vocabulary; `mergeable` is false when the forge reports conflicts, null otherwise | | `checks` | CI pipeline: `{status, source, stale}`; collected with `--full`. `status` is `passed`, `running`, or `failed` β€” null when a conflicts report masks it | | `dev_server` | `{url, listening}` from the project's `list.url` template | | `summary` | LLM branch summary; needs `--full`, `[list] summary = true`, and a `[commit.generation]` command | | `vars` | Per-branch variables from [`wt config state vars`](@/config.md#wt-config-state-vars) | | `display` | Rendered strings: `state` (schema 1's `main_state` vocabulary), `symbols`, `statusline` (with ANSI colors and OSC 8 hyperlinks), `columns` (custom-column cells keyed by header) | Schema 1 names map directly: `commit` β†’ `head`, `working_tree` β†’ `worktree.changes`, `main` + `main_state` β†’ `default_branch` + `display.state`, `remote` β†’ `upstream`, `ci` β†’ `pr` + `checks`, `url` + `url_active` β†’ `dev_server`, `statusline`/`symbols`/`columns` β†’ `display.*`, and the per-item `repo` moves to the envelope's `repo.forge`. {{ terminal(cmd="# Current worktree path (for scripts)|||wt list --format=json | jq -r '.items[] | select(.worktree.current) | .worktree.path'||||||# Branches with uncommitted changes|||wt list --format=json | jq '.items[] | select(.worktree.changes.modified)'||||||# Integrated branches (safe to remove)|||wt list --format=json | jq '.items[] | select(.display.state == __WT_QUOT__integrated__WT_QUOT__ or .display.state == __WT_QUOT__empty__WT_QUOT__) | .branch'||||||# Worktrees ahead of upstream (needs pushing)|||wt list --format=json | jq '.items[] | select(.upstream.ahead > 0) | .branch'") }} A JSON Schema for the envelope is published at [worktrunk.dev/schema/list-v2.json](https://worktrunk.dev/schema/list-v2.json). It describes what `wt` writes, so a field the absence rule can omit is optional there rather than required-and-null. ### Schema 1 The original bare-array format, and the default while unset: {{ terminal(cmd="# Current worktree path (for scripts)|||wt list --format=json | jq -r '.[] | select(.is_current) | .path'||||||# Branches with uncommitted changes|||wt list --format=json | jq '.[] | select(.working_tree.modified)'||||||# Worktrees with merge conflicts|||wt list --format=json | jq '.[] | select(.operation_state == __WT_QUOT__conflicts__WT_QUOT__)'||||||# Branches ahead of main (needs merging)|||wt list --format=json | jq '.[] | select(.main.ahead > 0) | .branch'||||||# Integrated branches (safe to remove)|||wt list --format=json | jq '.[] | select(.main_state == __WT_QUOT__integrated__WT_QUOT__ or .main_state == __WT_QUOT__empty__WT_QUOT__) | .branch'||||||# Branches without worktrees|||wt list --format=json --branches | jq '.[] | select(.kind == __WT_QUOT__branch__WT_QUOT__) | .branch'||||||# Worktrees ahead of remote (needs pushing)|||wt list --format=json | jq '.[] | select(.remote.ahead > 0) | {branch, ahead: .remote.ahead}'||||||# Stale CI (local changes not reflected in CI)|||wt list --format=json --full | jq '.[] | select(.ci.stale) | .branch'") }} **Fields:** | Field | Type | Description | |-------|------|-------------| | `branch` | string/null | Branch name (null for detached HEAD) | | `path` | string | Worktree path (absent for branches without worktrees) | | `kind` | string | `"worktree"` or `"branch"` | | `commit` | object | Commit info (see below) | | `working_tree` | object | Working tree state (see below) | | `main_state` | string | Relation to the default branch (see below) | | `integration_reason` | string | Why branch is integrated (see below) | | `operation_state` | string | `"conflicts"`, `"rebase"`, or `"merge"` (see [Worktree](#worktree)); absent when clean | | `main` | object | Relationship to the default branch (see below); absent when is_main | | `remote` | object | Tracking branch info (see below); absent when no tracking | | `worktree` | object | Worktree metadata (see below) | | `is_main` | boolean | Is the main worktree | | `is_current` | boolean | Is the current worktree | | `is_previous` | boolean | Previous worktree from wt switch | | `ci` | object | CI status (see below); `--full` only, then absent when no PR/MR or branch workflow | | `repo_url` | string | Repository web URL derived from the primary remote; absent when the remote URL cannot be parsed | | `repo` | object | Structured repository metadata (see below); includes `remote` | | `url` | string | Dev server URL from project config; absent when not configured | | `url_active` | boolean | Whether the URL's port is listening; absent when not configured | | `summary` | string | LLM-generated branch summary; `--full` only, then absent when not configured or no summary | | `statusline` | string | Pre-formatted status with colors and links | | `symbols` | string | Raw status symbols without colors (e.g., `"!?↓"`) | | `vars` | object | Per-branch variables from [`wt config state vars`](@/config.md#wt-config-state-vars) (absent when empty) | | `columns` | object | Rendered [custom column](#custom-columns) values keyed by header; empty cells omitted (absent when none configured) | ### Commit object | Field | Type | Description | |-------|------|-------------| | `sha` | string | Full commit SHA (40 chars) | | `short_sha` | string | Short commit SHA, abbreviated per `core.abbrev` (auto-extends for ambiguous prefixes) | | `message` | string | Commit message (first line) | | `timestamp` | number | Unix timestamp | ### working_tree object The five change flags map to the [Working tree](#working-tree) symbols (`renamed` and `deleted` have none of their own): | Field | Type | Description | |-------|------|-------------| | `staged` | boolean | Has staged files | | `modified` | boolean | Has modified files (unstaged) | | `untracked` | boolean | Has untracked files | | `renamed` | boolean | Has renamed files | | `deleted` | boolean | Has deleted files | | `diff` | object | Lines changed vs HEAD: `{added, deleted}` | ### main object | Field | Type | Description | |-------|------|-------------| | `ahead` | number | Commits ahead of the default branch | | `behind` | number | Commits behind the default branch | | `diff` | object | Lines changed vs the default branch: `{added, deleted}` | ### remote object `ahead` / `behind` drive the [Remote](#remote) divergence symbol: | Field | Type | Description | |-------|------|-------------| | `name` | string | Remote name (e.g., `"origin"`) | | `branch` | string | Remote branch name | | `ahead` | number | Commits ahead of remote | | `behind` | number | Commits behind remote | ### worktree object Present only for worktree-kind items. `state` is the worktree-location attribute β€” see [Worktree](#worktree) for its symbols: | Field | Type | Description | |-------|------|-------------| | `state` | string | `"branch_worktree_mismatch"`, `"duplicate_branch"`, `"prunable"`, or `"locked"` (absent when normal) | | `reason` | string | Reason for locked/prunable state | | `detached` | boolean | HEAD is detached | ### ci object | Field | Type | Description | |-------|------|-------------| | `status` | string | CI status (see below) | | `source` | string | `"pr"` (PR/MR) or `"branch"` (branch workflow) | | `number` | integer | PR/MR number; absent for branch workflows | | `stale` | boolean | Local HEAD differs from remote (unpushed changes) | | `url` | string | URL to the PR/MR page | | `repo_url` | string | Web URL of the repo the PR/MR targets (the upstream for fork PRs); absent when `url` is absent or unrecognized | | `repo` | object | Structured metadata for the repository the PR/MR targets; never includes `remote` | | `review_state` | string | Review state (see below); absent when the forge reports no review signal | ### repo object Top-level `repo` describes the local checkout's repository as derived from the primary remote. `ci.repo` describes the repository targeted by the PR/MR URL in `ci.url` (for fork PRs, this is the upstream target). Existing `repo_url` and `ci.repo_url` fields remain available and carry the same URL as `repo.url` / `ci.repo.url`. | Field | Type | Description | |-------|------|-------------| | `url` | string | Repository web URL | | `provider` | string | `"github"`, `"gitlab"`, `"gitea"`, `"azure-devops"`, or `"unknown"` | | `host` | string | Repository web host | | `owner` | string | Owner, organization, or namespace path | | `name` | string | Repository name | | `project` | string | Azure DevOps project name; absent for other providers | | `remote` | string | Local remote name used for top-level repo metadata; absent from `ci.repo` | ### main_state values The single highest-priority state describing the branch's relation to the default branch; absent when none applies (a normal up-to-date branch). Each value is one Default-branch symbol β€” see [Default branch](#default-branch) for the symbol and the full meaning of each value (`"is_main"`, `"orphan"`, `"empty"`, `"integrated"`, `"would_conflict"`, `"same_commit"`, `"diverged"`, `"ahead"`, `"behind"`). ### integration_reason values Set only when `main_state == "integrated"` (the `βŠ‚` symbol), recording which check matched. Checks run cheapest-first and the first match wins. JSON-only β€” every reason renders as the same `βŠ‚`: | Value | Meaning | |-------|---------| | `"ancestor"` | Branch HEAD is an ancestor of the default branch, which has moved past it | | `"no-added-changes"` | The three-dot diff (`main...branch`) is empty β€” no file changes beyond the merge-base | | `"trees-match"` | Different history, but the branch's tree is identical to the default branch's | | `"merge-adds-nothing"` | The branch has changes, but merging them leaves the default branch's tree unchanged (e.g. a squash merge where the target advanced on other files) | | `"patch-id-match"` | The branch's squashed diff matches a single commit on the default branch (e.g. a GitHub/GitLab squash merge) | ### ci.status and ci.review_state values The [CI status](#ci-status) section above is the single source for both fields: the table maps each colored value, and the notes below it cover `"draft"` and `"approved"`. `ci.status` is one of `"passed"`, `"running"`, `"failed"`, `"conflicts"`, `"no-ci"`, `"error"`; `ci.review_state` is one of `"changes_requested"`, `"pending"`, `"draft"`, `"approved"`, absent when the forge reports no review signal. The vocabulary matches Claude Code's statusline `pr.review_state` field. Missing a field that would be generally useful? [Open an issue](https://github.com/max-sixty/worktrunk/issues). ## See also - [`wt switch`](@/switch.md) β€” Switch worktrees or open interactive picker ## Command reference {% terminal() %} wt list - List worktrees and their status Usage: **wt list** [OPTIONS] **wt list** <COMMAND> **Commands:** **statusline** Single-line status for the current worktree **Options:** **--format** <FORMAT> Output format [default: table] [possible values: table, json] **--branches** Include branches without worktrees **--remotes** Include remote branches **--full** Show CI status and LLM summaries **--progressive** Show fast info immediately, update with slow info Displays local data (branches, paths, status) first, then updates with remote data (CI, upstream) as it arrives. Use --no-progressive to force buffered rendering. Auto-enabled for TTY. **-h**, **--help** Print help (see a summary with '-h') **Global Options:** **-C** <path> Working directory for this command **--config** <path> User config file path **--config-set** <toml> Override config with inline TOML, e.g. --config-set list.full=true (repeatable) **-v**, **--verbose**... Verbose output (-v: info logs + hook/alias template variables on stderr; -vv: also debug logs and raw subprocess output written to .git/wt/logs/). Set WORKTRUNK_VERBOSE=0|1|2 to apply the same level everywhere β€” including shell completion, which no flag can reach **-y**, **--yes** Skip approval prompts {% end %} # Subcommands ## wt list statusline Single-line status for the current worktree. The line carries the same cells as the worktree's row in `wt list`. A stale CI status cache makes it reach the network for a second or two, so it fits a statusline the host renders in the background β€” Claude Code's, a `tmux` status bar β€” better than a prompt the shell blocks on. Want it fast enough for a synchronous prompt? [Open an issue](https://github.com/max-sixty/worktrunk/issues). ### Output formats - `table` (default): `branch status HEADΒ± main↕ main…± Remoteβ‡… CI URL` - `json`: A one-entry array in the `wt list --format=json` schema - `claude-code`: the `table` cells, preceded by `dir` and followed by `model context pace` A cell with nothing to show is left out rather than blanked, so most lines are shorter than that; `claude-code` also drops `branch` where `dir` already ends in `.`. A line that still overruns the terminal drops whole cells, least important first, starting with the dev server URL. The CI reference links to its PR/MR, and a dev server URL carrying a port shows as `:3000` linking to the URL in full, dim until something answers on that port. Both are underlined, which is what marks them as clickable. They are OSC 8 links, and a terminal that doesn't support those discards the escape, leaving the underlined text unclickable. ### Claude Code mode `--format=claude-code` reads JSON context from stdin (`.workspace.current_dir` is required; the rest are optional): - `.workspace.current_dir` β€” working directory - `.model.display_name` β€” model name - `.context_window.used_percentage` β€” context usage (0–100), rendered as `πŸŒ” 65%`, the moon waning πŸŒ•β†’πŸŒ‘ as context fills - `.rate_limits.{five_hour,seven_day}.used_percentage` β€” rate-limit window usage (0–100) - `.rate_limits.{five_hour,seven_day}.resets_at` β€” window reset time (Unix epoch seconds) The pace segment appears only when usage is likely to hit a rate limit before its window resets, and shows the higher-risk window: `2.9Γ—(Tue–Tue 5pm)` reads as 2.9Γ— the pace that would exactly fill that window. Above 90% used it shows usage instead of pace β€” `93%(Tue–Tue 5pm)` β€” near the cap, how much is left matters more than how fast it's going. "Likely" is a Bayesian forecast; early-window bursts don't trigger it. Its colour deepens with severity β€” dim, then dim-yellow, then yellow β€” as the forecast lockout (how much of the window would be spent capped) grows, so a fast pace that would only tip over near the reset stays dim rather than alarming. With `-vv`, each window's inputs and projection are logged to `.git/wt/logs/trace.log`. [Claude Code statusline setup](@/claude-code.md#statusline-claude-code-only) has the `~/.claude/settings.json` entry that feeds this mode. ### Command reference {% terminal() %} wt list statusline - Single-line status for the current worktree Usage: **wt list statusline** [OPTIONS] **Options:** **--format** <FORMAT> Output format Possible values: - **table** - **json** - **claude-code**: Claude Code statusline mode (reads context from stdin) [default: table] **-h**, **--help** Print help (see a summary with '-h') **Global Options:** **-C** <path> Working directory for this command **--config** <path> User config file path **--config-set** <toml> Override config with inline TOML, e.g. --config-set list.full=true (repeatable) **-v**, **--verbose**... Verbose output (-v: info logs + hook/alias template variables on stderr; -vv: also debug logs and raw subprocess output written to .git/wt/logs/). Set WORKTRUNK_VERBOSE=0|1|2 to apply the same level everywhere β€” including shell completion, which no flag can reach **-y**, **--yes** Skip approval prompts {% end %} --- ## File: docs/content/llm-commits.md +++ title = "LLM Commit Messages" description = "Generate commit messages from diffs using any LLM. Integrates with wt merge, wt step commit, and wt step squash." weight = 22 [extra] group = "Reference" +++ Worktrunk generates commit messages by building a templated prompt and piping it to an external command. This integrates with `wt merge`, `wt step commit`, and `wt step squash`.
## Setup Any command that reads a prompt from stdin and outputs a commit message works. Add to `~/.config/worktrunk/config.toml`: ### Claude Code ```toml [commit.generation] command = "MAX_THINKING_TOKENS=0 claude -p --no-session-persistence --model=haiku --tools='' --safe-mode --setting-sources='user' --system-prompt=''" ``` `--no-session-persistence` prevents the commit conversation from polluting `claude --continue`. `--safe-mode` keeps the run hermetic β€” no hooks, plugins, MCP, skills, or CLAUDE.md β€” while leaving authentication working normally, so setups that authenticate via `apiKeyHelper` (not just OAuth or `ANTHROPIC_API_KEY`) still get a key. `--setting-sources='user'` scopes settings to your user config so a project `.claude/settings.json` can't override auth. The remaining flags disable tools, system prompt, and thinking for fast text-only output. `--safe-mode` requires Claude Code β‰₯ 2.1.169. See [Claude Code docs](https://code.claude.com/docs/en/setup) for installation. ### Codex ```toml [commit.generation] command = "codex exec -m gpt-5.6-luna -c model_reasoning_effort='low' -c system_prompt='' --sandbox=read-only --json - | jq -sr '[.[] | select(.item.type? == \"agent_message\")] | last.item.text'" ``` Uses the fast mini model with low reasoning effort and an empty system prompt for faster output. Requires `jq` for JSON parsing. See [Codex CLI docs](https://developers.openai.com/codex/cli/). ### Other tools ```toml # opencode β€” use a fast model variant command = "opencode run -m anthropic/claude-haiku-4.5 --variant fast" # llm command = "llm -m claude-haiku-4.5" # aichat command = "aichat -m claude:claude-haiku-4.5" ``` ## Usage These examples assume a feature worktree with changes to commit. ### wt merge Squashes all changes (uncommitted + existing commits) into one commit with an LLM-generated message, then merges to the default branch: {% terminal(cmd="wt merge") %} wt merge β—Ž Squashing 3 commits into a single commit (5 files, +16)... β—Ž Generating squash commit message... **feat(auth): Implement JWT authentication system** Add comprehensive JWT token handling including validation, refresh logic, and authentication tests. βœ“ Squashed @ a1b2c3d β—Ž Merging 1 commit to **main** @ a1b2c3d (no rebase needed) * a1b2c3d feat(auth): Implement JWT authentication system auth.rs | 2 ++ auth_test.rs | 2 ++ integration_test.rs | 6 ++++++ jwt.rs | 3 +++ jwt_test.rs | 3 +++ 5 files changed, 16 insertions(+) βœ“ Merged to **main** (1 commit, 5 files, +16) β—Ž Removing **feature** worktree & branch in background (same commit as **main**, _) β—‹ Switched to worktree for **main** @ **~/repo** {% end %} ### wt step commit Stages and commits with LLM-generated message: {% terminal(cmd="wt step commit") %} wt step commit β—Ž Generating commit message and committing changes... (2 files, +26) **feat(validation): add input validation utilities** βœ“ Committed changes @ a1b2c3d {% end %} ### wt step squash Squashes branch commits into one with LLM-generated message: {% terminal(cmd="wt step squash") %} wt step squash β—Ž Squashing 3 commits into a single commit (5 files, +16)... β—Ž Generating squash commit message... **feat(auth): Implement JWT authentication system** Add comprehensive JWT token handling including validation, refresh logic, and authentication tests. βœ“ Squashed @ a1b2c3d {% end %} See [`wt merge`](@/merge.md) and [`wt step`](@/step.md) for full documentation. ## Branch summaries With `summary = true` and a `[commit.generation] command` configured, Worktrunk generates LLM branch summaries β€” one-line descriptions of each branch's changes since the default branch. Summaries appear in: - **`wt switch`** [interactive picker](@/switch.md#interactive-picker) β€” preview tab 5 - **`wt list --full`** β€” the Summary column (see [`wt list`](@/list.md#llm-summaries)) Enable in user config: ```toml [list] summary = true ``` Summaries are cached and regenerated only when the diff changes. ## Prompt templates Worktrunk uses [minijinja](https://docs.rs/minijinja/) templates (Jinja2-like syntax) to build prompts. ### Custom templates Override the defaults with inline templates: ```toml [commit.generation] command = "llm -m claude-haiku-4.5" template = """ Write a commit message for this diff. One line, under 50 chars. Branch: {{ branch }} Diff: {{ git_diff }} """ squash-template = """ Combine these {{ commit_details | length }} commits into one message: {% for c in commit_details %} - {{ c.subject }} {% endfor %} Diff: {{ git_diff }} """ ``` ### Template variables | Variable | Description | |----------|-------------| | `{{ git_diff }}` | The diff (staged changes or combined diff for squash) | | `{{ git_diff_stat }}` | Diff statistics (files changed, insertions, deletions) | | `{{ branch }}` | Current branch name | | `{{ repo }}` | Repository name | | `{{ recent_commits }}` | Recent commit subjects (for style reference) | | `{{ commit_details }}` | Commits being squashed (squash template only); each renders as its subject and exposes `.subject` / `.body` | | `{{ target_branch }}` | Merge target branch (squash template only) | | `{{ user_guidance }}` | Rendered user `template-append` fragment (see below) | | `{{ project_guidance }}` | Rendered project `template-append` fragment (see below) | ### Template syntax Templates use [minijinja](https://docs.rs/minijinja/latest/minijinja/syntax/index.html), which supports: - **Variables**: `{{ branch }}`, `{{ repo | upper }}` - **Filters**: `{{ commit_details | length }}`, `{{ repo | upper }}` - **Conditionals**: `{% if recent_commits %}...{% endif %}` - **Loops**: `{% for c in commit_details %}{{ c.subject }}{% endfor %}` - **Loop variables**: `{{ loop.index }}`, `{{ loop.length }}` - **Whitespace control**: `{%- ... -%}` strips surrounding whitespace See `wt config create --help` for the full default templates. ## Appending to the prompt `template-append` adds to the commit and squash prompts instead of replacing them. It lives in both user config (personal preferences) and project config (`.config/wt.toml`, shared so every teammate's LLM sees the same style guide). Each fragment is itself a [minijinja](https://docs.rs/minijinja/) template β€” Worktrunk renders it with the same variables as the main template (`{{ branch }}`, `{{ git_diff }}`, …), then appends the result after `