Skip to content

wt list

List worktrees and their status.

Shows uncommitted changes, divergence from the default branch and remote, and optional CI status and LLM summaries.

wt list demo
Progressive rendering, then --full and --branches

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 adds the two columns that reach off-machine: CI status (GitHub/GitLab pipeline pass/fail, over the network) and LLM-generated summaries of each branch’s changes.

List all worktrees:

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: ha…
+ fix-auth ↕| ↑2 ↓1 +25 -11 | b772e68 5h Add secure token s…
+ fix-typos _| | 41ee083 4d Merge fix-auth: ha…
Showing 4 worktrees, 1 with changes, 2 ahead, hidden: Path

Include CI status and LLM summaries:

wt list --full
Branch Status HEAD± main↕ main…± Summary Remote⇅ CI
@ feature-api + ↕⇡ +54 -5 ↑4 ↓1 +234 -24 Refactor API to REST archit… ⇡3 #412
^ main ^⇅ ⇡1 ⇣1 #
+ fix-auth ↕| ↑2 ↓1 +25 -11 Harden auth with constant-t… | #408
+ fix-typos _| | #410
Showing 4 worktrees, 1 with changes, 2 ahead, hidden: Path, Commit, Age, Message

Include branches that don’t have worktrees:

wt list --branches --full
Branch Status HEAD± main↕ main…± Summary Remote⇅ CI
@ feature-api + ↕⇡ +54 -5 ↑4 ↓1 +234 -24 Refactor API to REST archit… ⇡3 #412
^ main ^⇅ ⇡1 ⇣1 #
+ fix-auth ↕| ↑2 ↓1 +25 -11 Harden auth with constant-t… | #408
+ fix-typos _| | #410
/ exp /↕ ↑2 ↓1 +137 Explore GraphQL schema and…
/ wip /↕ ↑1 ↓1 +33 Start API documentation
Showing 4 worktrees, 2 branches, 1 with changes, 4 ahead, hidden: Path, Commit, Age, Message

Output as JSON for scripting:

wt list --format=json
ColumnShows
BranchBranch name, elided with past 32 characters; a detached worktree shows its short hash
StatusCompact symbols (see below)
HEAD±Uncommitted changes, including untracked files: +added -deleted lines
main↕Commits ahead/behind default branch
main…±Line diffs since the merge-base (three-dot) with the default branch
SummaryLLM-generated branch summary; requires --full, summary = true, and commit.generation
Remote⇅Commits ahead/behind tracking branch
CIPR/MR number colored by pipeline status; --full only
PathWorktree directory
URLDev server URL from project config; dimmed if port is not listening
(custom)User-defined custom columns from [list.custom-columns] user config
CommitShort hash, abbreviated per core.abbrev
AgeTime since last commit
MessageLast commit message (truncated)

The main↕ and main…± headers keep the familiar main label in every repository.

The table sizes itself to the terminal. When the columns don’t all fit, the least important go first — roughly right to left, since the order above runs from identity to nice-to-have — and the summary footer names them (hidden: Commit, Age, Message). A wider terminal brings them back. To pin a set rather than leave it to the width, name the columns in [list] columns; --format=json carries every field at any width.

The leftmost column marks each row by physical presence, from most present to least:

SymbolMeaning
@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)

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). Green, blue, red, yellow, and gray show the pipeline state; magenta and cyan show the review state. checks object and review states give the JSON form of each value:

IndicatorValueMeaning
# 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)pr and checks absentNo upstream, or no PR/MR and no branch workflow

The two remaining review states have no indicator of their own: "draft" only dims the cell and "approved" leaves the color unchanged.

Changes requested (magenta) outranks running checks, while a required review (cyan) only recolors an otherwise green or gray branch. GitLab reports only the "pending" and "draft" review states.

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 (checks.stale). Results are cached for 30-60 seconds; use wt config state cache to view or clear.

Reuses the commit.generation 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.

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, and {{ git.branch.* }}, the branch’s own git config under branch.<name>.* (a jira key you set yourself, or the git-native description) — useful for tracking what each of many (often agent-driven) branches is for:

