Files
max-sixty__worktrunk/docs/content/config.md
Worktrunk Bot 246c6bd919 fix(list): keep [list] columns out of the --format json plan (#3812)
Closes the `[list] columns` half of #3787, per the call in [this
comment](https://github.com/max-sixty/worktrunk/issues/3787#issuecomment-5273942067):
JSON always emits the same shape, and `list.columns` only affects the
actual columns.

Before, `--format json` planned `all_columns` (source `Default`)
*unioned* with the selection's forced-on columns, so the selection
reached JSON in one direction only — it couldn't narrow the emitted
fields, but a listed `ci` did force the forge fetch on without `--full`.
That made a presentation setting decide whether a machine-readable call
talks to GitHub, which is the thing the Neovim plugin in #3787 had to
pin `--config-set 'list.columns=[…]'` against. Now the JSON branch plans
`all_columns` alone; `--full` is the only switch for the gated data, and
it's the one a caller controls.

The table and the `wt switch` picker are untouched — a listed `ci` still
renders the CI column without `--full`, and the picker still unions the
selection in so its table matches `wt list`'s.

Only `ci` and `summary` are affected: every other column is ungated, so
`full_plan()` already covered them, and custom columns require no
background task.

**For the release note — this changes schema 1 too.** A caller with
`[list] columns = […, "ci"]` and no `--full` used to get the `ci` object
in schema-1 JSON and now won't; schema 1 has no `collected` envelope to
say why. The schema-1 `ci` row already documented `` `--full` only ``,
so the docs get *more* accurate, but the observable output changes for
anyone who was relying on the forcing path. Schema 2 reports the same
narrowing through `collected.ci`.

Docs updated in `after_long_help` (the `[list] columns` section plus the
schema-2 `pr`, `summary`, and `checks` rows — `summary` now names
`--full` alongside `[list] summary = true`, and `checks` names the
`--full` gate it shares with `pr`), with the generated mirrors,
`dev/config.example.toml`, and the `--help` snapshots regenerated. The
`CLAUDE.md` network inventory and the `collect` planning comment now
record the exemption too.

<details><summary>Test</summary>

`test_list_json_columns_selection_does_not_force_ci` in
`tests/integration_tests/list_config.rs` asserts schema 2's
`collected.ci` across three configs: unset (false), `columns =
["branch", "ci"]` without `--full` (false — the regression this fixes),
and the same with `--full` (true). `collected` records what the plan
requested rather than what a fetch returned, so the test needs no forge
and no `gh` on PATH. It sits next to
`test_list_json_ignores_columns_selection`, which owns the narrowing
direction, and `test_list_config_listed_column_overrides_full_gate`,
which owns the table's forcing behaviour and still passes unchanged.

Ran locally: full `cargo test --test integration` and `cargo test --lib
--bins`, plus `cargo clippy --all-targets` and `cargo fmt --check`. One
unrelated failure,
`test_copy_ignored_preserves_file_executable_permissions`, is a umask
artifact of this sandbox (expects `0644`, the runner's `umask 002`
produces `0664`); it touches no code in this diff.

The docs-row follow-up in df5c238 re-ran `cargo test --test integration
-- test_help test_docs_are_in_sync` (48 passed) and `cargo fmt --check`.

</details>

---------

Co-authored-by: worktrunk-bot <254187624+worktrunk-bot@users.noreply.github.com>
2026-08-15 08:00:04 -07:00

60 KiB
Raw Permalink Blame History

+++ 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:

# ~/.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:

# .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.

Examples for repo at ~/code/myproject, branch feature/auth:

Default — sibling directory (~/code/myproject.feature-auth):

worktree-path = "{{ repo_path }}/../{{ repo }}.{{ branch | sanitize }}"

Inside the repository (~/code/myproject/.worktrees/feature-auth):

worktree-path = "{{ repo_path }}/.worktrees/{{ branch | sanitize }}"

Friendly branch-derived names (~/code/myproject.malleable-opah):

worktree-path = "{{ repo_path }}/../{{ repo }}.{{ branch | codename(2) }}"

Friendly names with branch identity in a parent directory (~/code/worktrees/feature-auth/malleable-opah):

worktree-path = "{{ repo_path }}/../worktrees/{{ branch | sanitize }}/{{ branch | codename(2) }}"

Centralized worktrees directory (~/worktrees/myproject/feature-auth):

worktree-path = "~/worktrees/{{ repo }}/{{ branch | sanitize }}"

By remote owner path (~/development/max-sixty/myproject/feature/auth):

worktree-path = "~/development/{{ owner }}/{{ repo }}/{{ branch }}"

Bare repository (~/code/myproject/feature-auth):

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

[commit.generation]
command = "MAX_THINKING_TOKENS=0 claude -p --no-session-persistence --model=haiku --tools='' --safe-mode --setting-sources='user' --system-prompt=''"

Codex

[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

[commit.generation]
command = "opencode run -m anthropic/claude-haiku-4.5 --variant fast"

llm

[commit.generation]
command = "llm -m claude-haiku-4.5"

aichat

[commit.generation]
command = "aichat -m claude:claude-haiku-4.5"

See LLM commits docs for setup and Custom prompt templates for template customization.

Command config

List

Persistent flag values for wt list. Override on command line as needed.

[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 (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, 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.

[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.
  • {{ git.branch.* }} — the branch's own git config under branch.<name>.*, 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.<name>.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:

[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.

[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.

[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.

[remove]
delete-branch = true   # Delete branch after removal (--no-delete-branch to keep)

Switch

[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

[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 list them. User config and project config exclusions are combined.

Aliases

Command templates that run as wt <name>. See the Extending Worktrunk guide for usage and flags.

[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 [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 shared with teammates.

Entries are keyed by project identifier — <host>/<owner>/<repo> 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.

[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.

# 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 block, for a self-hosted host whose name carries no github, gitlab, or gitea for detection to read.

[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. 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:

# 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 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)

Default template:

[commit.generation]
template = """
<task>Write a commit message for the staged changes below.</task>

<format>
- 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
</format>

<style>
- Imperative mood: "Add feature" not "Added feature"
- Match recent commit style (conventional commits if used)
- Describe the change, not the intent or benefit
</style>
{% if user_guidance %}
<user-guidance>
{{ user_guidance }}
</user-guidance>
{% endif %}{% if project_guidance %}
<project-guidance>
{{ project_guidance }}
</project-guidance>
{% endif %}
<diffstat>
{{ git_diff_stat }}
</diffstat>

<diff>
{{ git_diff }}
</diff>

<context>
Branch: {{ branch }}
{% if recent_commits %}<recent_commits>
{% for commit in recent_commits %}- {{ commit }}
{% endfor %}</recent_commits>{% endif %}
</context>

"""

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:

[commit.generation]
squash-template = """
<task>Write a commit message for the combined effect of these commits.</task>

<format>
- 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
</format>

<style>
- Imperative mood: "Add feature" not "Added feature"
- Match the style of commits being squashed (conventional commits if used)
- Describe the change, not the intent or benefit
</style>
{% if user_guidance %}
<user-guidance>
{{ user_guidance }}
</user-guidance>
{% endif %}{% if project_guidance %}
<project-guidance>
{{ project_guidance }}
</project-guidance>
{% endif %}
<commits branch="{{ branch }}" target="{{ target_branch }}">
{% for detail in commit_details %}- {{ detail.subject }}
{% endfor %}</commits>

<diffstat>
{{ git_diff_stat }}
</diffstat>

<diff>
{{ git_diff }}
</diff>

"""

Appending to the prompt

template-append adds personal conventions to the commit and squash prompts without restating the whole template:

[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.

Hooks

See wt hook for hook types, execution order, template variables, and examples. User hooks apply to all projects; project hooks 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 for hook types, execution order, and examples.

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):

[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:

[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 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:

[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, since they describe per-developer environment (which CLI is installed, which agent the developer prefers). How the fragment renders: the LLM commits guide.

Copy-ignored excludes

Additional excludes for wt step copy-ignored:

[step.copy-ignored]
exclude = [".cache/", ".turbo/"]

Built-in excludes (VCS metadata and tool-state directories) always apply; the wt step copy-ignored docs list them. User config and project config exclusions are combined.

Aliases

Command templates that run as wt <name>. See the Extending Worktrunk guide for usage and flags.

[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 [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 cds 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)
CLICOLOR_FORCE Force colored output even when not a TTY

Inline config overrides (--config-set)

--config-set <toml> 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 … <command> 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 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

  <b><span class=c>--config</span></b><span class=c> &lt;path&gt;</span>
      User config file path

  <b><span class=c>--config-set</span></b><span class=c> &lt;toml&gt;</span>
      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

  <b><span class=c>--config</span></b><span class=c> &lt;path&gt;</span>
      User config file path

  <b><span class=c>--config-set</span></b><span class=c> &lt;toml&gt;</span>
      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:

{
  "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

  <b><span class=c>--config</span></b><span class=c> &lt;path&gt;</span>
      User config file path

  <b><span class=c>--config-set</span></b><span class=c> &lt;toml&gt;</span>
      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 <name>. See the Extending Worktrunk guide 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

  <b><span class=c>--config</span></b><span class=c> &lt;path&gt;</span>
      User config file path

  <b><span class=c>--config-set</span></b><span class=c> &lt;toml&gt;</span>
      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

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

  <b><span class=c>--config</span></b><span class=c> &lt;path&gt;</span>
      User config file path

  <b><span class=c>--config-set</span></b><span class=c> &lt;toml&gt;</span>
      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, 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

  <b><span class=c>--config</span></b><span class=c> &lt;path&gt;</span>
      User config file path

  <b><span class=c>--config-set</span></b><span class=c> &lt;toml&gt;</span>
      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; $(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

  <b><span class=c>--config</span></b><span class=c> &lt;path&gt;</span>
      User config file path

  <b><span class=c>--config-set</span></b><span class=c> &lt;toml&gt;</span>
      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

  <b><span class=c>--config</span></b><span class=c> &lt;path&gt;</span>
      User config file path

  <b><span class=c>--config-set</span></b><span class=c> &lt;toml&gt;</span>
      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. This subcommand still works but prints a deprecation notice.

Status values, display symbols, and fetch behavior: wt list 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

  <b><span class=c>--config</span></b><span class=c> &lt;path&gt;</span>
      User config file path

  <b><span class=c>--config-set</span></b><span class=c> &lt;toml&gt;</span>
      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 plugin sets markers automatically
  • Notes — Any short text: "blocked", "needs tests"

Storage

Stored in git config as worktrunk.state.<branch>.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

  <b><span class=c>--config</span></b><span class=c> &lt;path&gt;</span>
      User config file path

  <b><span class=c>--config-set</span></b><span class=c> &lt;toml&gt;</span>
      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 as {{ vars.<key> }}. Use the default filter for keys that may not be set:

[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}'") }}

[post-start]
dev = "npm start -- --port {{ vars.config.port }}"

Storage format

Stored in git config as worktrunk.state.<branch>.vars.<key>. 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

  <b><span class=c>--config</span></b><span class=c> &lt;path&gt;</span>
      User config file path

  <b><span class=c>--config-set</span></b><span class=c> &lt;toml&gt;</span>
      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 %}