vibe list
The list command shows every worktree of the current repository in one aligned table, together with the branch it is based on, how long ago its tip was committed, and whether it has uncommitted changes.
vibe list [options]Options
Section titled “Options”| Option | Description |
|---|---|
--json |
Emit the listing as a JSON array |
--dirty |
Only worktrees with uncommitted changes |
--clean |
Only worktrees with no uncommitted changes |
--base <branch> |
Only worktrees based on <branch> |
--recent <dur> |
Only worktrees committed to within <dur> |
--stale <dur> |
Only worktrees not committed to for <dur> |
--sort <key> |
Sort by age, name or status |
--reverse |
Reverse the final display order |
--limit <n> |
Show at most <n> worktrees (<n> must be at least 1) |
-V, --verbose |
Show detailed output |
-q, --quiet |
Suppress non-essential output |
--dirty and --clean cannot be combined, and neither can --recent and --stale; either pair exits 2.
--quiet does not silence the listing itself: the table is the command’s product, so vibe list --quiet would otherwise exit 0 having printed nothing. Only the surrounding diagnostics are suppressed.
Example
Section titled “Example”$ vibe list* feat/login develop 2h M 3 /path/to/repo-feat-login fix/crash develop 3d clean /path/to/repo-fix-crash (detached) - 1w clean /path/to/repo-detached scratch/2026… develop 12m clean /path/to/scratch-20260101 (scratch)Columns
Section titled “Columns”| Column | Meaning |
|---|---|
| (marker) | * marks the worktree you are currently standing in |
| BRANCH | The checked-out branch, or (detached) for a detached-HEAD worktree |
| BASE | The branch this one is based on — see BASE below |
| AGE | How long ago the tip commit was made (now, 12m, 2h, 3d, 1w, 5mo, 2y) |
| STATUS | clean, or M <n> — see How changes are counted |
| SUMMARY | Only when [summary] is configured — see SUMMARY below |
| PATH | The worktree directory |
A (scratch) label is appended to worktrees created by vibe scratch.
How changes are counted
Section titled “How changes are counted”The number in M <n> (and the dirty_files field) is the number of entries git status reports, which is not always the number of changed files. git collapses a wholly untracked directory into a single entry, so a new directory containing five files counts as 1, not 5. Tracked modifications, staged changes and individually untracked files each count as one entry, and a rename counts once rather than twice.
This is git’s default reporting (-unormal). vibe deliberately does not pass -uall, which would expand every untracked directory: it makes git walk untracked trees in full on every row of the listing — unbounded work in exactly the repositories (a stale node_modules, a fat build output) where it is least wanted — to refine a number whose job is to convey “there is something here”.
Any cell whose value could not be determined renders as -. A worktree that git cannot read (for example one left behind by a deleted checkout) still appears as a row; only the affected cells degrade.
A failure to read a worktree’s STATUS is reported as a warning on stderr. A failure to resolve AGE or BASE (for example when git log cannot read a detached worktree’s HEAD) degrades to - silently — it is expected often enough that warning about it would be noise.
BASE is resolved as:
- The branch’s configured upstream, with the remote prefix stripped (
origin/develop→develop). - Otherwise the repository’s default branch (from
refs/remotes/origin/HEAD, theninit.defaultBranch).
A branch is never shown as based on itself, so the main worktree — and any branch whose upstream is its own remote tracking ref — shows -. A detached HEAD also shows -: it is not based on a branch in any sense that can be stated truthfully.
vibe list deliberately does not run git merge-base per worktree. That would cost one git invocation per row, and its answer is a commit, not a branch — mapping that commit back to a branch name is ambiguous whenever several branches share the merge point, which is the common case right after branching. An upstream is a fact you configured, and the default branch is the documented fallback, so both are explainable.
AGE is the elapsed time since the tip commit’s committer date, truncated (never rounded up) so it never claims more time than has actually passed. The mo and y units are display-only approximations (30 and 365 days); --json publishes the exact timestamp in last_commit_at instead.
A branch with no commits yet (an unborn branch) has no tip to date, so its AGE reads - and its last_commit_at is null.
SUMMARY
Section titled “SUMMARY”The SUMMARY column appears only when the repository’s .vibe.toml sets [summary] command. vibe runs that command once per vibe list, hands it the batch of worktrees on stdin, and shows what it prints back:
[summary]command = "./examples/summary/last-commit.sh"timeout_seconds = 30$ vibe list* feat/login develop 2h M 3 Adds the login form /path/to/repo-feat-login fix/crash develop 3d clean Fixes the startup panic /path/to/repo-fix-crashResults are cached per worktree, so a second vibe list over an unchanged repository does not run the command at all. If the command fails or times out, vibe list warns and shows the previously cached summary rather than failing.
The column’s presence follows the configuration, not the command’s success: a worktree the command stayed silent about shows an empty cell, so an empty column never has to be read as “maybe the feature is off”.
Because the command runs with your shell and your permissions, it is covered by the trust mechanism — editing [summary] command invalidates .vibe.toml’s hash and vibe list fails until you run vibe trust again.
See .vibe.toml → Summary Configuration for the full contract, the limits applied to the command’s output, and the cache invalidation rules. Ready-to-use scripts live in examples/summary/.
Filtering, sorting and limiting
Section titled “Filtering, sorting and limiting”The flags form a pipeline applied in a fixed order:
filter (AND) → sort → reverse → limitEvery stage runs on the fully resolved rows, so a filter and --json can never disagree about a worktree’s base, age or status — and the same flags select the same worktrees in both output modes.
Examples
Section titled “Examples”# What did I touch this week and leave unfinished?vibe list --recent 1w --dirty
# What has been sitting untouched for a month?vibe list --stale 30d
# The five oldest worktreesvibe list --sort age --reverse --limit 5
# Everything branched off develop, messiest firstvibe list --base develop --sort status
# The three most recently committed, as JSONvibe list --json --sort age --limit 3 2>&1 | jq .Filters
Section titled “Filters”Filters combine with AND: each flag narrows the result, so adding one never returns more rows.
| Flag | Keeps |
|---|---|
--dirty |
Worktrees whose STATUS is dirty |
--clean |
Worktrees whose STATUS is clean |
--base <branch> |
Worktrees whose BASE is exactly <branch> |
--recent <dur> |
Worktrees whose tip commit is at most <dur> old |
--stale <dur> |
Worktrees whose tip commit is more than <dur> old |
A worktree whose status could not be read (STATUS -) is excluded by --dirty and by --clean. The two flags therefore do not partition the listing: a worktree git could not answer for belongs to neither answer, and presenting it as one or the other would be a confident guess. The unfiltered listing still shows it, with -, and warns.
--base compares against the resolved BASE column. The match is exact, not a prefix: --base develop does not match develop-2. A worktree with no base (the main worktree, or a detached HEAD) is never matched by --base.
The argument is accepted either verbatim or with a leading remote name removed, so every spelling you might reasonably type works:
| Argument | Matches a base of |
|---|---|
develop |
develop |
origin/develop |
origin/develop, develop |
release/next |
release/next |
origin/release/next |
origin/release/next, release/next |
Both readings are tried because they cannot be told apart from the argument alone: origin/develop is a remote-qualified develop, but release/next is a plain branch whose name happens to contain a slash, and both are spelled <word>/<word>. Trying only the stripped reading would turn --base release/next into --base next and match nothing.
The cost of accepting both is that --base origin/develop also matches a local branch literally named origin/develop, if you have one. Surfacing an extra row is the deliberate trade against silently returning none.
Durations
Section titled “Durations”--recent and --stale take a duration written as a positive integer followed by one unit:
| Unit | Meaning |
|---|---|
s |
seconds |
m |
minutes |
h |
hours |
d |
days |
w |
weeks |
Examples: 90s, 30m, 12h, 2d, 1w.
Anything else exits 2 with an explanation: an empty value, a bare number with no unit (30), a compound form (1h30m), a fraction (1.5d), a zero (0d), or a value so large it would overflow.
The boundary is exact and the two flags are complements of each other for any worktree with a known age:
--recent <dur>keeps rows wherenow − commit ≤ dur(inclusive)--stale <dur>keeps rows wherenow − commit > dur(exclusive)
So vibe list --recent 1d and vibe list --stale 1d together account for every worktree whose tip commit date is known, with no overlap.
A commit dated in the future — routine when clock skew exists between the machine that made the commit and this one — counts as --recent, matching the now the AGE column already shows for it.
A worktree with no tip commit (an unborn branch, or one whose log could not be read) matches neither --recent nor --stale. Both ask a question about a commit date that row does not have.
Sorting
Section titled “Sorting”--sort takes one of three keys:
| Key | Order |
|---|---|
age |
Newest tip commit first; ties broken by name |
name |
Lexicographic by name |
status |
Dirty first, then most changed entries first, then by name |
Passing --sort replaces the default order entirely, including the current-worktree-first rule: --sort age promises the newest row first, and exempting one row from that would move whichever worktree you happen to be standing in.
Under --sort name, a detached-HEAD worktree takes part using its directory basename (the same value --json publishes as name).
Every sort ends in a final tie-break on the worktree path, so the output is fully deterministic: two detached worktrees in sibling directories can share a basename, and without it their order would depend on which one you jumped to more recently.
Rows with an unknown age always sort last under --sort age, and stay last under --reverse too: “the oldest worktrees” is a question about worktrees that have an age, so --sort age --reverse --limit 5 returns five actual answers rather than spending slots on rows the question does not apply to.
--reverse and --limit
Section titled “--reverse and --limit”--reverse reverses whatever the final display order would have been, so it is meaningful on its own (it flips the default current-first MRU order) as well as after --sort.
--limit <n> truncates after sorting and reversing. That order is what makes “the five oldest” expressible as --sort age --reverse --limit 5; limiting first would take the five newest and merely print them backwards, which is a different set. --limit 0 is rejected (exit 2) rather than printing nothing, because an empty listing is indistinguishable from a repository with no worktrees.
When a filter matches nothing, vibe list says so explicitly (No worktrees matched the given filters.) rather than reporting No worktrees found. — the two call for different next actions. In --json mode the result is simply [].
JSON output
Section titled “JSON output”vibe list --json 2>&1 | jq .The 2>&1 is required. vibe writes the JSON payload to stderr, because stdout is the shell-eval channel: the shell wrapper runs eval "$(command vibe "$@")", so anything written there would be executed as shell code. In --json mode the payload is the only thing list itself writes to stderr — even --verbose diagnostics and the command’s own warnings are withheld so the document stays parseable.
One exception is outside this command’s control: a warning about conflicting global flags (vibe --verbose --quiet list --json prints Warning: Both --verbose and --quiet specified.) is emitted before any subcommand runs, and would precede the payload. Avoid combining conflicting global flags when parsing the output.
Schema
Section titled “Schema”Each array element is an object with these fields:
| Field | Type | Description |
|---|---|---|
branch |
string | null | The checked-out branch; null for a detached HEAD |
path |
string | Absolute path to the worktree |
current |
boolean | Whether this is the worktree the command was invoked from |
scratch |
boolean | Whether the branch is an auto-generated scratch/<timestamp> worktree |
name |
string | Always present: the branch, or the directory basename for a detached HEAD |
base |
string | null | The BASE column’s value; null when it does not apply or could not be read |
head |
string | null | The commit sha HEAD points at; null for an unborn branch (no commits yet) |
last_commit_at |
string | null | The tip’s committer date in ISO 8601; null for an unborn branch |
status |
string | null | "clean" or "dirty"; null when git could not be asked |
dirty_files |
number | null | How many entries git status reported; null whenever status is null |
summary |
string | Present only when [summary] is configured; "" when unanswered |
branch, path, current and scratch are the fields shipped in v3.1.0 and keep their names, types and position. New fields are only ever appended.
head is always either a sha you can pass to git show or null. A worktree whose branch has no commits yet reports null rather than git’s all-zero placeholder OID, so the field never carries a sha-shaped value that does not resolve.
summary is omitted entirely when [summary] is not configured, so a repository not using the feature produces exactly the document it produced before the feature existed.
The relative AGE string is not published. A consumer wanting “3 days ago” is better served computing it from last_commit_at, which is exact.
Examples
Section titled “Examples”# Every dirty worktree — the filter does the selecting, jq only projectsvibe list --json --dirty 2>&1 | jq -r '.[] | .path'
# Branch and base, as a tablevibe list --json 2>&1 | jq -r '.[] | "\(.name)\t\(.base // "-")"'
# The worktree you are standing invibe list --json 2>&1 | jq -r '.[] | select(.current) | .path'
# The three worktrees you have not touched in longestvibe list --json --sort age --reverse --limit 3 2>&1 | jq -r '.[] | .name'The filter, sort and limit flags apply to --json exactly as they do to the table, so a script can push the selection into vibe instead of reimplementing it in jq. A filtered listing that matches nothing is [].
Ordering
Section titled “Ordering”Rows are ordered: the current worktree first, then the remaining worktrees most-recently-jumped-to first (the same MRU order vibe jump uses for its selection prompt), then any never-visited worktree in git’s own order.
A missing or corrupt MRU store degrades to git’s order; it never fails the listing.
This is the default order. --sort replaces it, and --reverse flips whichever order is in effect.
Exit Codes
Section titled “Exit Codes”| Code | Condition |
|---|---|
0 |
The listing was produced (including when a filter matched nothing) |
1 |
Not inside a git repository |
2 |
Invalid arguments: conflicting filters, a malformed duration, --limit 0, an unknown sort key |