~/.config/worktrunk/config.toml
[list.custom-columns.Ticket]
template = "{{ vars.ticket }}"

The custom columns config covers templates, widths, and drop priority.

The Status column packs several subcolumns, left to right, each mapping to a schema-2 field in --format=json (schema 1 spells several of them differently — see Schema 1). 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.

Independent flags from git status; several can show at once (e.g. +!?). Each maps to a boolean in the changes object:

Symbolworktree.changesMeaning
+stagedStaged files
!modifiedModified files (unstaged)
?untrackedUntracked files

worktree.changes also reports renamed and deleted, which have no dedicated symbol in the column.

An in-progress git operation, a worktree-location attribute, or a branch with no worktree. One symbol shows, highest priority first (✘ > ↻ > ⊟ > ⊞ > ⊘ > ⚑ > /):

SymbolJSONMeaning
worktree.changes.conflictedMerge conflicts
worktree.operation "rebase", "merge", "cherry_pick", "revert", "bisect"A git operation is in progress; git status names it
worktree.prunablePrunable (worktree directory or its .git gone)
worktree.lockedLocked worktree
worktree.detachedDetached HEAD
worktree.duplicate_branchBranch checked out in more than one worktree
worktree.branch_mismatchWorktree isn’t at the path its branch implies
/no worktree objectBranch without a worktree

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 display.state value:

Symboldisplay.stateMeaning
^"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 into the default branch or merge target via different history; the matching check is in default_branch.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 tracked 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_ ("empty") or ("integrated").

Relation to the tracking branch, derived from the upstream.ahead / upstream.behind counts; blank when there is no upstream:

SymbolupstreamMeaning
|ahead 0, behind 0In sync with remote
ahead > 0Ahead of remote
behind > 0Behind remote
ahead > 0, behind > 0Diverged from remote

The last subcolumn shows the branch’s marker, set with wt config state marker set — usually an emoji saying what the branch is for. It reads back as the item’s marker field.

These appear across all columns while the table is loading:

SymbolMeaning
·Data is loading, or collection timed out / branch too stale

--format=json emits schema 2 by default: the envelope format below. Set [list] json-schema = 1 to retain the original bare-array format.

One envelope object. Items carry independent facts; rendered strings (including the collapsed Status value) live under display:

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

Envelope fields:

FieldTypeDescription
schemanumberFormat version; always 2
repoobject{default_branch, forge} — the branch every default_branch object measures against (absent when detection failed), and forge metadata derived from the primary remote (absent when no remote URL parses; see repo object)
collectedobject{ci, summary} — which gated fact families this run requested, so an absent pr/checks/summary reads as “not requested” rather than “none”
itemsarrayOne object per row: a worktree, a local branch, or a remote-only branch

Item fields:

FieldTypeDescription
branchstring/nullBranch name; null for a detached-HEAD worktree. Remote rows carry the bare name with the remote in remote
remotestringRemote name, present only on remote-only branch rows
headobject/nullHEAD commit (see head object); null for unborn branches
worktreeobjectWorktree facts (see worktree object); absent on branch-only rows
default_branchobjectRelation to the default branch (see default_branch object); absent on the default branch itself
upstreamobjectTracking branch (see upstream object); absent when none is configured
probjectOpen PR/MR (see pr object); collected with --full
checksobjectCI pipeline (see checks object); collected with --full
dev_serverobject{url, listening} from the project’s list.url template; absent when not configured
summarystringLLM branch summary; needs --full, [list] summary = true, and a [commit.generation] command
markerstringBranch marker from wt config state marker; absent when none is set
varsobjectPer-branch variables from wt config state vars
displayobjectRendered strings (see display object)
FieldTypeDescription
shastringFull commit SHA (40 chars)
short_shastringShort commit SHA, abbreviated per core.abbrev (auto-extends for ambiguous prefixes)
subjectstring/nullCommit subject (first line); null when not loaded, as for a prunable worktree
committed_atstring/nullCommitter time, RFC 3339 UTC; null when not loaded

