A small command-line helper for managing coding-agent git worktrees.
An agent works best in a worktree of its own, one per task, under
<repo-root>/.claude/worktrees/<name>. ccwt lets you create, list, jump between, and
tear down those worktrees from your own shell, with a table saying what each one is about
and whether something is running in it.
It started out as a Claude Code helper, and Claude Code
is still hardcoded in a handful of places: the .claude/worktrees/ layout and the
adjective-verb-noun naming scheme (which is what keeps ccwt and claude --worktree
interoperable), the AGENT column, which looks for running Claude Code processes, and
TOPIC, the freshness sort and the worklog's session page, which all read Claude Code
transcripts. The rest — the worktrees, the table, the tui, the queue, the ✳ marker —
doesn't care which agent you run, and task_command is where you say which one that is.
go install github.com/mkmik/ccwt@latest
This drops a ccwt binary in $(go env GOPATH)/bin (make sure that's on your PATH).
A program can't change its parent shell's working directory, so on its own ccwt new
can only print the path of the worktree it created. To make ccwt actually cd you
into a worktree (and ccwt .. jump you back to the repo root), source the integration
snippet from your shell's rc file:
source <(ccwt init zsh)
Everything still works without it — you just won't get the automatic directory change.
Under zsh the same snippet also installs completion: ccwt <TAB> lists the commands,
and ccwt cd <TAB> (like remove and lock) lists the worktrees of the repo you're
standing in. Source it after compinit, which is what defines compdef — sourced
before, you keep the cd and silently get no completion.
ccwt new # create a fresh worktree on a new branch, and cd into it
# ... do your work, run your agent, commit, etc. ...
ccwt list # show all agent worktrees in this repo
ccwt list -g # ... or in every project ccwt knows
ccwt .. # jump back to the repository root
ccwt remove <name> # delete a worktree and its branch when you're done
ccwt remove . # ... or the one you're in: deletes it and cds you out
ccwt gc # ... or all the finished ones at once, after confirming
ccwt new creates a worktree at .claude/worktrees/<name> on a new branch
worktree-<name> (the prefix is configurable). With no name
it generates one (e.g. dreamy-foraging-hickey); pass a name to choose your own. Run from
inside an existing worktree, it returns that worktree instead of nesting a new one.
ccwt new --switch foobar # check out an existing branch instead of creating one
ccwt new mywt --switch foobar # ... in a worktree of your own name
ccwt new --path # print the absolute path instead of the name
.env and friends overA worktree is a fresh checkout, so the gitignored files your build needs — .env,
local credentials, editor settings — aren't in it. List them in a .worktreeinclude
file at the repository root and ccwt new copies them into every worktree it creates:
.env
.env.local
config/secrets.json
The syntax is .gitignore's. Only files that both match a pattern and are ignored by
git are copied, so a broad pattern can't duplicate a tracked file or drag in a stray
scratch file — and file modes carry over, so a 0600 secret stays 0600. This is the
same file Claude Code reads when it creates
a worktree, so either tool gives you the same result.
ccwt list renders a table of the repo's worktrees with their branch, age, whether an
agent session is currently running in each, and what each one is about:
NAME BRANCH AGE AGENT TOPIC
* * dreamy-foraging-hickey worktree-dreamy-…-hickey 2h yes ✳ Goal was a widget on the dashboard; it's built and…
✓ calm-baking-otter worktree-calm-bak…-otter 1d no ⎇ Fix the flux capacitor
Every row leads with two glyphs. The first is a * on the worktree you're currently in.
The second says whether the worktree can go: ✓ means its branch is already merged into
main (or master), so it's safe to ccwt remove — a squash, a rebase or a
cherry-pick counts too; ☐ means it isn't merged but every
commit is already on its upstream, so nothing here is waiting on you — it's waiting on a
review or on CI; * means it has uncommitted changes, which isn't safe to remove whatever
git makes of the branch or the remote, and ± means the same on a merged branch — the changes
are the only thing in there that isn't on main yet; ✳ beats them all and means an
agent is working in there right now — see Herdr integration.
The glyphs are omitted when stdout isn't a terminal, so piped output stays parseable. The tui
under Herdr puts a third ahead of both, the agents' dot.
The freshest worktree is on top. Freshness is the younger of two things: the worktree's
last commit, and the last line written to its newest agent session — so a worktree an
agent has been working in all afternoon without committing sorts above one whose commit is
newer but that nobody has touched since, and one an agent started twenty minutes ago sorts
to the top rather than to the bottom on its zero commits. --sort=commit is git's answer
on its own, which is what the list did before there was a choice; the default is
configurable and --sort overrides it, on both ccwt list and
ccwt tui. Under Herdr the worktrees open in a workspace come first, in the order of its
sidebar, and under -g so do the sections of the repos it has open; the rest follow as above.
In the tui the rest are folded away behind a ▸ … N more row, which a click or ↵ opens.
TOPIC says what the worktree is about, and its glyph says where that came from. ✳ is an
agent session — the newest transcript for that worktree, showing the last recap it produced
(/recap, or one the agent wrote on its own), or the first prompt you typed when it never
recapped. ⎇ is the last commit, for a worktree nobody has run a session in.
AGENT and TOPIC's ✳ both read Claude Code's own transcripts, so an agent that files its
sessions somewhere else reads as no with its last commit for a topic — the worktree, and
everything else the row says about it, is the same either way.
The table is sized to your terminal, so it never wraps. BRANCH is kept narrow even when
there's room — it's usually the worktree's own name with a worktree- in front — and the
space goes to TOPIC. Branches and commit subjects lose their middle rather than their tail,
since the last word is usually what tells them apart, while a session summary simply stops:
NAME BRANCH AGE AGENT TOPIC
* * dreamy-foraging-hickey worktree-dreamy-…-hickey 2h yes ✳ Goal was a widget on…
✓ calm-baking-otter worktree-calm-b…-otter 1d no ⎇ Fix the flux ca… (#41)
Squash, rebase and cherry-pick all rewrite commit SHAs, so the branch a PR landed from is
no ancestor of main afterwards — which is how most projects merge, and it leaves ccwt
refusing to remove worktrees whose work is long since shipped.
If the git-tree-merged plugin is on your PATH, ccwt asks
it instead: it compares tree hashes rather than SHAs, so a branch whose content is on
main byte for byte reads as merged however the history was rewritten. Without it ccwt
falls back to plain ancestry, which errs the safe way — a missed ✓, never a wrong one.
ccwt ps answers the other half of that question — not what each worktree is about, but
what is running in it right now, as a process tree per worktree:
frolicking-tumbling-duckling
49757 -zsh
79860 -zsh
9935 ↳ claude --worktree frolicking-tumbling-duckling
snug-juggling-dream
23136 -zsh
23730 ↳ go test ./...
A tree's root is the topmost process whose working directory is in the worktree — the shell
you started there, or a leftover test server nobody stopped — and under it hangs whatever it
spawned, wherever that one's working directory has since wandered off to. By default only
the first generation is shown, which is the shell and what it's busy with; --depth/-d
goes further (-d 0 for the shells alone, --depth=-1 for everything). Worktrees with
nothing running in them are left out, and -g spans every known project, a section
per project, as ccwt list -g does.
The tui shows the same thing on P, kept up to date, and there
a process is somewhere you can go.
ccwt tui — or just ccwt, which is what a bare invocation runs — shows that same table
full-screen and keeps it up to date, repainting in place so it doesn't flicker, with a
status bar along the bottom:
NAME BRANCH AGE AGENT TOPIC
* * dreamy-foraging-hickey worktree-dreamy-…-hickey 2h yes ✳ Goal was a widget on the dashboard; it's built and…
✓ calm-baking-otter worktree-calm-bak…-otter 1d no ⎇ Fix the flux capacitor
☰ q:quit p:pull g:git /:search l:log a:agents x:new space:open n:queue m:vcs d:details r:remove │ main ↑1 v1.2.3
q (or Ctrl-C) quits, p runs git pull and reports the result in the bar — though while the branch is
behind what the background fetch already brought in, it only fast-forwards to that, which
takes no network; once caught up, p again fetches for real. The rest of
the bar is the branch you launched it from and how far it has drifted from its upstream
(↑ ahead, ↓ behind), and the far corner is the version of the ccwt binary drawing it —
dropped on a terminal too narrow to hold both it and the keys. Those counts are kept honest by a background git fetch of
origin/main alone; --fetch (default 1m) sets how often, or 0 never. --interval
(default 2s) sets the refresh rate.
The arrow keys (or j/k) select a worktree, and the bar grows the actions that apply to
it: r removes it, refusing an unmerged branch, uncommitted changes or a working agent,
exactly like ccwt remove. Clicking a row selects it. Three more keys — x, c and
space — appear only under Herdr.
Dragging selects text and copies it. A terminal stops selecting for itself once an
application asks to be told about clicks, so the tui does the selecting: press, drag, let go,
and what the drag covered — from where it started to where it ended, whole lines in between,
as text reads — goes on the clipboard (pbcopy, wl-copy or xclip, and OSC 52 where
there's none of those, so it works over ssh too). The selection stays inverted until the
next keystroke, and a press that never moved is still an ordinary click.
The ☰ in the corner is that same list of actions as a menu: click it and they drop out of
the bar, one per line, to be clicked instead of typed (the arrows and ↵ work too, esc
closes it, and clicking the ☰ again shuts it). A menu entry is the key it names — picking
one does exactly what typing it does — so the menu and the bar can't drift apart. Two entries
are only there: G runs ccwt gc on the repo in view,
collecting every worktree that's merged, clean and idle in one go — rare enough that the bar
has no room for it, and picking it is the confirmation — and P swaps the worktrees for
what is running in them.
g shows the history in a window over the list: whatever your own git ll alias prints,
run in the worktree the selected row is in — or in the repo itself when the row isn't in
one. It's the same kind of page the worklog opens, and scrolls the same way: the arrows (or
j/k) by a line, space/b by a screenful, g/G to either end, esc back to the
list. A git with no ll alias of its own has nothing to print, and says so in the bar.
m opens the worktree's review in your browser — the branch is here, the review is over
there. Which one that is depends on where origin points: a GitHub remote is a pull request
and gh finds it, a GitLab one is a merge request and
glab does. Either cli already knows which host the
remote is and which token to ask it with; ccwt only decides which of the two to run, and
hands the url it prints to open (macOS) or xdg-open. Without the cli installed, or on a
branch with no review yet, the bar just says there's none to open.
github.com, gitlab.com, and any host with gitlab in its name are recognised as they
are. A self-hosted instance that doesn't keep the word — code.example.com — needs a line
in ~/.config/ccwt/config.toml, since there's nothing in the hostname to go on:
[forges]
"code.example.com" = "glab"
d opens the selected worktree's details as a window over the list: every column's value in
full, one per line and wrapped to the width, instead of the row the table had to cut down to
fit. It's modal — esc closes it, and while it's up the other keys do nothing (bar e on a
queued prompt) and the list behind it holds still. Every column the table
knows about is in there, including any the config leaves out of the list.
NAME BRANCH AGE AGENT TOPIC
✓ calm-baking-otter worktree-calm-bak…-otter 1d no ⎇ Fix the flux capacitor
┌─ dreamy-foraging-hickey ───────────────────────────────────────────────────────┐
│ NAME dreamy-foraging-hickey │
│ BRANCH worktree-dreamy-foraging-hickey │
│ AGE 2h │
│ AGENT yes │
│ TOPIC ✳ Goal was a widget on the dashboard; it's built and wired up, and the │
│ numbers on it are still the mocked ones │
└────────────────────────────────────────────────────────────────────────────────┘
P swaps the worktrees for what is running in them — ccwt ps,
in the tui's own window and refreshed on the same interval, -g included when the tui was
started with it:
RUNNING
calm-baking-otter
23136 -zsh
23730 ↳ go test ./...
dreamy-foraging-hickey
49757 -zsh
79860 ↳ claude --resume
☰ q:quit /:search space:go esc:worktrees │
It's the same list, not a window over one: the arrows walk it, / and n/N search it,
clicking selects, and the rows scroll the way the worktrees do. What changes is what a row
is, and so what there is to do with one — space goes to where it is. On a process that
means the pane it's running in, the same place ccwt nav ps would
take you (herdr or tmux, whichever it's under); on the worktree line above it, the workspace,
as space does on the list. esc brings the worktrees back — the first esc clears the
search, if there is one — and so does another P.
A process's pane is an environment variable it inherited, and macOS hides the environment of
everything under /bin: a shell can't answer for itself, so its descendants are asked
instead, which is why going to a bare -zsh still lands in the right place as long as
something is running under it.
Removing a worktree also removes the only record of what it was for: the branch is deleted,
the directory is gone, and the agent files its transcript under a path that no longer
exists. So ccwt remove writes a line about it first — when it went, what it was called,
and the TOPIC the list showed — into $XDG_STATE_HOME/ccwt/tasks.db, next to the queued
prompts, and every removal goes in: ccwt remove, ccwt done, ccwt gc, and r in the
tui.
l shows that log as a window over the list, newest first; esc (or l again) closes it.
Its rows select like the list's — the arrows (or j/k) walk them, a click lands on one —
and ↵ opens the selected removal's page.
NAME BRANCH AGE AGENT TOPIC
✓ calm-baking-otter worktree-calm-bak…-otter 1d no ⎇ Fix the flux capacitor
┌─ worklog ──────────────────────────────────────────────────────────────────────┐
│ AGE NAME TOPIC │
│ 2h kind-munching-melody ✳ Kept a log of removed worktrees, with a pane in… │
│ 1d dreamy-foraging-hickey ⎇ Quote a queued prompt on its way to th… (#79) │
└────────────────────────────────────────────────────────────────────────────────┘
That page is everything left to know about one removed worktree: what the log recorded, and
then the whole of the last Claude Code session that ran in it — the prompts, what the agent
said back, and the tools it reached for, which survive the removal because transcripts are
filed under your home directory rather than in the tree they were about. It scrolls:
the arrows (or j/k) by a line, space/b by a screenful, g/G to either end, esc
back to the log.
┌─ dreamy-foraging-hickey ───────────────────────────────────────────────────────┐
│ NAME dreamy-foraging-hickey │
│ PROJECT /Users/you/src/ccwt │
│ REMOVED 1d ago, 2026-08-12 09:31 │
│ PATH /Users/you/src/ccwt/.claude/worktrees/dreamy-foraging-hickey │
│ TOPIC ⎇ Quote a queued prompt on its way to the pane (#79) │
│ SESSION ~/.claude/projects/-Users-you-src-ccwt--claude-worktrees-dreamy-… │
│ │
│ › the prompt reaches the pane unquoted, fix it │
│ │
│ The prompt is pasted into a shell line, so it needs shell quoting. │
│ │
│ ⚒ Read: tui.go │
└────────────────────────────────────────────────────────────────────────────────┘
Tool results are left out — they are the bulk of the megabytes, and what a session did
reads perfectly well from the prompts, the prose and the calls. The SESSION line says
where the raw transcript is if you want the rest of it.
ccwt worklog prints the same table on the command line, -n (default 20) saying how many
lines of it. -g spans every known project instead of this repo's, and adds a
PROJECT column, since a worktree name on its own doesn't say which repo it was in; the
pane does the same when the tui is running under -g.
Work you've thought of but can't start yet — because it needs the worktree you're in to be
finished first — goes on the list as a prompt queued behind it. n, for what to do next,
opens a box over the middle of the list, and what you type there is recorded under the
selected row:
NAME BRANCH AGE AGENT TOPIC
* * dreamy-foraging-hickey worktree-dreamy-…-hickey 2h yes ✳ Goal was a widget on…
✓ calm-baking-otter worktree-calm-bak…-otter 1d no ⎇ Fix the flux capacitor
┌─ dreamy-foraging-hickey ──────────────────────────┐
│ rebase onto main, open the PR, and then port the │
│ widget to the mobile layout█ │
│ │
└───────────────────────────────────────────────────┘
↵:queue ctrl-g:$EDITOR esc:cancel
It's three fifths of the terminal, that size whatever is in it — a box that grew as you
typed would shift the list behind it line by line — and the row the prompt will hang off is
one of the ones still showing around it. ↵ records it, esc throws it away. Behind
another queued prompt the box's top rule names that prompt instead, since a chain can have
several links and the worktree's name wouldn't say which one you're extending.
Above it is a one-line title box, as on ccwt ws's seed prompt. The box opens there:
↵ or tab takes you down to the prompt, and shift-tab takes you back up. Leaving it
empty is fine. A queued prompt with a title shows the title in TOPIC, and the details pane
and e still have the prompt in full. When the prompt starts, its title becomes the
agent's --name and the workspace's label.
Once recorded it's a row of the list:
NAME BRANCH AGE AGENT TOPIC
* * dreamy-foraging-hickey worktree-dreamy-…-hickey 2h yes ✳ Goal was a widget on…
↳ <queued> 4m rebase onto main and open the PR
↳ <queued> 3m then port the widget to the mobile layout
✓ calm-baking-otter worktree-calm-bak…-otter 1d no ⎇ Fix the flux capacitor
It has no worktree of its own yet, and <queued> in NAME says that's why the column is
otherwise empty, the way <new> does below for a prompt whose turn has come.
n on a queued prompt queues another one behind that one instead, so a chain of "and
then" is a tree hanging off the worktree it starts from. d shows a long prompt in full in
the details pane, and r deletes one — along with everything queued behind it, since
nothing waiting on a prompt that isn't going to happen can happen either.
e in that details pane — where the prompt is legible in full, which is where you notice it
needs a word changing — reopens it in the same box for rewriting. The edit box takes the
pane's place rather than stacking on it, ↵ saves, and the prompt stays where it is in the
chain: whatever was waiting on it still is.
The box is a line editor, since the word that needs changing is rarely the last one, and its
keys are the ones the zsh line editor and Claude Code's own box use: ← and → move the
caret, Ctrl-←/→ (or Alt-B/F) move it a word at a time, ↑ and ↓ (or Ctrl-P/N) move it
a line at a time, home and end (or Ctrl-A and Ctrl-E) go to either end of the line,
backspace and delete take the character on their own side of it, and Ctrl-W, Alt-D,
Ctrl-U and Ctrl-K rub out the word before the caret, the word after it, the rest of the line
before it and the rest of the line after it. Text goes in where the caret is, and the box
scrolls to wherever that is, so the start of a prompt taller than the box is reachable.
Shift-↵ puts a line break in — ↵ on its own records the prompt — so a prompt can be a
paragraph rather than a sentence. Only ↵ and esc close the box: a key it doesn't know,
whatever your terminal sends for Cmd-←, does nothing at all rather than throwing away what
you have typed. (A terminal has to be told to send something for shift-↵, since by default
it sends the same thing ↵ does. Claude Code's own /terminal-setup does that for iTerm2
and VS Code, and this box takes what it binds; Ctrl-J is the line break everywhere else.)
The table folds the breaks back to spaces for the one row it has to draw the prompt on.
For a prompt that wants more than that, Ctrl-G finishes it in $EDITOR — the same key
Claude Code's own prompt box binds it to, since it's the same box for the same purpose. The
tui hands over the screen and the keyboard, opens $VISUAL, $EDITOR or vi on what
you've typed so far, and takes back whatever you save, with the list drawn again around it.
An editor that quits without saving leaves the prompt as it was.
A long paste — over 800 characters or two line breaks — goes in as [Pasted text #1 +40 lines], the marker Claude Code's own box puts in for one, so what you type around it stays
in view. The paste goes back in its place when the prompt is queued or its agent started, and
$EDITOR gets it in full, with the marker back over it if it comes back untouched. A paste
is text however long it is: a line break in one is a line break, not the ↵ that records the
prompt.
n is also vim's next-match key, and while a search pattern is in force that's what it
stays: /, then n and N to walk the matches, and esc to clear the pattern and get the
queue key back. The bar says which one is live.
Removing the worktree does not take the chain with it — it starts it. Every prompt that
was waiting on that worktree moves up to a row of its own, called <new> because its turn
has come and it hasn't got a worktree yet, with whatever was queued behind it still under
it:
NAME BRANCH AGE AGENT TOPIC
✓ calm-baking-otter worktree-calm-bak…-otter 1d no ⎇ Fix the flux capacitor
<new> 2h rebase onto main and open the PR
↳ <queued> 2h then port the widget to the mobile layout
space (or a double-click) on a <new> row is where the worktree finally gets made: a
fresh one, opened as its own Herdr workspace the way c
opens one — ccwt ws in its first tab — with the prompt running in a tab of its own next to
it — claude "<the prompt>", or whatever task_command in the config says, e.g.
task_command = "claude --permission-mode plan"
The prompt itself travels in a file — the pane is handed claude "$(cat …)", and the file
deletes itself as it is read. That command is typed at the pane's shell prompt, and a
terminal takes about a kilobyte of a typed line before it drops the rest, which a prompt
worth queueing is easily longer than; a filename is the same hundred characters however long
the prompt is, and the shell never has to be taught to read back the quotes, newlines and
backslashes in it.
The row is then a worktree like any other, and the rest of the chain hangs off it, waiting on the work that has just started rather than on the prompt that started it.
A <new> row is also where work that isn't waiting for anything starts, and n with
nothing selected is how you write one down: there's no row to hang it off, so it goes on the
list as a <new> of its own straight away — a worktree that hasn't been made yet, with the
prompt to run in it when it is. esc is how you get back to nothing selected, and space
on the row makes the worktree, exactly as it does for one that got there by having its
worktree removed. Under -g this needs a project selected, its section header being enough
to say which repo the worktree is to be made in; with nothing selected there the bar drops
the key.
The queue lives in $XDG_STATE_HOME/ccwt/tasks.db (~/.local/state/ccwt/tasks.db by
default) — one SQLite database for every project, so a prompt queued in a project's own tui
shows up in tui -g and the other way round, within a refresh interval either way.
The same database keeps a seen table: every repo and Claude Code worktree ccwt has been
run in or has made, with when it was first and last used and how many times — nothing reads
it yet.
/ searches, as in vim or less: type a pattern into the bar and the selection moves to the
match as you type, enter accepts it, esc puts back the pattern and the row you started
from, and n/N walk the rest of the matches forwards and back, wrapping around the ends.
? searches upwards instead. Every match on screen is picked out, not just the selected
one; a bare esc clears them, and clears the selection with them — nothing is selected
again, and the bar drops the keys that only apply to a row.
The pattern is a regular expression, always
case-insensitive, matched against the whole line as drawn — name, branch, age and topic
alike, so /✓ finds the worktrees whose branch is merged and /yes the ones with an agent
running in them. One that doesn't compile simply matches nothing, which is what half of one
is while you're still typing it. Under Herdr, where each row leads with its agents'
dot, what a search sees there is the symbol the dot stands in
for — × blocked, ◐ working, ✓ done, ○ idle, · none — so /✓ finds the finished
turns along with the merged branches.
Something parked in a pane for days goes on running the binary it started from, so the tui
watches that binary and says so when it's replaced — by go install, by a package manager,
by a downloaded release:
☰ q:quit p:pull /:search │ upgraded to v0.64.0 — restarting when idle
Then it does: once the keys have been quiet for four seconds, with nothing typed and no pane
open, the process replaces itself with the new binary — an exec, so it keeps its pid, its
pane and its flags — and the list comes back fresh; the selection and any pattern in force
are what doesn't survive it. The version is the one you're restarting into (asked of the new
binary itself; it's dropped from the line if it won't answer). Windows has no exec, so
there the notice reads restart ccwt and stays up until you do.
-g makes ccwt list and ccwt tui span every project you've configured instead of just
the repo you're standing in, a section per project, each holding that project's worktrees
newest-first:
NAME BRANCH AGE AGENT TOPIC
▾ ccwt (2)
* dreamy-foraging-hickey worktree-dreamy-…-hickey 2h yes ✳ Goal was a widget on…
✓ calm-baking-otter worktree-calm-bak…-otter 1d no ⎇ Fix the flux capacitor
▸ platform (14)
In the tui the sections fold: select one and press ↵, or double-click it, and it collapses to
the header line — the count stays, so you can see what's tucked away. platform above is
folded shut. Folding has its own key so that it can't misfire into opening a worktree:
space only opens, ↵ only folds.
It works from anywhere — including outside a git repository — and the directory you launch
it in takes no part in it: under -g the repos are the known ones and nothing else.
Every action applies to the project the selected row belongs to: r removes from that
repo, x creates one in that project — on a section header too, which is how you make the
first worktree in a project that has none — p pulls it, and the status bar shows that
project's branch state. With nothing selected yet there's no repo to act on, so those keys
say so instead of reaching for the current directory. The background --fetch covers every
known project.
The known projects are every repo ccwt has been run in, in any of its worktrees, in the
order it first was — kept in $XDG_STATE_HOME/ccwt/tasks.db, and left out once the repo is
no longer there. "hide" in the ☰ menu, on a project's section header, leaves one out for
good, however much ccwt goes on being run there. Under Herdr, o lists the known projects,
most recently used first, ● those Herdr has a workspace on and ○ the rest — typing filters
them, ↑↓ or a click selects — after which p runs git pull in it and P copies its path — ↵ or a second click switches to an open one's workspace, or
opens a closed one: a new workspace in the repo, with ccwt running in its first tab, named
prj. A repo it hasn't been run in yet, or one to put at the top, goes in
$XDG_CONFIG_HOME/ccwt/config.toml (~/.config/ccwt/config.toml when that variable isn't
set) — each entry the main checkout of a repo, the one whose .claude/worktrees/ the
worktrees live under:
[[projects]]
path = "~/src/ccwt"
[[projects]]
path = "~/src/platform"
Categories name the projects checked out under a directory (the longest match wins). Under
Herdr, a workspace named ------ <name> heads a category's section of the sidebar, and a
project o opens goes last in its own, just above the next ------ divider:
[[categories]]
name = "work"
path = "~/w"
[[categories]]
name = "personal"
path = "~/p"
branch_prefix sets what ccwt new puts in front of a worktree's name to make its branch
(worktree- when unset), for repos that want their branches namespaced:
branch_prefix = "mkm/"
It applies from the moment you set it: worktrees created under an older prefix keep their
branches, and ccwt remove on one of them leaves that branch behind rather than deleting
it — remove it with git branch -D if you want it gone.
columns picks which columns ccwt list and ccwt tui draw, in the order you name them
— name, branch, age, agent, topic, all of them when unset:
columns = ["name", "age", "topic"]
The */✓/±/☐/✳ markers ride on name, so leaving it out leaves them out too.
sort is the order ccwt list and ccwt tui put the worktrees in — freshness when
unset, the last commit or the last thing written to the newest session there, whichever is
younger, or commit for git's answer alone. --sort overrides it for one run:
sort = "commit"
task_command is the agent cli — which agent ccwt starts for you, and the one place that
choice lives: what a queued prompt is handed to when its worktree is made,
and what the tui's c runs on its own. It is claude when
unset, and split on spaces so flags get through:
task_command = "claude --permission-mode plan"
forges says which cli knows about a host's reviews, for the tui's m key and
for ccwt mr, which asks GitHub for a "gh" host and GitLab for any
other. Only a host whose name doesn't give it away needs a line — a self-hosted GitLab most
of all, since github.com, gitlab.com and anything with gitlab in the name are guessed:
[forges]
"code.example.com" = "glab"
environments renames an environment for ccwt mr's ENV
column: the name GitLab or GitHub knows it by on the left, what to call it on the right.
Deployment pipelines name environments to be unambiguous in a settings page, which is
longer than a column you only glance at wants:
[environments]
production = "prod"
staging = "stg"
ccwt config view prints that file and ccwt config edit opens it in $EDITOR, either one
creating an empty file first when you don't have one yet.
Herdr manages terminal workspaces for coding agents. ccwt doesn't need
it — everything above works on its own. When Herdr is running, these extras appear, and
this section is the whole of it:
The ✳ marker. In ccwt list and the tui, ✳ in the leading glyphs means Herdr says an
agent is working in that worktree right now. It outranks *, ±, ☐ and ✓, and it's the case git
can't see at all: a branch made a minute ago with nothing committed to it is merged and clean.
It comes from Herdr rather than from the agent's own files, so it holds for whatever agent is
running there — unlike the AGENT column, which knows Claude Code alone. ccwt remove,
ccwt done and ccwt gc all refuse a worktree marked this way (ccwt remove -D interrupts
the agent anyway).
Agent dots. In the tui, each worktree's row leads with Herdr's own mark for the agents in
it, ahead of the other two glyphs — the dot ccwt ws, below, gives a tab, by the same rules:
red blocked, yellow working, green done, grey idle, and a · where Herdr has no agent.
Several agents in one worktree show whichever most wants you, and green is a turn that has
ended since anyone last looked at its tab, which going there — space — clears. An agent is in
the worktree its own directory or its pane's is in, as for ✳.
NAME BRANCH AGE AGENT TOPIC
● ✳ dreamy-foraging-hickey worktree-dreamy-…-hickey 2h yes ✳ Goal was a widget on the dashboard; it's built and…
○ ✓ calm-baking-otter worktree-calm-bak…-otter 1d no ⎇ Fix the flux capacitor
AGENT goes on saying whether Claude Code is running there. ccwt list has no dots: it draws
the table once, with nothing to remember what you have read, and would call every finished
turn unread.
Opening workspaces from the tui. space opens the selected worktree as its own Herdr
workspace (via herdr worktree open), and a double-click on its row does the same. x
creates a worktree and opens it without leaving the list, and c does that and starts
ccwt ws in it — the workspace's own tui, below, which is where its agents are seeded
from. These keys only exist when the tui
is itself running in a Herdr pane — elsewhere there's no session to open a workspace in, so
they're dropped from the bar and do nothing.
Workspace naming. A workspace opened on a new worktree is named after the worktree:
Herdr would otherwise list it under its repo by branch, prefix and all. Reopening one leaves
its name alone, so a workspace you've renamed yourself stays renamed. And the name is what
NAME shows, in ccwt list and the tui alike, when they run in a Herdr pane: a worktree open
as a workspace goes by the workspace's name, so the list reads like Herdr's sidebar. Piped
output keeps the worktree's own name, which is the one ccwt remove takes.
A workspace's own tui: ccwt ws. The tui above is the repo's view: its worktrees,
whatever workspace each is open in. ccwt ws is a workspace's, in two sections: where the
workspace's review stands, and under it the tabs working towards it.
MR STATUS PIPELINE TITLE
acme/…/api!42 needs approval failed: unit, lint Wire the widget to real numbers
TAB AGENT DIR TITLE
* 1 · dreamy-foraging-hickey zsh
2 ● dreamy-foraging-hickey Wiring the widget to real numbers
3 ○ dreamy-foraging-hickey Waiting on your review
☰ q:quit /:search n:agent space:go g:git │ started
The first section is what ccwt mr prints for the branch the workspace is
working on — can it go in, and did its pipeline pass — which is the question the whole
workspace exists to get a yes to. A fresh worktree hasn't got one yet, and says so in a line
rather than in a row of column names with nothing under it:
no merge request yet
The lookup is a handful of GitLab or GitHub round trips, so it runs in the background and is asked again at most once a minute: the table you're reading never waits on the network, and the row appears under the heading a moment after the review does.
The second section is the workspace's tabs — the tab's label, whether Herdr sees an agent
working in it, the directory it sits in, and what its terminal calls itself, which for an
agent is its own one-line account of what it is doing — and space (or ↵, or a
double-click) goes to the selected tab. A * leads the tab the tui itself is in. The rows
come in the order the tabs sit along the tab bar, so dragging one about in Herdr moves its
row with it. They're the only rows the selection walks: the review above them is something to
read, and it stays put while they scroll.
The AGENT mark is Herdr's own: a circle, and the state is the colour of it —
red blocked, yellow working, green done, grey idle, and a · for a tab Herdr sees no agent
in at all. Herdr keeps that state per agent rather than per tab, so a tab split between
several shows whichever of them most wants you: an approval waiting on an answer first, then
a turn that has ended with something to read, then work still going on. Herdr draws the three
busy states as the same circle too, so a workspace reads the same in the tab bar and in the
table below it — in the same colours, too: the dots are asked for by palette number, and
Herdr's theme is what answers.
Green is the one mark the table keeps itself. Herdr's done is the server's seen state, and
one look spends it for good: sit in a tab until its agent finishes, and every turn it ends
after that is plain idle, however far away you were for it. Herdr's own tui doesn't take
that answer either — it keeps the badge per client, each tracking what it has been shown. So
this one goes on when an agent has settled and has moved since the last time anyone was
looking at its tab, and comes off when you go: space, or Herdr saying that tab is the one
you are looking at.
What it reads that off is state_change_seq, Herdr's own counter for when an agent last
changed state, rather than off watching for the change itself — which means a ccwt ws that
has only just come up still says which tabs have something waiting in them. A conductor that
has to witness the turn end to colour it says nothing about the ones that ended first, and
ccwt ws restarts itself on every ccwt upgrade. The cost is a third round trip to Herdr per
round, next to the tab and pane lists the table is built from.
n opens the same box the queue types into, and what goes in it is a seed
prompt: ↵ opens a tab of the workspace in the workspace's own worktree — the one the tui is
standing in — and runs task_command there with the prompt. No worktree and no tab name of
its own: the agents of one workspace are working on one thing, in one checkout, and an
unnamed tab is named by what runs in it, which is the agent saying what it's doing. The tab
opens behind the one you're typing in, so the table shows it and you go when you're ready. A workspace with no
other tab yet opens straight on that box, since the first thing to do in a fresh workspace is
say what it's for; esc puts it away. g shows the history of the worktree under the
selected tab, and /, the mouse and the ☰ are the tui's own.
When the agents are Claude Code — task_command is claude, or unset — and Claude Code
hasn't been told to trust the repository yet, ccwt ws asks that first, over everything
else, the seed prompt included: an agent started in a tab nobody is looking at would
otherwise sit at Claude Code's own trust dialog with your prompt waiting behind it. y
records the answer where that dialog would, in ~/.claude.json (or $CLAUDE_CONFIG_DIR's),
under Claude Code's own lock, and on the repository's main checkout — which is what Claude
Code files it under, so one yes covers every worktree of it. n leaves Claude Code to ask
for itself. The home directory is never asked about: Claude Code only ever trusts that one
for a session.
r is the other end of that: ccwt done for the workspace itself — the
worktree the tui is standing in is removed, branch and all, and the workspace closes behind
it, this tui with it. It's on the bar only when that removal would go through as it stands:
branch merged, nothing uncommitted, and no agent still working in there — the three checks
ccwt remove makes before -D, asked of the very same code, so r on the bar means this
workspace is finished with. Whether it's there is a property of the workspace and not of any
one tab, so it doesn't come and go as you walk the table: it's on the bar or it isn't,
whichever row is selected. A tui run somewhere that isn't a worktree — the repo itself —
never has one.
This is part of a longer plan — what each tab is about, and which tickets and reviews each
one touches — and TODO.md has the rest of it.
A workspace whose ccwt ws has gone. The tab c starts ccwt ws in is named ws, and
the name outlives the tui: quit it, or restart Herdr — which brings the tabs back but not what
was running in them — and the tab is a bare shell under the same name. The tui names each of
its worktrees' workspaces in that state, in a yellow box in its bottom-right corner:
┌─ ccwt ws not running ↻ ─┐
│ audit restore │
└─────────────────────────┘
☰ q:quit p:pull g:git /:search l:log a:agents x:new c:new+ws n:queue │ main
It listens to Herdr's events, and looks again whenever a tab or a workspace is made, renamed
or closed — and every ten seconds besides, since Herdr says nothing when a program exits back
to its shell. It covers only the workspaces open on the worktrees it lists — the ones Herdr
shows under the repo, or under any known project with -g — and leaves another repo's to
that repo's tui. Only the tui does this, not ccwt ws, and only when it is running in a Herdr
pane.
With -g the box is ccwt tui not running instead, and names the projects' own workspaces —
the ones open on each configured repo's root checkout — that have no ccwt in any pane of any
tab: the repo tuis that are gone.
A click on the ↻ in its title starts them again, once you have read the plan it shows and
said y to it. Each workspace gets its tui back in the tab it belongs in — prj for a
project's own workspace under -g, running ccwt; ws for a worktree's, running ccwt ws.
A workspace with no such tab gets one, made first in its bar; one whose tab is at its shell
has the tui typed there; one whose tab is busy with something else is left to it, and the plan
says so.
Closing workspaces on removal. Once its checks pass, ccwt remove closes the workspace
open on the worktree, ending the agent living in it — including your own, which is closed
last of all, after the worktree is gone and the shell has been cd'd out. Because a workspace
vanishing is a bigger surprise than a directory, a terminal is asked first: remove <name> and close its herdr workspace? [y/N] on stderr, answered "no" by default. -y/--yes skips
the question, and so does a ccwt remove whose stderr isn't a terminal — a script or the tui
gets on with it. ccwt done is ccwt remove . -y. Closing the workspace you're looking at
would leave Herdr focused on whatever it had open before — some unrelated pane — so when
yours is the one on screen, the repo's own workspace, the one open on the root checkout, is
focused first: you land back in the repo the worktree came from. Switch to another workspace
while the removal runs and nothing moves — closing a workspace nobody is looking at takes no
focus with it, and where you switched to is where you meant to be.
The plugin in herdr-plugin/ adds a New ccwt worktree action to Herdr — the same
thing the tui's x does.
The tui is meant to be parked in a pane — e.g. the main pane of a Herdr workspace — as a live view of what's running where.
Claude Code has a built-in --worktree flag that runs a session in its own worktree.
The catch: Claude creates and uses that worktree internally, but your terminal tab's
working directory stays at the base project root. Open a new tab or split and you land
back in the base repo, not in the worktree the session is actually using.
ccwt new fixes this by creating the worktree up front:
claude --worktree;Terminals and multiplexers that honour OSC 7 (iTerm2, Ghostty, WezTerm, tmux/cmux, …)
will then open new tabs and splits in the worktree directory. Because ccwt and Claude
Code share the same .claude/worktrees/<name> layout, the name printed by ccwt new is
exactly what claude --worktree expects.
This used to be doable with a Claude Code hook that printed the escape code itself, but Claude Code no longer lets hooks emit raw escape sequences to the terminal. Emitting it from
ccwt newin a small wrapper aroundclaudeis the way to get it back.
Save this as claude-wt somewhere on your PATH, chmod +x it, and run it instead of
claude. It allocates a fresh worktree per session unless you ask for a specific one:
#!/usr/bin/env bash
# claude-wt — run each Claude Code session in its own ccwt worktree, and let the
# terminal's cwd follow into it (via the OSC 7 sequence ccwt emits on stderr).
#
# claude-wt # fresh worktree, terminal cd's into it
# claude-wt --worktree foo # use the worktree named "foo"
# claude-wt --no-worktree # skip worktree handling entirely (wrapper-only flag)
set -eo pipefail
args=()
has_worktree=0
no_worktree=0
for arg in "$@"; do
case "$arg" in
--no-worktree) no_worktree=1 ;; # wrapper-only; drop it
--worktree|--worktree=*) has_worktree=1; args+=("$arg") ;;
*) args+=("$arg") ;;
esac
done
if [ "$has_worktree" -eq 0 ] && [ "$no_worktree" -eq 0 ]; then
# `ccwt new` prints the worktree name on stdout (captured here) and emits the
# OSC 7 cwd report on stderr, which we deliberately let flow to the terminal.
worktree=$(ccwt new)
args=(--worktree "$worktree" "${args[@]}")
fi
exec claude "${args[@]}"
Naming the wrapper something other than claude (here, claude-wt) keeps exec claude
from re-invoking the wrapper itself. If you'd rather call it claude, point the exec
line at the real binary by absolute path instead (e.g. exec "$HOME/.local/bin/claude").
The key detail is that command substitution — worktree=$(ccwt new) — captures only
stdout, so the OSC 7 sequence on stderr still reaches the terminal. Don't redirect or
swallow stderr, or you'll lose the cwd report.
| Command | Description |
|---|---|
ccwt new [name] | Create a worktree under .claude/worktrees/<name> on a new branch worktree-<name>, and print <name>. Generates a name if omitted; reuses an existing worktree of the same name. --switch <branch> checks the worktree out on an existing branch instead of creating one (ccwt new + git switch <branch>). When run inside a worktree it returns the enclosing one instead of creating a new one (override with --force-create). --path prints the worktree's absolute path instead of <name>. |
ccwt cd <name> | cd into an existing worktree under .claude/worktrees/<name> (with shell integration) — never creates it, errors if it doesn't exist, and the name is required. ccwt cd .. is shorthand for ccwt .., and ccwt cd - jumps to the previous directory ($OLDPWD), like the shell's cd -. |
ccwt list | List the repo's agent worktrees with branch, age, running-session status, and last commit, freshest first — the last commit or the last thing written to the newest agent session there, whichever is younger. --sort=commit orders by the commit alone, --sort=freshness is the default, and sort in the config file picks which one you get without the flag. -g lists every known project instead, a section per project. --no-headers leaves out the header row, for feeding the table to cut, awk or a shell loop. |
ccwt tui | The default command, so a bare ccwt runs it. Show the ccwt list table full-screen, refreshing in place without flicker, over a status bar showing how far the current branch is ahead/behind its upstream, with the ccwt version in its far corner. q (or Ctrl-C) quits, p runs git pull (a fast-forward to what was already fetched, while the branch is behind it). Arrow keys (or j/k) select a worktree, a click on its row selects it, dragging selects text and copies it to the clipboard, the ☰ in the bar's corner drops the bar's actions out as a clickable menu, / (or ?) searches the way vim does — incrementally, case-insensitive regexp, all matches highlighted, n/N for the next and previous while a pattern is in force; d shows the selected worktree's column values in full in a pane over the list (esc closes it), g shows the history your own git ll alias prints, in a scrolling page over the list, m opens the branch's review in the browser, and r removes it, like ccwt remove. a narrows the list to the worktrees an agent is running in — one herdr has, idle or not, or a Claude Code session — and a again brings the rest back. l shows the worklog — the worktrees already removed and what they were about — in a pane of the same kind, whose rows select like the list's; ↵ (or a double-click) opens the selected removal's page, which is what the log recorded about it followed by the whole agent session that ran in it, scrolled with the arrows, space/b and g/G. n with no pattern in force queues a prompt behind the selected row — work to start once that worktree (or the prompt above it) is finished — typed into a box over the list and drawn as a tree under the row it waits on, kept in $XDG_STATE_HOME/ccwt/tasks.db and so shared with every other ccwt running; e in a queued prompt's details pane rewrites it in place — the box is a line editor, with the arrows, home/end and Ctrl-W/U/K, shift-↵ for a line break, and Ctrl-G to finish the prompt in $EDITOR, as in Claude Code — r deletes it and everything queued behind it, and removing the worktree promotes what was queued on it to <new> rows — worktrees waiting to be made, which space makes and starts the prompt in. n with nothing selected queues a prompt that waits on nothing, which is a <new> row from the start; under -g that takes a project selected, its section header included. -g spans the known projects as a foldable section each (↵, or a double-click on the header, folds one shut) and ignores the current directory entirely, every action (p included) applying to the selected row's project. --interval (default 2s) sets the refresh rate, --fetch (default 1m) how often origin/main is fetched in the background, and --sort orders the worktrees, as it does on ccwt list. The bar also says when the ccwt binary underneath it has been upgraded, and to which version — and once the keys have been quiet for four seconds the tui restarts itself into it. Under Herdr space opens the selected worktree as a workspace (double-click does too), x creates one and opens it, and c also starts ccwt ws, the workspace's own tui, in it; each row leads with Herdr's dot for the agents in its worktree, as ccwt ws draws one for a tab; a box in the bottom-right corner names those of its worktrees' workspaces whose ws tab has no ccwt running in it. |
ccwt ws | The tui for the first tab of a Herdr workspace, in two sections. First the workspace's own review, as ccwt mr prints it for the branch the tui is standing on — one line saying there isn't one while the branch has none, and looked up in the background (at most once a minute) so the table never waits on GitLab or GitHub. Then that workspace's tabs as a table — label, agent status, directory, terminal title — with space/↵ (or a double-click) going to the selected tab, n asking for a seed prompt that starts an agent in a tab of its own (task_command run with the prompt in the workspace's own worktree, the tab unnamed and opened behind the current one), g the history of the worktree under the tab, r being ccwt done for the workspace itself — the worktree the tui is standing in removed and the workspace closed behind it, offered only when that removal would go through (merged branch, nothing uncommitted, no agent still working there), and so on the bar or not whichever row is selected — and / searching as in the tui. Opens on the prompt when the workspace has no other tab — and before that, when the agents are Claude Code and it doesn't trust the repository yet, on whether it may: y records the trust in Claude Code's own config, as its dialog would. --interval (default 2s) sets the refresh rate. Refuses to run outside Herdr. |
ccwt remove <name> | Remove the worktree at .claude/worktrees/<name> and delete its branch. . means the worktree you're currently in; removing the one you're in cds you to the repo root, like ccwt ... The branch is deleted only if merged: an unmerged branch refuses the whole removal, worktree included, so nothing is stranded, and so does a worktree with uncommitted changes, and so does one with an agent working in it (the ✳ of ccwt list). Pass -D to remove anyway (unmerged branch deleted, uncommitted changes thrown away, working agent interrupted), or --keep-branch to remove only the worktree. Under Herdr the worktree's workspace is closed too, once those checks pass — your own one last, after the removal, with focus handed to the repo's own workspace first if yours is the workspace on screen — and a terminal is asked before that happens (-y/--yes skips the question). |
ccwt done | Finish with the worktree you're in: ccwt remove . -y — the removal, and then, under Herdr, closing the workspace you're sitting in, without asking (closing it is what "done" means). Same checks and same flags (-D, --keep-branch); a refusal leaves the workspace open. Outside Herdr it's just ccwt remove .. |
ccwt gc | Remove every worktree that's finished with: branch already merged (the ✓ of ccwt list), nothing uncommitted in it (no * or ±), no agent working in it (no ✳) and no agent session running in it (a no in the AGENT column). Prints the list it found and asks before touching anything — -y/--yes skips the question. Each removal is exactly what ccwt remove <name> does, branch included. The worktree you're standing in is never removed — it says so on stderr and leaves it to ccwt remove .. |
ccwt worklog | Print the worktrees that have been removed, newest first, with what each one was about — the TOPIC its row had, recorded by the removal because nothing else survives it. -n (default 20) says how many lines. -g covers every known project rather than this repo's, with a PROJECT column saying which one each removal was in. The log lives in $XDG_STATE_HOME/ccwt/tasks.db, alongside the queued prompts, and l in the tui shows the same table. |
ccwt ps | Show what is running in each worktree, a process tree per worktree: every process whose working directory is in the worktree while its parent's isn't — the shell you started there, or something orphaned that outlived it — with what each one spawned underneath. --depth/-d (default 1) says how many generations below those roots to draw: 0 for the roots alone, --depth=-1 for the whole tree (spelled with an =, since a bare -1 reads as a flag). Worktrees with nothing running in them are left out. -g covers every known project, a section per project, like ccwt list -g. The working directories come from lsof, so a process whose cwd can't be read simply isn't placed in a worktree. |
ccwt mr <thing> (or ccwt pr) | Say where a review stands and what its pipeline did, without opening a browser: a table with a row per merge request, saying where each one STATUS stands — merged, can be merged, conflict, needs approval, open comments, merge train for one queued on a merge train or waiting on a pipeline to join it — GitLab itself calls that ci still running, which reads as a pipeline to go and wait on when it's the train's own and there is nothing to do about it — or whatever else GitLab's own mergeability check says, spelled out (need rebase, ci still running) — what its PIPELINE did: green, running, or failed: lint, test, …, naming the first few failed jobs; and the ENV it is running on — the environments whose current deployment carries the commit this merge request landed as (stg, stg, prod), which is empty until it's merged and released. [environments] in the config file renames them for the column (production = "prod"). <thing> is either a merge request url, which gets a table of the one row, or a Jira key (PROJ-1234) or its browse url, which gets a row per merge request that names the ticket, with the title to tell them apart. Left out entirely it's the merge request of the branch you're standing on; - reads a list of things off stdin, one per line, and puts all of their merge requests in the one table (a merge request that answers to two of them is still one row). On a terminal the MR column is a link to the merge request, a merged status is green — the one word either table spends colour on, so that the colour still means something when it turns up — and a spinner on stderr says the lookups are still going; piped or redirected, the table comes out as markdown — a rule under the header, the MR column as a markdown link, and nothing shortened to fit a width nothing has — so it lands in a merge request comment, a ticket or a chat message as a table rather than as columns that only line up in a monospace font. A merge request already merged or closed has no pipeline reported — it's history, and there's nothing to do about it either way — when none of the rows has one the PIPELINE column isn't drawn at all. ENV is always drawn, empty for a merge request that isn't running anywhere. --no-environments (it's on by default) fills ENV with ?? instead and sends none of those lookups — a deployments call per project and a merge-base call per environment — for when the answer isn't worth the round trips; ?? says nobody asked, which isn't what an empty cell says. It's read off the project's deployments (newest first, so the first mention of an environment is what's on it now) and answered by asking GitLab whether the environment's commit has the merge request's in its history — "on it now", rather than GitLab's own MR badge, which means "reached it at least once" and survives a rollback. It asks glab, which already knows the host and the token; the ticket's merge requests come from GitLab's own search for the key, so one that mentions the ticket only in a commit message or a comment isn't found. A review on GitHub is a pull request, and gets the same table from gh: a pull request url, or — with no argument, or a ticket — a repo whose origin is on GitHub, which is told apart the way the tui's m tells it. STATUS is in the same words — ci still running while checks nobody requires are still going, which GitHub itself counts as mergeable — plus draft, changes requested, behind, and merge queue for GitHub's merge train; one GitHub hasn't worked out yet is asked again a second later rather than shown as unknown. PIPELINE is the checks on its last commit, and ENV the repo's deployments, the same question asked of GitHub's compare. A ticket is searched for in the pull requests of whoever owns origin's repo, since all of github.com would be everybody's tickets. |
ccwt new-worktree-name | Print a generated worktree name (adjective-verb-noun) without creating anything. |
ccwt nav ps <pattern>ccwt nav ws <pattern> | Go to where something is. ps searches your running processes by command line and puts you in the pane the best match is running in; ws searches Herdr workspaces by title and by the name of the worktree each has open, and goes to the best match. Both patterns are fuzzy — the characters in order, not necessarily adjacent, case ignored, so ccwt nav ws mtls finds the workspace titled "mTLS subresource" and ccwt nav ws duckling the one holding frolicking-tumbling-duckling; of everything a pattern matches, the tightest run wins and the earlier one breaks the tie. ps finds the pane by reading the variable a multiplexer exports into it (HERDR_PANE_ID, TMUX_PANE) off the process itself rather than by asking the multiplexer, so Herdr and tmux both answer; it never matches ccwt itself or anything that spawned it, the pane you're already in being nowhere to navigate to. macOS hides the environment of system binaries, so a bare shell can't be found this way — search for the agent or the program running in the pane instead. --dry-run goes nowhere and prints where it would have gone instead, three lines: the workspace's name, the tab's name, and the tab's address — w16:t2, what herdr tab focus takes (under tmux the same three are the session, the window and the pane id). |
ccwt repo-root | Print the root of the current git repository. Add --root-worktree to print the enclosing repo root when you're inside a .claude/worktrees/<name> worktree. |
ccwt .. | Shorthand for repo-root --root-worktree: print (and, with shell integration, cd to) the enclosing repository root. |
ccwt init <shell> | Emit the shell-integration snippet to source from your rc file. For zsh the snippet carries completion too: commands, and worktree names for cd, remove and lock. |
ccwt config view / ccwt config edit | Print the config file, or open it in $EDITOR (vi if unset). Both create an empty one if there isn't any. The file holds [[projects]], repos for -g to span besides the ones ccwt has been run in, branch_prefix — what ccwt new puts in front of a worktree's name to make its branch (worktree- when unset) — columns, which columns the table draws, sort, the order the worktrees come in (freshness when unset), task_command, the agent cli a queued prompt is run with (claude when unset), forges, which cli finds a host's reviews for the tui's m and ccwt mr, environments, what ccwt mr's ENV column calls each environment (production = "prod"), and categories, which section of Herdr's sidebar o puts a project in. |
ccwt --version | Print version information. |
ccwt follows Claude Code's convention:
<repo-root>/.claude/worktrees/<name>worktree-<name> (branch_prefix renames the
worktree- part)Because the layout matches, worktrees you create with ccwt are visible to Claude Code
and vice versa.
A small command-line helper for managing coding-agent git worktrees.
An agent works best in a worktree of its own, one per task, under
<repo-root>/.claude/worktrees/<name>. ccwt lets you create, list, jump between, and
tear down those worktrees from your own shell, with a table saying what each one is about
and whether something is running in it.
It started out as a Claude Code helper, and Claude Code
is still hardcoded in a handful of places: the .claude/worktrees/ layout and the
adjective-verb-noun naming scheme (which is what keeps ccwt and claude --worktree
interoperable), the AGENT column, which looks for running Claude Code processes, and
TOPIC, the freshness sort and the worklog's session page, which all read Claude Code
transcripts. The rest — the worktrees, the table, the tui, the queue, the ✳ marker —
doesn't care which agent you run, and task_command is where you say which one that is.
go install github.com/mkmik/ccwt@latest
This drops a ccwt binary in $(go env GOPATH)/bin (make sure that's on your PATH).
A program can't change its parent shell's working directory, so on its own ccwt new
can only print the path of the worktree it created. To make ccwt actually cd you
into a worktree (and ccwt .. jump you back to the repo root), source the integration
snippet from your shell's rc file:
source <(ccwt init zsh)
Everything still works without it — you just won't get the automatic directory change.
Under zsh the same snippet also installs completion: ccwt <TAB> lists the commands,
and ccwt cd <TAB> (like remove and lock) lists the worktrees of the repo you're
standing in. Source it after compinit, which is what defines compdef — sourced
before, you keep the cd and silently get no completion.
ccwt new # create a fresh worktree on a new branch, and cd into it
# ... do your work, run your agent, commit, etc. ...
ccwt list # show all agent worktrees in this repo
ccwt list -g # ... or in every project ccwt knows
ccwt .. # jump back to the repository root
ccwt remove <name> # delete a worktree and its branch when you're done
ccwt remove . # ... or the one you're in: deletes it and cds you out
ccwt gc # ... or all the finished ones at once, after confirming
ccwt new creates a worktree at .claude/worktrees/<name> on a new branch
worktree-<name> (the prefix is configurable). With no name
it generates one (e.g. dreamy-foraging-hickey); pass a name to choose your own. Run from
inside an existing worktree, it returns that worktree instead of nesting a new one.
ccwt new --switch foobar # check out an existing branch instead of creating one
ccwt new mywt --switch foobar # ... in a worktree of your own name
ccwt new --path # print the absolute path instead of the name
.env and friends overA worktree is a fresh checkout, so the gitignored files your build needs — .env,
local credentials, editor settings — aren't in it. List them in a .worktreeinclude
file at the repository root and ccwt new copies them into every worktree it creates:
.env
.env.local
config/secrets.json
The syntax is .gitignore's. Only files that both match a pattern and are ignored by
git are copied, so a broad pattern can't duplicate a tracked file or drag in a stray
scratch file — and file modes carry over, so a 0600 secret stays 0600. This is the
same file Claude Code reads when it creates
a worktree, so either tool gives you the same result.
ccwt list renders a table of the repo's worktrees with their branch, age, whether an
agent session is currently running in each, and what each one is about:
NAME BRANCH AGE AGENT TOPIC
* * dreamy-foraging-hickey worktree-dreamy-…-hickey 2h yes ✳ Goal was a widget on the dashboard; it's built and…
✓ calm-baking-otter worktree-calm-bak…-otter 1d no ⎇ Fix the flux capacitor
Every row leads with two glyphs. The first is a * on the worktree you're currently in.
The second says whether the worktree can go: ✓ means its branch is already merged into
main (or master), so it's safe to ccwt remove — a squash, a rebase or a
cherry-pick counts too; ☐ means it isn't merged but every
commit is already on its upstream, so nothing here is waiting on you — it's waiting on a
review or on CI; * means it has uncommitted changes, which isn't safe to remove whatever
git makes of the branch or the remote, and ± means the same on a merged branch — the changes
are the only thing in there that isn't on main yet; ✳ beats them all and means an
agent is working in there right now — see Herdr integration.
The glyphs are omitted when stdout isn't a terminal, so piped output stays parseable. The tui
under Herdr puts a third ahead of both, the agents' dot.
The freshest worktree is on top. Freshness is the younger of two things: the worktree's
last commit, and the last line written to its newest agent session — so a worktree an
agent has been working in all afternoon without committing sorts above one whose commit is
newer but that nobody has touched since, and one an agent started twenty minutes ago sorts
to the top rather than to the bottom on its zero commits. --sort=commit is git's answer
on its own, which is what the list did before there was a choice; the default is
configurable and --sort overrides it, on both ccwt list and
ccwt tui. Under Herdr the worktrees open in a workspace come first, in the order of its
sidebar, and under -g so do the sections of the repos it has open; the rest follow as above.
In the tui the rest are folded away behind a ▸ … N more row, which a click or ↵ opens.
TOPIC says what the worktree is about, and its glyph says where that came from. ✳ is an
agent session — the newest transcript for that worktree, showing the last recap it produced
(/recap, or one the agent wrote on its own), or the first prompt you typed when it never
recapped. ⎇ is the last commit, for a worktree nobody has run a session in.
AGENT and TOPIC's ✳ both read Claude Code's own transcripts, so an agent that files its
sessions somewhere else reads as no with its last commit for a topic — the worktree, and
everything else the row says about it, is the same either way.
The table is sized to your terminal, so it never wraps. BRANCH is kept narrow even when
there's room — it's usually the worktree's own name with a worktree- in front — and the
space goes to TOPIC. Branches and commit subjects lose their middle rather than their tail,
since the last word is usually what tells them apart, while a session summary simply stops:
NAME BRANCH AGE AGENT TOPIC
* * dreamy-foraging-hickey worktree-dreamy-…-hickey 2h yes ✳ Goal was a widget on…
✓ calm-baking-otter worktree-calm-b…-otter 1d no ⎇ Fix the flux ca… (#41)
Squash, rebase and cherry-pick all rewrite commit SHAs, so the branch a PR landed from is
no ancestor of main afterwards — which is how most projects merge, and it leaves ccwt
refusing to remove worktrees whose work is long since shipped.
If the git-tree-merged plugin is on your PATH, ccwt asks
it instead: it compares tree hashes rather than SHAs, so a branch whose content is on
main byte for byte reads as merged however the history was rewritten. Without it ccwt
falls back to plain ancestry, which errs the safe way — a missed ✓, never a wrong one.
ccwt ps answers the other half of that question — not what each worktree is about, but
what is running in it right now, as a process tree per worktree:
frolicking-tumbling-duckling
49757 -zsh
79860 -zsh
9935 ↳ claude --worktree frolicking-tumbling-duckling
snug-juggling-dream
23136 -zsh
23730 ↳ go test ./...
A tree's root is the topmost process whose working directory is in the worktree — the shell
you started there, or a leftover test server nobody stopped — and under it hangs whatever it
spawned, wherever that one's working directory has since wandered off to. By default only
the first generation is shown, which is the shell and what it's busy with; --depth/-d
goes further (-d 0 for the shells alone, --depth=-1 for everything). Worktrees with
nothing running in them are left out, and -g spans every known project, a section
per project, as ccwt list -g does.
The tui shows the same thing on P, kept up to date, and there
a process is somewhere you can go.
ccwt tui — or just ccwt, which is what a bare invocation runs — shows that same table
full-screen and keeps it up to date, repainting in place so it doesn't flicker, with a
status bar along the bottom:
NAME BRANCH AGE AGENT TOPIC
* * dreamy-foraging-hickey worktree-dreamy-…-hickey 2h yes ✳ Goal was a widget on the dashboard; it's built and…
✓ calm-baking-otter worktree-calm-bak…-otter 1d no ⎇ Fix the flux capacitor
☰ q:quit p:pull g:git /:search l:log a:agents x:new space:open n:queue m:vcs d:details r:remove │ main ↑1 v1.2.3
q (or Ctrl-C) quits, p runs git pull and reports the result in the bar — though while the branch is
behind what the background fetch already brought in, it only fast-forwards to that, which
takes no network; once caught up, p again fetches for real. The rest of
the bar is the branch you launched it from and how far it has drifted from its upstream
(↑ ahead, ↓ behind), and the far corner is the version of the ccwt binary drawing it —
dropped on a terminal too narrow to hold both it and the keys. Those counts are kept honest by a background git fetch of
origin/main alone; --fetch (default 1m) sets how often, or 0 never. --interval
(default 2s) sets the refresh rate.
The arrow keys (or j/k) select a worktree, and the bar grows the actions that apply to
it: r removes it, refusing an unmerged branch, uncommitted changes or a working agent,
exactly like ccwt remove. Clicking a row selects it. Three more keys — x, c and
space — appear only under Herdr.
Dragging selects text and copies it. A terminal stops selecting for itself once an
application asks to be told about clicks, so the tui does the selecting: press, drag, let go,
and what the drag covered — from where it started to where it ended, whole lines in between,
as text reads — goes on the clipboard (pbcopy, wl-copy or xclip, and OSC 52 where
there's none of those, so it works over ssh too). The selection stays inverted until the
next keystroke, and a press that never moved is still an ordinary click.
The ☰ in the corner is that same list of actions as a menu: click it and they drop out of
the bar, one per line, to be clicked instead of typed (the arrows and ↵ work too, esc
closes it, and clicking the ☰ again shuts it). A menu entry is the key it names — picking
one does exactly what typing it does — so the menu and the bar can't drift apart. Two entries
are only there: G runs ccwt gc on the repo in view,
collecting every worktree that's merged, clean and idle in one go — rare enough that the bar
has no room for it, and picking it is the confirmation — and P swaps the worktrees for
what is running in them.
g shows the history in a window over the list: whatever your own git ll alias prints,
run in the worktree the selected row is in — or in the repo itself when the row isn't in
one. It's the same kind of page the worklog opens, and scrolls the same way: the arrows (or
j/k) by a line, space/b by a screenful, g/G to either end, esc back to the
list. A git with no ll alias of its own has nothing to print, and says so in the bar.
m opens the worktree's review in your browser — the branch is here, the review is over
there. Which one that is depends on where origin points: a GitHub remote is a pull request
and gh finds it, a GitLab one is a merge request and
glab does. Either cli already knows which host the
remote is and which token to ask it with; ccwt only decides which of the two to run, and
hands the url it prints to open (macOS) or xdg-open. Without the cli installed, or on a
branch with no review yet, the bar just says there's none to open.
github.com, gitlab.com, and any host with gitlab in its name are recognised as they
are. A self-hosted instance that doesn't keep the word — code.example.com — needs a line
in ~/.config/ccwt/config.toml, since there's nothing in the hostname to go on:
[forges]
"code.example.com" = "glab"
d opens the selected worktree's details as a window over the list: every column's value in
full, one per line and wrapped to the width, instead of the row the table had to cut down to
fit. It's modal — esc closes it, and while it's up the other keys do nothing (bar e on a
queued prompt) and the list behind it holds still. Every column the table
knows about is in there, including any the config leaves out of the list.
NAME BRANCH AGE AGENT TOPIC
✓ calm-baking-otter worktree-calm-bak…-otter 1d no ⎇ Fix the flux capacitor
┌─ dreamy-foraging-hickey ───────────────────────────────────────────────────────┐
│ NAME dreamy-foraging-hickey │
│ BRANCH worktree-dreamy-foraging-hickey │
│ AGE 2h │
│ AGENT yes │
│ TOPIC ✳ Goal was a widget on the dashboard; it's built and wired up, and the │
│ numbers on it are still the mocked ones │
└────────────────────────────────────────────────────────────────────────────────┘
P swaps the worktrees for what is running in them — ccwt ps,
in the tui's own window and refreshed on the same interval, -g included when the tui was
started with it:
RUNNING
calm-baking-otter
23136 -zsh
23730 ↳ go test ./...
dreamy-foraging-hickey
49757 -zsh
79860 ↳ claude --resume
☰ q:quit /:search space:go esc:worktrees │
It's the same list, not a window over one: the arrows walk it, / and n/N search it,
clicking selects, and the rows scroll the way the worktrees do. What changes is what a row
is, and so what there is to do with one — space goes to where it is. On a process that
means the pane it's running in, the same place ccwt nav ps would
take you (herdr or tmux, whichever it's under); on the worktree line above it, the workspace,
as space does on the list. esc brings the worktrees back — the first esc clears the
search, if there is one — and so does another P.
A process's pane is an environment variable it inherited, and macOS hides the environment of
everything under /bin: a shell can't answer for itself, so its descendants are asked
instead, which is why going to a bare -zsh still lands in the right place as long as
something is running under it.
Removing a worktree also removes the only record of what it was for: the branch is deleted,
the directory is gone, and the agent files its transcript under a path that no longer
exists. So ccwt remove writes a line about it first — when it went, what it was called,
and the TOPIC the list showed — into $XDG_STATE_HOME/ccwt/tasks.db, next to the queued
prompts, and every removal goes in: ccwt remove, ccwt done, ccwt gc, and r in the
tui.
l shows that log as a window over the list, newest first; esc (or l again) closes it.
Its rows select like the list's — the arrows (or j/k) walk them, a click lands on one —
and ↵ opens the selected removal's page.
NAME BRANCH AGE AGENT TOPIC
✓ calm-baking-otter worktree-calm-bak…-otter 1d no ⎇ Fix the flux capacitor
┌─ worklog ──────────────────────────────────────────────────────────────────────┐
│ AGE NAME TOPIC │
│ 2h kind-munching-melody ✳ Kept a log of removed worktrees, with a pane in… │
│ 1d dreamy-foraging-hickey ⎇ Quote a queued prompt on its way to th… (#79) │
└────────────────────────────────────────────────────────────────────────────────┘
That page is everything left to know about one removed worktree: what the log recorded, and
then the whole of the last Claude Code session that ran in it — the prompts, what the agent
said back, and the tools it reached for, which survive the removal because transcripts are
filed under your home directory rather than in the tree they were about. It scrolls:
the arrows (or j/k) by a line, space/b by a screenful, g/G to either end, esc
back to the log.
┌─ dreamy-foraging-hickey ───────────────────────────────────────────────────────┐
│ NAME dreamy-foraging-hickey │
│ PROJECT /Users/you/src/ccwt │
│ REMOVED 1d ago, 2026-08-12 09:31 │
│ PATH /Users/you/src/ccwt/.claude/worktrees/dreamy-foraging-hickey │
│ TOPIC ⎇ Quote a queued prompt on its way to the pane (#79) │
│ SESSION ~/.claude/projects/-Users-you-src-ccwt--claude-worktrees-dreamy-… │
│ │
│ › the prompt reaches the pane unquoted, fix it │
│ │
│ The prompt is pasted into a shell line, so it needs shell quoting. │
│ │
│ ⚒ Read: tui.go │
└────────────────────────────────────────────────────────────────────────────────┘
Tool results are left out — they are the bulk of the megabytes, and what a session did
reads perfectly well from the prompts, the prose and the calls. The SESSION line says
where the raw transcript is if you want the rest of it.
ccwt worklog prints the same table on the command line, -n (default 20) saying how many
lines of it. -g spans every known project instead of this repo's, and adds a
PROJECT column, since a worktree name on its own doesn't say which repo it was in; the
pane does the same when the tui is running under -g.
Work you've thought of but can't start yet — because it needs the worktree you're in to be
finished first — goes on the list as a prompt queued behind it. n, for what to do next,
opens a box over the middle of the list, and what you type there is recorded under the
selected row:
NAME BRANCH AGE AGENT TOPIC
* * dreamy-foraging-hickey worktree-dreamy-…-hickey 2h yes ✳ Goal was a widget on…
✓ calm-baking-otter worktree-calm-bak…-otter 1d no ⎇ Fix the flux capacitor
┌─ dreamy-foraging-hickey ──────────────────────────┐
│ rebase onto main, open the PR, and then port the │
│ widget to the mobile layout█ │
│ │
└───────────────────────────────────────────────────┘
↵:queue ctrl-g:$EDITOR esc:cancel
It's three fifths of the terminal, that size whatever is in it — a box that grew as you
typed would shift the list behind it line by line — and the row the prompt will hang off is
one of the ones still showing around it. ↵ records it, esc throws it away. Behind
another queued prompt the box's top rule names that prompt instead, since a chain can have
several links and the worktree's name wouldn't say which one you're extending.
Above it is a one-line title box, as on ccwt ws's seed prompt. The box opens there:
↵ or tab takes you down to the prompt, and shift-tab takes you back up. Leaving it
empty is fine. A queued prompt with a title shows the title in TOPIC, and the details pane
and e still have the prompt in full. When the prompt starts, its title becomes the
agent's --name and the workspace's label.
Once recorded it's a row of the list:
NAME BRANCH AGE AGENT TOPIC
* * dreamy-foraging-hickey worktree-dreamy-…-hickey 2h yes ✳ Goal was a widget on…
↳ <queued> 4m rebase onto main and open the PR
↳ <queued> 3m then port the widget to the mobile layout
✓ calm-baking-otter worktree-calm-bak…-otter 1d no ⎇ Fix the flux capacitor
It has no worktree of its own yet, and <queued> in NAME says that's why the column is
otherwise empty, the way <new> does below for a prompt whose turn has come.
n on a queued prompt queues another one behind that one instead, so a chain of "and
then" is a tree hanging off the worktree it starts from. d shows a long prompt in full in
the details pane, and r deletes one — along with everything queued behind it, since
nothing waiting on a prompt that isn't going to happen can happen either.
e in that details pane — where the prompt is legible in full, which is where you notice it
needs a word changing — reopens it in the same box for rewriting. The edit box takes the
pane's place rather than stacking on it, ↵ saves, and the prompt stays where it is in the
chain: whatever was waiting on it still is.
The box is a line editor, since the word that needs changing is rarely the last one, and its
keys are the ones the zsh line editor and Claude Code's own box use: ← and → move the
caret, Ctrl-←/→ (or Alt-B/F) move it a word at a time, ↑ and ↓ (or Ctrl-P/N) move it
a line at a time, home and end (or Ctrl-A and Ctrl-E) go to either end of the line,
backspace and delete take the character on their own side of it, and Ctrl-W, Alt-D,
Ctrl-U and Ctrl-K rub out the word before the caret, the word after it, the rest of the line
before it and the rest of the line after it. Text goes in where the caret is, and the box
scrolls to wherever that is, so the start of a prompt taller than the box is reachable.
Shift-↵ puts a line break in — ↵ on its own records the prompt — so a prompt can be a
paragraph rather than a sentence. Only ↵ and esc close the box: a key it doesn't know,
whatever your terminal sends for Cmd-←, does nothing at all rather than throwing away what
you have typed. (A terminal has to be told to send something for shift-↵, since by default
it sends the same thing ↵ does. Claude Code's own /terminal-setup does that for iTerm2
and VS Code, and this box takes what it binds; Ctrl-J is the line break everywhere else.)
The table folds the breaks back to spaces for the one row it has to draw the prompt on.
For a prompt that wants more than that, Ctrl-G finishes it in $EDITOR — the same key
Claude Code's own prompt box binds it to, since it's the same box for the same purpose. The
tui hands over the screen and the keyboard, opens $VISUAL, $EDITOR or vi on what
you've typed so far, and takes back whatever you save, with the list drawn again around it.
An editor that quits without saving leaves the prompt as it was.
A long paste — over 800 characters or two line breaks — goes in as [Pasted text #1 +40 lines], the marker Claude Code's own box puts in for one, so what you type around it stays
in view. The paste goes back in its place when the prompt is queued or its agent started, and
$EDITOR gets it in full, with the marker back over it if it comes back untouched. A paste
is text however long it is: a line break in one is a line break, not the ↵ that records the
prompt.
n is also vim's next-match key, and while a search pattern is in force that's what it
stays: /, then n and N to walk the matches, and esc to clear the pattern and get the
queue key back. The bar says which one is live.
Removing the worktree does not take the chain with it — it starts it. Every prompt that
was waiting on that worktree moves up to a row of its own, called <new> because its turn
has come and it hasn't got a worktree yet, with whatever was queued behind it still under
it:
NAME BRANCH AGE AGENT TOPIC
✓ calm-baking-otter worktree-calm-bak…-otter 1d no ⎇ Fix the flux capacitor
<new> 2h rebase onto main and open the PR
↳ <queued> 2h then port the widget to the mobile layout
space (or a double-click) on a <new> row is where the worktree finally gets made: a
fresh one, opened as its own Herdr workspace the way c
opens one — ccwt ws in its first tab — with the prompt running in a tab of its own next to
it — claude "<the prompt>", or whatever task_command in the config says, e.g.
task_command = "claude --permission-mode plan"
The prompt itself travels in a file — the pane is handed claude "$(cat …)", and the file
deletes itself as it is read. That command is typed at the pane's shell prompt, and a
terminal takes about a kilobyte of a typed line before it drops the rest, which a prompt
worth queueing is easily longer than; a filename is the same hundred characters however long
the prompt is, and the shell never has to be taught to read back the quotes, newlines and
backslashes in it.
The row is then a worktree like any other, and the rest of the chain hangs off it, waiting on the work that has just started rather than on the prompt that started it.
A <new> row is also where work that isn't waiting for anything starts, and n with
nothing selected is how you write one down: there's no row to hang it off, so it goes on the
list as a <new> of its own straight away — a worktree that hasn't been made yet, with the
prompt to run in it when it is. esc is how you get back to nothing selected, and space
on the row makes the worktree, exactly as it does for one that got there by having its
worktree removed. Under -g this needs a project selected, its section header being enough
to say which repo the worktree is to be made in; with nothing selected there the bar drops
the key.
The queue lives in $XDG_STATE_HOME/ccwt/tasks.db (~/.local/state/ccwt/tasks.db by
default) — one SQLite database for every project, so a prompt queued in a project's own tui
shows up in tui -g and the other way round, within a refresh interval either way.
The same database keeps a seen table: every repo and Claude Code worktree ccwt has been
run in or has made, with when it was first and last used and how many times — nothing reads
it yet.
/ searches, as in vim or less: type a pattern into the bar and the selection moves to the
match as you type, enter accepts it, esc puts back the pattern and the row you started
from, and n/N walk the rest of the matches forwards and back, wrapping around the ends.
? searches upwards instead. Every match on screen is picked out, not just the selected
one; a bare esc clears them, and clears the selection with them — nothing is selected
again, and the bar drops the keys that only apply to a row.
The pattern is a regular expression, always
case-insensitive, matched against the whole line as drawn — name, branch, age and topic
alike, so /✓ finds the worktrees whose branch is merged and /yes the ones with an agent
running in them. One that doesn't compile simply matches nothing, which is what half of one
is while you're still typing it. Under Herdr, where each row leads with its agents'
dot, what a search sees there is the symbol the dot stands in
for — × blocked, ◐ working, ✓ done, ○ idle, · none — so /✓ finds the finished
turns along with the merged branches.
Something parked in a pane for days goes on running the binary it started from, so the tui
watches that binary and says so when it's replaced — by go install, by a package manager,
by a downloaded release:
☰ q:quit p:pull /:search │ upgraded to v0.64.0 — restarting when idle
Then it does: once the keys have been quiet for four seconds, with nothing typed and no pane
open, the process replaces itself with the new binary — an exec, so it keeps its pid, its
pane and its flags — and the list comes back fresh; the selection and any pattern in force
are what doesn't survive it. The version is the one you're restarting into (asked of the new
binary itself; it's dropped from the line if it won't answer). Windows has no exec, so
there the notice reads restart ccwt and stays up until you do.
-g makes ccwt list and ccwt tui span every project you've configured instead of just
the repo you're standing in, a section per project, each holding that project's worktrees
newest-first:
NAME BRANCH AGE AGENT TOPIC
▾ ccwt (2)
* dreamy-foraging-hickey worktree-dreamy-…-hickey 2h yes ✳ Goal was a widget on…
✓ calm-baking-otter worktree-calm-bak…-otter 1d no ⎇ Fix the flux capacitor
▸ platform (14)
In the tui the sections fold: select one and press ↵, or double-click it, and it collapses to
the header line — the count stays, so you can see what's tucked away. platform above is
folded shut. Folding has its own key so that it can't misfire into opening a worktree:
space only opens, ↵ only folds.
It works from anywhere — including outside a git repository — and the directory you launch
it in takes no part in it: under -g the repos are the known ones and nothing else.
Every action applies to the project the selected row belongs to: r removes from that
repo, x creates one in that project — on a section header too, which is how you make the
first worktree in a project that has none — p pulls it, and the status bar shows that
project's branch state. With nothing selected yet there's no repo to act on, so those keys
say so instead of reaching for the current directory. The background --fetch covers every
known project.
The known projects are every repo ccwt has been run in, in any of its worktrees, in the
order it first was — kept in $XDG_STATE_HOME/ccwt/tasks.db, and left out once the repo is
no longer there. "hide" in the ☰ menu, on a project's section header, leaves one out for
good, however much ccwt goes on being run there. Under Herdr, o lists the known projects,
most recently used first, ● those Herdr has a workspace on and ○ the rest — typing filters
them, ↑↓ or a click selects — after which p runs git pull in it and P copies its path — ↵ or a second click switches to an open one's workspace, or
opens a closed one: a new workspace in the repo, with ccwt running in its first tab, named
prj. A repo it hasn't been run in yet, or one to put at the top, goes in
$XDG_CONFIG_HOME/ccwt/config.toml (~/.config/ccwt/config.toml when that variable isn't
set) — each entry the main checkout of a repo, the one whose .claude/worktrees/ the
worktrees live under:
[[projects]]
path = "~/src/ccwt"
[[projects]]
path = "~/src/platform"
Categories name the projects checked out under a directory (the longest match wins). Under
Herdr, a workspace named ------ <name> heads a category's section of the sidebar, and a
project o opens goes last in its own, just above the next ------ divider:
[[categories]]
name = "work"
path = "~/w"
[[categories]]
name = "personal"
path = "~/p"
branch_prefix sets what ccwt new puts in front of a worktree's name to make its branch
(worktree- when unset), for repos that want their branches namespaced:
branch_prefix = "mkm/"
It applies from the moment you set it: worktrees created under an older prefix keep their
branches, and ccwt remove on one of them leaves that branch behind rather than deleting
it — remove it with git branch -D if you want it gone.
columns picks which columns ccwt list and ccwt tui draw, in the order you name them
— name, branch, age, agent, topic, all of them when unset:
columns = ["name", "age", "topic"]
The */✓/±/☐/✳ markers ride on name, so leaving it out leaves them out too.
sort is the order ccwt list and ccwt tui put the worktrees in — freshness when
unset, the last commit or the last thing written to the newest session there, whichever is
younger, or commit for git's answer alone. --sort overrides it for one run:
sort = "commit"
task_command is the agent cli — which agent ccwt starts for you, and the one place that
choice lives: what a queued prompt is handed to when its worktree is made,
and what the tui's c runs on its own. It is claude when
unset, and split on spaces so flags get through:
task_command = "claude --permission-mode plan"
forges says which cli knows about a host's reviews, for the tui's m key and
for ccwt mr, which asks GitHub for a "gh" host and GitLab for any
other. Only a host whose name doesn't give it away needs a line — a self-hosted GitLab most
of all, since github.com, gitlab.com and anything with gitlab in the name are guessed:
[forges]
"code.example.com" = "glab"
environments renames an environment for ccwt mr's ENV
column: the name GitLab or GitHub knows it by on the left, what to call it on the right.
Deployment pipelines name environments to be unambiguous in a settings page, which is
longer than a column you only glance at wants:
[environments]
production = "prod"
staging = "stg"
ccwt config view prints that file and ccwt config edit opens it in $EDITOR, either one
creating an empty file first when you don't have one yet.
Herdr manages terminal workspaces for coding agents. ccwt doesn't need
it — everything above works on its own. When Herdr is running, these extras appear, and
this section is the whole of it:
The ✳ marker. In ccwt list and the tui, ✳ in the leading glyphs means Herdr says an
agent is working in that worktree right now. It outranks *, ±, ☐ and ✓, and it's the case git
can't see at all: a branch made a minute ago with nothing committed to it is merged and clean.
It comes from Herdr rather than from the agent's own files, so it holds for whatever agent is
running there — unlike the AGENT column, which knows Claude Code alone. ccwt remove,
ccwt done and ccwt gc all refuse a worktree marked this way (ccwt remove -D interrupts
the agent anyway).
Agent dots. In the tui, each worktree's row leads with Herdr's own mark for the agents in
it, ahead of the other two glyphs — the dot ccwt ws, below, gives a tab, by the same rules:
red blocked, yellow working, green done, grey idle, and a · where Herdr has no agent.
Several agents in one worktree show whichever most wants you, and green is a turn that has
ended since anyone last looked at its tab, which going there — space — clears. An agent is in
the worktree its own directory or its pane's is in, as for ✳.
NAME BRANCH AGE AGENT TOPIC
● ✳ dreamy-foraging-hickey worktree-dreamy-…-hickey 2h yes ✳ Goal was a widget on the dashboard; it's built and…
○ ✓ calm-baking-otter worktree-calm-bak…-otter 1d no ⎇ Fix the flux capacitor
AGENT goes on saying whether Claude Code is running there. ccwt list has no dots: it draws
the table once, with nothing to remember what you have read, and would call every finished
turn unread.
Opening workspaces from the tui. space opens the selected worktree as its own Herdr
workspace (via herdr worktree open), and a double-click on its row does the same. x
creates a worktree and opens it without leaving the list, and c does that and starts
ccwt ws in it — the workspace's own tui, below, which is where its agents are seeded
from. These keys only exist when the tui
is itself running in a Herdr pane — elsewhere there's no session to open a workspace in, so
they're dropped from the bar and do nothing.
Workspace naming. A workspace opened on a new worktree is named after the worktree:
Herdr would otherwise list it under its repo by branch, prefix and all. Reopening one leaves
its name alone, so a workspace you've renamed yourself stays renamed. And the name is what
NAME shows, in ccwt list and the tui alike, when they run in a Herdr pane: a worktree open
as a workspace goes by the workspace's name, so the list reads like Herdr's sidebar. Piped
output keeps the worktree's own name, which is the one ccwt remove takes.
A workspace's own tui: ccwt ws. The tui above is the repo's view: its worktrees,
whatever workspace each is open in. ccwt ws is a workspace's, in two sections: where the
workspace's review stands, and under it the tabs working towards it.
MR STATUS PIPELINE TITLE
acme/…/api!42 needs approval failed: unit, lint Wire the widget to real numbers
TAB AGENT DIR TITLE
* 1 · dreamy-foraging-hickey zsh
2 ● dreamy-foraging-hickey Wiring the widget to real numbers
3 ○ dreamy-foraging-hickey Waiting on your review
☰ q:quit /:search n:agent space:go g:git │ started
The first section is what ccwt mr prints for the branch the workspace is
working on — can it go in, and did its pipeline pass — which is the question the whole
workspace exists to get a yes to. A fresh worktree hasn't got one yet, and says so in a line
rather than in a row of column names with nothing under it:
no merge request yet
The lookup is a handful of GitLab or GitHub round trips, so it runs in the background and is asked again at most once a minute: the table you're reading never waits on the network, and the row appears under the heading a moment after the review does.
The second section is the workspace's tabs — the tab's label, whether Herdr sees an agent
working in it, the directory it sits in, and what its terminal calls itself, which for an
agent is its own one-line account of what it is doing — and space (or ↵, or a
double-click) goes to the selected tab. A * leads the tab the tui itself is in. The rows
come in the order the tabs sit along the tab bar, so dragging one about in Herdr moves its
row with it. They're the only rows the selection walks: the review above them is something to
read, and it stays put while they scroll.
The AGENT mark is Herdr's own: a circle, and the state is the colour of it —
red blocked, yellow working, green done, grey idle, and a · for a tab Herdr sees no agent
in at all. Herdr keeps that state per agent rather than per tab, so a tab split between
several shows whichever of them most wants you: an approval waiting on an answer first, then
a turn that has ended with something to read, then work still going on. Herdr draws the three
busy states as the same circle too, so a workspace reads the same in the tab bar and in the
table below it — in the same colours, too: the dots are asked for by palette number, and
Herdr's theme is what answers.
Green is the one mark the table keeps itself. Herdr's done is the server's seen state, and
one look spends it for good: sit in a tab until its agent finishes, and every turn it ends
after that is plain idle, however far away you were for it. Herdr's own tui doesn't take
that answer either — it keeps the badge per client, each tracking what it has been shown. So
this one goes on when an agent has settled and has moved since the last time anyone was
looking at its tab, and comes off when you go: space, or Herdr saying that tab is the one
you are looking at.
What it reads that off is state_change_seq, Herdr's own counter for when an agent last
changed state, rather than off watching for the change itself — which means a ccwt ws that
has only just come up still says which tabs have something waiting in them. A conductor that
has to witness the turn end to colour it says nothing about the ones that ended first, and
ccwt ws restarts itself on every ccwt upgrade. The cost is a third round trip to Herdr per
round, next to the tab and pane lists the table is built from.
n opens the same box the queue types into, and what goes in it is a seed
prompt: ↵ opens a tab of the workspace in the workspace's own worktree — the one the tui is
standing in — and runs task_command there with the prompt. No worktree and no tab name of
its own: the agents of one workspace are working on one thing, in one checkout, and an
unnamed tab is named by what runs in it, which is the agent saying what it's doing. The tab
opens behind the one you're typing in, so the table shows it and you go when you're ready. A workspace with no
other tab yet opens straight on that box, since the first thing to do in a fresh workspace is
say what it's for; esc puts it away. g shows the history of the worktree under the
selected tab, and /, the mouse and the ☰ are the tui's own.
When the agents are Claude Code — task_command is claude, or unset — and Claude Code
hasn't been told to trust the repository yet, ccwt ws asks that first, over everything
else, the seed prompt included: an agent started in a tab nobody is looking at would
otherwise sit at Claude Code's own trust dialog with your prompt waiting behind it. y
records the answer where that dialog would, in ~/.claude.json (or $CLAUDE_CONFIG_DIR's),
under Claude Code's own lock, and on the repository's main checkout — which is what Claude
Code files it under, so one yes covers every worktree of it. n leaves Claude Code to ask
for itself. The home directory is never asked about: Claude Code only ever trusts that one
for a session.
r is the other end of that: ccwt done for the workspace itself — the
worktree the tui is standing in is removed, branch and all, and the workspace closes behind
it, this tui with it. It's on the bar only when that removal would go through as it stands:
branch merged, nothing uncommitted, and no agent still working in there — the three checks
ccwt remove makes before -D, asked of the very same code, so r on the bar means this
workspace is finished with. Whether it's there is a property of the workspace and not of any
one tab, so it doesn't come and go as you walk the table: it's on the bar or it isn't,
whichever row is selected. A tui run somewhere that isn't a worktree — the repo itself —
never has one.
This is part of a longer plan — what each tab is about, and which tickets and reviews each
one touches — and TODO.md has the rest of it.
A workspace whose ccwt ws has gone. The tab c starts ccwt ws in is named ws, and
the name outlives the tui: quit it, or restart Herdr — which brings the tabs back but not what
was running in them — and the tab is a bare shell under the same name. The tui names each of
its worktrees' workspaces in that state, in a yellow box in its bottom-right corner:
┌─ ccwt ws not running ↻ ─┐
│ audit restore │
└─────────────────────────┘
☰ q:quit p:pull g:git /:search l:log a:agents x:new c:new+ws n:queue │ main
It listens to Herdr's events, and looks again whenever a tab or a workspace is made, renamed
or closed — and every ten seconds besides, since Herdr says nothing when a program exits back
to its shell. It covers only the workspaces open on the worktrees it lists — the ones Herdr
shows under the repo, or under any known project with -g — and leaves another repo's to
that repo's tui. Only the tui does this, not ccwt ws, and only when it is running in a Herdr
pane.
With -g the box is ccwt tui not running instead, and names the projects' own workspaces —
the ones open on each configured repo's root checkout — that have no ccwt in any pane of any
tab: the repo tuis that are gone.
A click on the ↻ in its title starts them again, once you have read the plan it shows and
said y to it. Each workspace gets its tui back in the tab it belongs in — prj for a
project's own workspace under -g, running ccwt; ws for a worktree's, running ccwt ws.
A workspace with no such tab gets one, made first in its bar; one whose tab is at its shell
has the tui typed there; one whose tab is busy with something else is left to it, and the plan
says so.
Closing workspaces on removal. Once its checks pass, ccwt remove closes the workspace
open on the worktree, ending the agent living in it — including your own, which is closed
last of all, after the worktree is gone and the shell has been cd'd out. Because a workspace
vanishing is a bigger surprise than a directory, a terminal is asked first: remove <name> and close its herdr workspace? [y/N] on stderr, answered "no" by default. -y/--yes skips
the question, and so does a ccwt remove whose stderr isn't a terminal — a script or the tui
gets on with it. ccwt done is ccwt remove . -y. Closing the workspace you're looking at
would leave Herdr focused on whatever it had open before — some unrelated pane — so when
yours is the one on screen, the repo's own workspace, the one open on the root checkout, is
focused first: you land back in the repo the worktree came from. Switch to another workspace
while the removal runs and nothing moves — closing a workspace nobody is looking at takes no
focus with it, and where you switched to is where you meant to be.
The plugin in herdr-plugin/ adds a New ccwt worktree action to Herdr — the same
thing the tui's x does.
The tui is meant to be parked in a pane — e.g. the main pane of a Herdr workspace — as a live view of what's running where.
Claude Code has a built-in --worktree flag that runs a session in its own worktree.
The catch: Claude creates and uses that worktree internally, but your terminal tab's
working directory stays at the base project root. Open a new tab or split and you land
back in the base repo, not in the worktree the session is actually using.
ccwt new fixes this by creating the worktree up front:
claude --worktree;Terminals and multiplexers that honour OSC 7 (iTerm2, Ghostty, WezTerm, tmux/cmux, …)
will then open new tabs and splits in the worktree directory. Because ccwt and Claude
Code share the same .claude/worktrees/<name> layout, the name printed by ccwt new is
exactly what claude --worktree expects.
This used to be doable with a Claude Code hook that printed the escape code itself, but Claude Code no longer lets hooks emit raw escape sequences to the terminal. Emitting it from
ccwt newin a small wrapper aroundclaudeis the way to get it back.
Save this as claude-wt somewhere on your PATH, chmod +x it, and run it instead of
claude. It allocates a fresh worktree per session unless you ask for a specific one:
#!/usr/bin/env bash
# claude-wt — run each Claude Code session in its own ccwt worktree, and let the
# terminal's cwd follow into it (via the OSC 7 sequence ccwt emits on stderr).
#
# claude-wt # fresh worktree, terminal cd's into it
# claude-wt --worktree foo # use the worktree named "foo"
# claude-wt --no-worktree # skip worktree handling entirely (wrapper-only flag)
set -eo pipefail
args=()
has_worktree=0
no_worktree=0
for arg in "$@"; do
case "$arg" in
--no-worktree) no_worktree=1 ;; # wrapper-only; drop it
--worktree|--worktree=*) has_worktree=1; args+=("$arg") ;;
*) args+=("$arg") ;;
esac
done
if [ "$has_worktree" -eq 0 ] && [ "$no_worktree" -eq 0 ]; then
# `ccwt new` prints the worktree name on stdout (captured here) and emits the
# OSC 7 cwd report on stderr, which we deliberately let flow to the terminal.
worktree=$(ccwt new)
args=(--worktree "$worktree" "${args[@]}")
fi
exec claude "${args[@]}"
Naming the wrapper something other than claude (here, claude-wt) keeps exec claude
from re-invoking the wrapper itself. If you'd rather call it claude, point the exec
line at the real binary by absolute path instead (e.g. exec "$HOME/.local/bin/claude").
The key detail is that command substitution — worktree=$(ccwt new) — captures only
stdout, so the OSC 7 sequence on stderr still reaches the terminal. Don't redirect or
swallow stderr, or you'll lose the cwd report.
| Command | Description |
|---|---|
ccwt new [name] | Create a worktree under .claude/worktrees/<name> on a new branch worktree-<name>, and print <name>. Generates a name if omitted; reuses an existing worktree of the same name. --switch <branch> checks the worktree out on an existing branch instead of creating one (ccwt new + git switch <branch>). When run inside a worktree it returns the enclosing one instead of creating a new one (override with --force-create). --path prints the worktree's absolute path instead of <name>. |
ccwt cd <name> | cd into an existing worktree under .claude/worktrees/<name> (with shell integration) — never creates it, errors if it doesn't exist, and the name is required. ccwt cd .. is shorthand for ccwt .., and ccwt cd - jumps to the previous directory ($OLDPWD), like the shell's cd -. |
ccwt list | List the repo's agent worktrees with branch, age, running-session status, and last commit, freshest first — the last commit or the last thing written to the newest agent session there, whichever is younger. --sort=commit orders by the commit alone, --sort=freshness is the default, and sort in the config file picks which one you get without the flag. -g lists every known project instead, a section per project. --no-headers leaves out the header row, for feeding the table to cut, awk or a shell loop. |
ccwt tui | The default command, so a bare ccwt runs it. Show the ccwt list table full-screen, refreshing in place without flicker, over a status bar showing how far the current branch is ahead/behind its upstream, with the ccwt version in its far corner. q (or Ctrl-C) quits, p runs git pull (a fast-forward to what was already fetched, while the branch is behind it). Arrow keys (or j/k) select a worktree, a click on its row selects it, dragging selects text and copies it to the clipboard, the ☰ in the bar's corner drops the bar's actions out as a clickable menu, / (or ?) searches the way vim does — incrementally, case-insensitive regexp, all matches highlighted, n/N for the next and previous while a pattern is in force; d shows the selected worktree's column values in full in a pane over the list (esc closes it), g shows the history your own git ll alias prints, in a scrolling page over the list, m opens the branch's review in the browser, and r removes it, like ccwt remove. a narrows the list to the worktrees an agent is running in — one herdr has, idle or not, or a Claude Code session — and a again brings the rest back. l shows the worklog — the worktrees already removed and what they were about — in a pane of the same kind, whose rows select like the list's; ↵ (or a double-click) opens the selected removal's page, which is what the log recorded about it followed by the whole agent session that ran in it, scrolled with the arrows, space/b and g/G. n with no pattern in force queues a prompt behind the selected row — work to start once that worktree (or the prompt above it) is finished — typed into a box over the list and drawn as a tree under the row it waits on, kept in $XDG_STATE_HOME/ccwt/tasks.db and so shared with every other ccwt running; e in a queued prompt's details pane rewrites it in place — the box is a line editor, with the arrows, home/end and Ctrl-W/U/K, shift-↵ for a line break, and Ctrl-G to finish the prompt in $EDITOR, as in Claude Code — r deletes it and everything queued behind it, and removing the worktree promotes what was queued on it to <new> rows — worktrees waiting to be made, which space makes and starts the prompt in. n with nothing selected queues a prompt that waits on nothing, which is a <new> row from the start; under -g that takes a project selected, its section header included. -g spans the known projects as a foldable section each (↵, or a double-click on the header, folds one shut) and ignores the current directory entirely, every action (p included) applying to the selected row's project. --interval (default 2s) sets the refresh rate, --fetch (default 1m) how often origin/main is fetched in the background, and --sort orders the worktrees, as it does on ccwt list. The bar also says when the ccwt binary underneath it has been upgraded, and to which version — and once the keys have been quiet for four seconds the tui restarts itself into it. Under Herdr space opens the selected worktree as a workspace (double-click does too), x creates one and opens it, and c also starts ccwt ws, the workspace's own tui, in it; each row leads with Herdr's dot for the agents in its worktree, as ccwt ws draws one for a tab; a box in the bottom-right corner names those of its worktrees' workspaces whose ws tab has no ccwt running in it. |
ccwt ws | The tui for the first tab of a Herdr workspace, in two sections. First the workspace's own review, as ccwt mr prints it for the branch the tui is standing on — one line saying there isn't one while the branch has none, and looked up in the background (at most once a minute) so the table never waits on GitLab or GitHub. Then that workspace's tabs as a table — label, agent status, directory, terminal title — with space/↵ (or a double-click) going to the selected tab, n asking for a seed prompt that starts an agent in a tab of its own (task_command run with the prompt in the workspace's own worktree, the tab unnamed and opened behind the current one), g the history of the worktree under the tab, r being ccwt done for the workspace itself — the worktree the tui is standing in removed and the workspace closed behind it, offered only when that removal would go through (merged branch, nothing uncommitted, no agent still working there), and so on the bar or not whichever row is selected — and / searching as in the tui. Opens on the prompt when the workspace has no other tab — and before that, when the agents are Claude Code and it doesn't trust the repository yet, on whether it may: y records the trust in Claude Code's own config, as its dialog would. --interval (default 2s) sets the refresh rate. Refuses to run outside Herdr. |
ccwt remove <name> | Remove the worktree at .claude/worktrees/<name> and delete its branch. . means the worktree you're currently in; removing the one you're in cds you to the repo root, like ccwt ... The branch is deleted only if merged: an unmerged branch refuses the whole removal, worktree included, so nothing is stranded, and so does a worktree with uncommitted changes, and so does one with an agent working in it (the ✳ of ccwt list). Pass -D to remove anyway (unmerged branch deleted, uncommitted changes thrown away, working agent interrupted), or --keep-branch to remove only the worktree. Under Herdr the worktree's workspace is closed too, once those checks pass — your own one last, after the removal, with focus handed to the repo's own workspace first if yours is the workspace on screen — and a terminal is asked before that happens (-y/--yes skips the question). |
ccwt done | Finish with the worktree you're in: ccwt remove . -y — the removal, and then, under Herdr, closing the workspace you're sitting in, without asking (closing it is what "done" means). Same checks and same flags (-D, --keep-branch); a refusal leaves the workspace open. Outside Herdr it's just ccwt remove .. |
ccwt gc | Remove every worktree that's finished with: branch already merged (the ✓ of ccwt list), nothing uncommitted in it (no * or ±), no agent working in it (no ✳) and no agent session running in it (a no in the AGENT column). Prints the list it found and asks before touching anything — -y/--yes skips the question. Each removal is exactly what ccwt remove <name> does, branch included. The worktree you're standing in is never removed — it says so on stderr and leaves it to ccwt remove .. |
ccwt worklog | Print the worktrees that have been removed, newest first, with what each one was about — the TOPIC its row had, recorded by the removal because nothing else survives it. -n (default 20) says how many lines. -g covers every known project rather than this repo's, with a PROJECT column saying which one each removal was in. The log lives in $XDG_STATE_HOME/ccwt/tasks.db, alongside the queued prompts, and l in the tui shows the same table. |
ccwt ps | Show what is running in each worktree, a process tree per worktree: every process whose working directory is in the worktree while its parent's isn't — the shell you started there, or something orphaned that outlived it — with what each one spawned underneath. --depth/-d (default 1) says how many generations below those roots to draw: 0 for the roots alone, --depth=-1 for the whole tree (spelled with an =, since a bare -1 reads as a flag). Worktrees with nothing running in them are left out. -g covers every known project, a section per project, like ccwt list -g. The working directories come from lsof, so a process whose cwd can't be read simply isn't placed in a worktree. |
ccwt mr <thing> (or ccwt pr) | Say where a review stands and what its pipeline did, without opening a browser: a table with a row per merge request, saying where each one STATUS stands — merged, can be merged, conflict, needs approval, open comments, merge train for one queued on a merge train or waiting on a pipeline to join it — GitLab itself calls that ci still running, which reads as a pipeline to go and wait on when it's the train's own and there is nothing to do about it — or whatever else GitLab's own mergeability check says, spelled out (need rebase, ci still running) — what its PIPELINE did: green, running, or failed: lint, test, …, naming the first few failed jobs; and the ENV it is running on — the environments whose current deployment carries the commit this merge request landed as (stg, stg, prod), which is empty until it's merged and released. [environments] in the config file renames them for the column (production = "prod"). <thing> is either a merge request url, which gets a table of the one row, or a Jira key (PROJ-1234) or its browse url, which gets a row per merge request that names the ticket, with the title to tell them apart. Left out entirely it's the merge request of the branch you're standing on; - reads a list of things off stdin, one per line, and puts all of their merge requests in the one table (a merge request that answers to two of them is still one row). On a terminal the MR column is a link to the merge request, a merged status is green — the one word either table spends colour on, so that the colour still means something when it turns up — and a spinner on stderr says the lookups are still going; piped or redirected, the table comes out as markdown — a rule under the header, the MR column as a markdown link, and nothing shortened to fit a width nothing has — so it lands in a merge request comment, a ticket or a chat message as a table rather than as columns that only line up in a monospace font. A merge request already merged or closed has no pipeline reported — it's history, and there's nothing to do about it either way — when none of the rows has one the PIPELINE column isn't drawn at all. ENV is always drawn, empty for a merge request that isn't running anywhere. --no-environments (it's on by default) fills ENV with ?? instead and sends none of those lookups — a deployments call per project and a merge-base call per environment — for when the answer isn't worth the round trips; ?? says nobody asked, which isn't what an empty cell says. It's read off the project's deployments (newest first, so the first mention of an environment is what's on it now) and answered by asking GitLab whether the environment's commit has the merge request's in its history — "on it now", rather than GitLab's own MR badge, which means "reached it at least once" and survives a rollback. It asks glab, which already knows the host and the token; the ticket's merge requests come from GitLab's own search for the key, so one that mentions the ticket only in a commit message or a comment isn't found. A review on GitHub is a pull request, and gets the same table from gh: a pull request url, or — with no argument, or a ticket — a repo whose origin is on GitHub, which is told apart the way the tui's m tells it. STATUS is in the same words — ci still running while checks nobody requires are still going, which GitHub itself counts as mergeable — plus draft, changes requested, behind, and merge queue for GitHub's merge train; one GitHub hasn't worked out yet is asked again a second later rather than shown as unknown. PIPELINE is the checks on its last commit, and ENV the repo's deployments, the same question asked of GitHub's compare. A ticket is searched for in the pull requests of whoever owns origin's repo, since all of github.com would be everybody's tickets. |
ccwt new-worktree-name | Print a generated worktree name (adjective-verb-noun) without creating anything. |
ccwt nav ps <pattern>ccwt nav ws <pattern> | Go to where something is. ps searches your running processes by command line and puts you in the pane the best match is running in; ws searches Herdr workspaces by title and by the name of the worktree each has open, and goes to the best match. Both patterns are fuzzy — the characters in order, not necessarily adjacent, case ignored, so ccwt nav ws mtls finds the workspace titled "mTLS subresource" and ccwt nav ws duckling the one holding frolicking-tumbling-duckling; of everything a pattern matches, the tightest run wins and the earlier one breaks the tie. ps finds the pane by reading the variable a multiplexer exports into it (HERDR_PANE_ID, TMUX_PANE) off the process itself rather than by asking the multiplexer, so Herdr and tmux both answer; it never matches ccwt itself or anything that spawned it, the pane you're already in being nowhere to navigate to. macOS hides the environment of system binaries, so a bare shell can't be found this way — search for the agent or the program running in the pane instead. --dry-run goes nowhere and prints where it would have gone instead, three lines: the workspace's name, the tab's name, and the tab's address — w16:t2, what herdr tab focus takes (under tmux the same three are the session, the window and the pane id). |
ccwt repo-root | Print the root of the current git repository. Add --root-worktree to print the enclosing repo root when you're inside a .claude/worktrees/<name> worktree. |
ccwt .. | Shorthand for repo-root --root-worktree: print (and, with shell integration, cd to) the enclosing repository root. |
ccwt init <shell> | Emit the shell-integration snippet to source from your rc file. For zsh the snippet carries completion too: commands, and worktree names for cd, remove and lock. |
ccwt config view / ccwt config edit | Print the config file, or open it in $EDITOR (vi if unset). Both create an empty one if there isn't any. The file holds [[projects]], repos for -g to span besides the ones ccwt has been run in, branch_prefix — what ccwt new puts in front of a worktree's name to make its branch (worktree- when unset) — columns, which columns the table draws, sort, the order the worktrees come in (freshness when unset), task_command, the agent cli a queued prompt is run with (claude when unset), forges, which cli finds a host's reviews for the tui's m and ccwt mr, environments, what ccwt mr's ENV column calls each environment (production = "prod"), and categories, which section of Herdr's sidebar o puts a project in. |
ccwt --version | Print version information. |
ccwt follows Claude Code's convention:
<repo-root>/.claude/worktrees/<name>worktree-<name> (branch_prefix renames the
worktree- part)Because the layout matches, worktrees you create with ccwt are visible to Claude Code
and vice versa.