Present only on worktree rows. The location attributes are independent, so they co-occur — see Worktree for the symbols, which pick one:

FieldTypeDescription
pathstringWorktree path
mainbooleanIs the main worktree
currentbooleanIs the worktree the command ran from
previousbooleanIs the previous worktree (wt switch -)
detachedbooleanHEAD is detached
lockedobject{reason}; absent when not locked
prunableobject{reason}; absent when git doesn’t report the worktree prunable
branch_mismatchbooleanWorktree isn’t at the path its branch implies
duplicate_branchbooleanAnother worktree has the same branch checked out
operationstring/nullIn-progress operation: "merge", "rebase", "cherry_pick", "revert", "bisect"; absent when none
changesobject/nullWorking-tree state (see changes object)

The five change flags map to the Working tree symbols (renamed and deleted have none of their own):

FieldTypeDescription
stagedbooleanHas staged files
modifiedbooleanHas modified files (unstaged)
untrackedbooleanHas untracked files
renamedbooleanHas renamed files
deletedbooleanHas deleted files
conflictedboolean/nullTracked files carry merge conflicts
diffobject/nullLines changed vs HEAD: {added, deleted}

Independent facts; the table’s priority-collapsed symbol is display.state.

FieldTypeDescription
aheadnumber/nullCommits ahead of the default branch (null for orphans)
behindnumber/nullCommits behind the default branch (null for orphans)
diffobject/nullLines changed vs the default branch: {added, deleted}
orphanboolean/nullNo common ancestor with the default branch
integrationobject/null{reason} — which check found the content integrated (see integration reasons); absent when determined not-integrated, null when a dirty tree skipped the checks
merge_conflictsboolean/nullMerging into the default branch would conflict, simulated locally with git merge-tree

ahead / behind drive the Remote divergence symbol:

FieldTypeDescription
remotestringRemote name (e.g., "origin")
branchstring/nullBranch name on the remote
aheadnumberCommits ahead of remote
behindnumberCommits behind remote
FieldTypeDescription
numberinteger/nullPR/MR number
urlstring/nullURL to the PR/MR page
reviewstringReview state (see review states); absent when the forge reports no review signal
mergeableboolean/nullFalse when the forge reports conflicts, null otherwise
repoobjectStructured metadata for the repository the PR/MR targets, the upstream for fork PRs (see repo object); absent when the URL doesn’t parse
FieldTypeDescription
statusstring/null"passed", "running", or "failed"; null when a conflicts report masks it
sourcestring"pr" (PR/MR) or "branch" (branch workflow)
stalebooleanLocal HEAD differs from remote (unpushed changes)

Three CI status values have no checks.status of their own, because the shape of pr and checks reports them: a fetch error () makes both null, no CI leaves checks absent, and merge conflicts leave checks.status null with pr.mergeable false.

FieldTypeDescription
urlstringDev server URL from project config
listeningboolean/nullWhether the URL’s port is listening

Presentation only — every value here renders facts that appear elsewhere in the item:

FieldTypeDescription
statestringThe table’s collapsed default-branch state (see state values); absent when none applies
symbolsstringRaw status symbols without colors (e.g., "!?↓")
statuslinestringPre-formatted status with colors and links
columnsobjectRendered custom column values keyed by header; empty cells omitted

repo.forge describes the local checkout’s repository as derived from the primary remote. pr.repo describes the repository targeted by the PR/MR (for fork PRs, the upstream target).

FieldTypeDescription
urlstringRepository web URL
providerstring"github", "gitlab", "gitea", "azure-devops", or "unknown"
hoststringRepository web host
ownerstringOwner, organization, or namespace path
namestringRepository name
projectstringAzure DevOps project name; absent for other providers
remotestringLocal remote name; present on repo.forge, absent from pr.repo

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 for the symbol and the full meaning of each value ("is_main", "orphan", "empty", "integrated", "would_conflict", "same_commit", "diverged", "ahead", "behind").

default_branch.integration.reason records which check matched. Checks run cheapest-first and the first match wins. JSON-only — every reason renders as the same :

ValueMeaning
"same_commit"Branch HEAD is the default branch’s commit
"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)

pr.review is one of "changes_requested", "pending", "draft", "approved", absent when the forge reports no review signal. CI status shows how each renders. The vocabulary matches Claude Code’s statusline pr.review_state field.

# 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)'
# Worktrees with merge conflicts
wt list --format=json | jq '.items[] | select(.worktree.changes.conflicted)'
# Branches ahead of main (needs merging)
wt list --format=json | jq '.items[] | select(.default_branch.ahead > 0) | .branch'
# Integrated branches (safe to remove)
wt list --format=json | jq '.items[] | select(.display.state == "integrated" or .display.state == "empty") | .branch'
# Branches without worktrees
wt list --format=json --branches | jq '.items[] | select(.worktree == null) | .branch'
# Worktrees ahead of upstream (needs pushing)
wt list --format=json | jq '.items[] | select(.upstream.ahead > 0) | {branch, ahead: .upstream.ahead}'
# Stale CI (local changes not reflected in CI)
wt list --format=json --full | jq '.items[] | select(.checks.stale) | .branch'

A JSON Schema for the envelope is published at worktrunk.dev/schema/list-v2.json.

The original bare-array format — one object per row, no envelope — selected by [list] json-schema = 1. Its fields all have a schema-2 home:

Schema 1Schema 2
branchbranch
pathworktree.path
kindthe row’s shape — a worktree object means "worktree", no worktree object means "branch"
commit.sha, .short_sha, .messagehead.sha, .short_sha, .subject
commit.timestamp (Unix)head.committed_at (RFC 3339 UTC)
working_tree.staged, .modified, .untracked, .renamed, .deleted, .diffworktree.changes.*
operation_state "conflicts"worktree.changes.conflicted
operation_state "rebase", "merge", …worktree.operation
main_statedisplay.state
integration_reasondefault_branch.integration.reason (snake_case, and it reports "same_commit" rather than folding it into main_state)
main.ahead, .behind, .diffdefault_branch.ahead, .behind, .diff
remote.name, .branch, .ahead, .behindupstream.remote, .branch, .ahead, .behind
worktree.stateworktree.locked, .prunable, .duplicate_branch, .branch_mismatch — independent, so they co-occur
worktree.reasonworktree.locked.reason, worktree.prunable.reason
worktree.detachedworktree.detached
is_main, is_current, is_previousworktree.main, .current, .previous
ci.statuschecks.status, plus the shapes described under checks object
ci.source, ci.stalechecks.source, checks.stale
ci.number, ci.url, ci.review_statepr.number, pr.url, pr.review
ci.repo, ci.repo_urlpr.repo, pr.repo.url
repo, repo_urlthe envelope’s repo.forge, repo.forge.url
url, url_activedev_server.url, dev_server.listening
summary, vars, markersummary, vars, marker
statusline, symbols, columnsdisplay.statusline, display.symbols, display.columns

The envelope’s repo.default_branch and collected have no schema-1 equivalent, and schema 2 separates “nothing to report” from “not determined” — see How “no value” reads.

Missing a field that would be generally useful? Open an issue.

  • wt switch — Switch worktrees or open interactive picker
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

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.

  • table (default): branch status HEAD± main↕ main…± Remote⇅ CI URL
  • json: the current wt list --format=json schema — a one-item envelope by default, or a one-item array with [list] json-schema = 1
  • claude-code: the table cells, preceded by dir and followed by model context pace

A cell with nothing to show is left out, so most lines are shorter than that. A line that overruns the terminal drops whole cells, least important first.

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 terminal links.

--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). Its color deepens from dim to yellow as more of the window would be spent capped.

Claude Code statusline setup has the ~/.claude/settings.json entry that feeds this mode.

Command reference

Section titled “Command reference”
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')