A terminal workspace for parallel coding-agent sessions.
Deck manages projects and sessions and draws the frame around them. The
agent itself is a separate program running in a pseudo-terminal — claude by
default, or cathode, or anything else that talks to a
terminal. Deck speaks no agent protocol, which is why the agent is just a
choice on the new-session form.
Two views:
- Dashboard — projects on the left, the selected project's sessions on the
right, with status and age per session.
tabmoves keyboard focus between the two columns; the focused one carries an accent-coloured top border, and the other keeps a dimmed cursor so you can still see where it is. - Session — a sidebar of sessions grouped by project, and a main pane that is the live agent terminal.
The pane hosts whatever program the session names — the one above is codex on
its first run, before it has been signed in.
Every project and session in these images is invented. make demo builds the
throwaway workspace they are taken against.
make build # -> ./deck
make install # -> ~/.local/bin/deck, same place as cathode
make test
make race # required before merging changes to internal/agent
make help # list every targetOverride the install location with PREFIX, and make install warns if it is
not on your PATH:
make install PREFIX=/usr/local/bin
make uninstall
make watch # rebuild + reinstall on save (needs entr)Running it needs claude on PATH with claude login already done. Verify with
claude then /status: anything but the subscription route means you are
billing the API.
cd ~/Work/some-repo
deck # seeds this repo as the first project
deck -agent cathode # override the remembered agent for this run
deck -agent claude -agent-args "--permission-mode plan"
deck -agent cathode -agent-args "-mode ask"Running cathode as the agent works and is the reason Deck hosts a process
rather than speaking a protocol. One caveat for parallel sessions: every
cathode instance shares $XDG_STATE_HOME/cathode, and its session list is
rewritten whole rather than merged, so two cathode sessions closing together
can drop entries from each other's ctrl+r picker. Conversations themselves
are safe — those live in claude's own per-directory transcripts.
Launching inside a git repository registers it and puts the cursor on it.
The store is global, not per-directory. Every project and session is
visible from wherever you run deck, and a session's agent runs in that
session's own worktree regardless of where you launched from. So you can cd
into another project, run deck, and pick up a session belonging to the first
one.
A project does not have to be a git repository. A directory that only
collects them — several checkouts side by side, coordinated from the parent —
is a project in its own right, and an agent running there sees all of them at
once. Launching deck in such a directory registers it, the same as launching
inside a repository does.
Sessions in a collector run in the project directory; there is no repository at its root to branch from, so that is the default and an isolated worktree is refused with a message saying why.
a opens a directory explorer for directories you are not standing in. It
starts beside the selected project, so its siblings are already on screen:
↑/↓ to move, → to open a directory, ↵ to pick one, esc to cancel.
Picking a directory that is already registered, or a path that is not a
directory at all, says so and leaves you where you were.
One key is taken from the agent: ctrl+g, the command prefix. Everything
else reaches the agent untouched, so esc, tab, shift+tab, ctrl+c, and
the arrows keep working inside claude.
ctrl+g rather than ctrl+a or ctrl+b, which are readline line-motions
claude uses, and rather than ctrl+], which needs AltGr on a Nordic layout.
| Chrome | |
|---|---|
↑/k ↓/j |
move selection |
←/h →/l |
switch section |
tab |
switch column (dashboard) — projects ⇄ sessions |
↵ |
open / attach |
n |
new session |
a |
add project |
e |
rename the focused project or session |
x |
end session — close and keep the worktree, or delete both |
c |
connect this session to one on another project |
t |
theme picker |
? |
help |
q |
quit |
x and c act on the selected session, so they need the sessions column to
have focus — tab moves it there, and the focused column carries the accent
border. e reads the same focus and renames whichever of the two the cursor
is on. ↵ opens from either column.
Command — press ctrl+g first |
|
|---|---|
^g d |
dashboard |
^g s |
sessions |
^g j / ^g k |
down / up the session list (stays attached if it is live) |
^g 1…^g 9 |
jump straight to that session |
^g n |
new session |
^g x |
stop the agent |
^g esc |
detach from the pane |
^g ↵ |
attach to the pane |
^g q |
quit |
^g t |
theme picker |
^g ? |
the full key list |
^g ^g |
send a literal ctrl+g to the agent |
Press ^g on its own and the footer becomes this list, so the prefix teaches
itself and you never have to remember the second key. Holding it also numbers
the first nine sidebar cards, which is what makes ^g 1…^g 9 usable without
counting. The numbers count sessions, not rows, so they run straight through a
project heading.
Opening a session asks where it should run:
- Isolated git worktree (default) — a new branch
session/<name>checked out under the state directory. Parallel sessions on one repository never collide. - Project directory — runs in the repository itself. Simple, but two sessions here fight over the working tree.
A repository with no commits yet cannot have a worktree — there is no commit to
check out. Deck says so and leaves the form open with your title intact, so
switching to Project directory is one tab and one → away.
Your agent instructions follow the session. git worktree add checks out
tracked files only, so a gitignored CLAUDE.md stays behind and an isolated
session would start with none of them. Deck links CLAUDE.md, AGENTS.md and
.claude into each new worktree — a symlink, so the project keeps one copy and
an edit made in any session is the edit every sibling reads.
Only names your repository ignores are linked. A file git can see would be
a link to an absolute path on your machine, one git add -A away from a commit.
A tracked CLAUDE.md needs nothing: it travels on its own.
The project field steps with ←/→ and opens the full list on ↵, so it
stays usable whether you have three projects or ninety.
A project is listed under the Name you give it when you register it, which
defaults to the directory it sits in. e renames one while the projects list
has focus; the path is not editable there, because changing it makes a
different project rather than the same one under another name. The last field
picks the agent — see Choosing the agent below.
Session names are scheming-hawk-jhgk: two words you can say out loud plus a
suffix that makes the branch unique. The title beside one is yours, and e
changes it when the work turns into something else. The generated name and the
branch stay as they are — the worktree is on disk under that name, and a
session whose agent is running tells its siblings the new title immediately.
x asks what to do with the worktree. Close stops the agent, forgets the
session, and leaves the worktree on disk because it may hold uncommitted work —
the footer says where. Delete removes the worktree and the branch, and the
row carries what that session changed so you are answering a fact rather than a
warning. Close is where the cursor starts.
Nothing is forced. A worktree holding modified or untracked files is refused and the session stays, so you can open it again, commit, and delete it after. If the tree is clean but its branch holds unmerged commits, the worktree goes and the branch is kept — the notice names it.
A session that ran in the project directory has no worktree of its own, so it skips the question and closes.
Sessions are isolated on purpose, which also makes them blind to each other.
Deck runs a small MCP server so agents on the same project can see what
their siblings are doing. Each session's agent is wired to it automatically
(claude and cathode; anything else you wire yourself with -agent-args).
The new-session form has an Agent field — claude, cathode, codex,
gemini — and the last choice becomes the default for the next session. Each
session remembers what it started, so changing the default never relabels a
session that is already running. -agent overrides the default for one run and
takes any program that talks to a terminal, listed or not.
Only claude reports exact turn status, and only claude and cathode are
handed a coordination config. Anything else runs perfectly well in a pane, with
status inferred from output and no sibling awareness.
| Tool | What the agent gets |
|---|---|
sessions |
Who else is working on this project, and the files they hold |
claim |
Announce paths you are about to touch; returns anyone already there |
release |
Give the paths back |
note |
Append to the project's shared log |
notes |
Read what other agents recorded |
work |
Read what another session has changed — summary and patch |
analyse |
Have a separate agent review a session's work; returns an id |
analysis |
Collect that review, with what it cost |
message |
Send to one sibling by name, or to all of them |
inbox |
Collect messages sent to you |
Claims are advisory — nothing stops another process writing the file. The value is answering "is anyone else in here", which no amount of written discipline can. They are compared repo-relative, so the same file collides across worktrees, and they are released when the session stops.
Messages are a mailbox the recipient collects, not a write into its terminal. Typing into a live pane would corrupt the input of an agent mid-turn, and submitting on its behalf would let one agent put instructions into another's prompt with nobody watching. The cost of pulling is that the recipient has to ask, so every other tool result carries an unread count.
An agent can also read a sibling's work directly, and have it reviewed. work
returns the summary and patch of another session's worktree, which needs
nothing from that session — it is read while that agent is mid-turn and
interrupts nothing. analyse goes further and starts a separate agent to
review it.
That review is asynchronous, because a real one outlasts the tool timeout of
whatever asked for it, and because a blocking call is invisible exactly while
you would want to watch it. The reviewer is handed the diff, given no tools,
and run in a mode that answers but cannot act. analysis collects the answer
along with what it cost.
The sidebar shows ⊙ n for claims held, ✉ n for messages waiting, and ⚗ for
reviews. That last one is the only thing in Deck that spends money with nobody
watching it: the review has no pane, and the session that asked for it has
moved on. While a review runs the badge shows how many are in flight and the
tokens they have produced; once they land it shows the total spent, and that
figure stays so it is not lost the moment it stops moving. It is tokens during
and dollars after because the CLI reports a cost only when a turn ends — a
dollar figure while the review ran would sit at zero and read as free.
Everything above is scoped to one project, which is where sessions share a
repository. Press c to connect the selected session to one on another
project — an API changing in one repository while its consumer changes in
another is the case that scope cannot express.
A connection is a pair, and it widens rather than narrows. The two sessions
appear in each other's sessions, can read each other's work, can analyse
it, can message each other, and see each other's notes. Connect A to B and A
to C and A sees both, while B and C stay invisible to each other. Pressing c
on a session you are already connected to disconnects it. Connections are
saved, so they survive restarting Deck.
Claims are the deliberate exception: they stay inside one project. A claim is a
path relative to a repository root, so two sessions in different repositories
touching internal/api/client.go are not in each other's way, and saying they
were would make the whole mechanism worth ignoring.
A claude session reports what it is doing rather than having it guessed.
Deck writes a per-session hooks config and passes it with --settings.
UserPromptSubmit and PostToolBatch open a turn, Stop and StopFailure
close one, and the Notification subtypes that block on a person —
permission_prompt, idle_prompt, agent_needs_input, elicitation_dialog,
elicitation_url_dialog — mean a human is wanted.
Registering fewer of them costs a wrong dot. A turn boundary is not always one event, and some transitions are visible only through a later event rather than one of their own, so too small a set leaves a session reporting a state that never clears.
Needs you clears when the approved tool finishes rather than when you
approve it, so a long tool call still reads Needs you while it runs.
Narrowing that is in docs/backlog.md.
Not every way of ending a turn is observable. A pane that has gone quiet for
ten seconds therefore overrides a Working report, which covers any turn-end
that arrives without an event rather than one particular case.
| Dot | Meaning |
|---|---|
● Working |
a turn is running |
◆ Needs you |
a permission prompt, a question, or a turn that hit an API error |
○ Idle |
the turn finished |
✕ Exited |
the process is gone |
· Closed |
never opened, or stopped |
Filled is busy, hollow is quiet. The shape carries the state on its own — the sidebar card shows the dot without the word beside it, and a colour is no distinction at all in a low-contrast theme or to a reader who cannot separate two hues. The dashboard still prints the word, which is where the column is wide enough for it.
Needs you is the one the old heuristic could never show: an agent blocked on
a prompt prints nothing, so it looked idle.
The hooks post to the same localhost server the coordination tools use, with the session id in the URL. Their timeout is 5 seconds rather than the 600s default — if Deck is gone, a turn boundary should not stall the agent for ten minutes.
Sessions run with -agent cathode keep the heuristic: cathode spawns its own
claude with its own arguments, so a --settings given to cathode would never
reach the process the hooks describe.
Notes live in $XDG_STATE_HOME/deck/notes/<project>.jsonl. They are
append-only because several agents write at once, capped at 50 on read, and
the file itself is trimmed to its newest 100 entries once it passes 400 — an
append-only log with no trimming grows for the life of the project.
t opens the picker (^g t from an attached pane). The frame restyles as the
cursor moves, so you judge a theme by the app rather than by its name; ↵
keeps it, esc puts back what you had.
Twelve themes, using the same ids and source colours as cathode: Cinder (default), BBS, Dracula, Nord, Solarized Dark, Tokyo Night, Gruvbox Dark, One Dark, Monokai, Catppuccin Mocha, GitHub Dark, Rosé Pine. A name means the same thing in both tools.
They are deliberately not shared through a common file. The two apps have different role vocabularies — cathode needs diff-gutter and "you"-label colours Deck has no use for, and Deck's selected-row tint is dark where cathode's equivalent is a bright fill — so a shared schema would have to satisfy both forever to save an edit made twice a year.
The choice persists in $XDG_STATE_HOME/deck/settings.json.
Everything lives under $XDG_STATE_HOME/deck (default
~/.local/state/deck), which is where cathode keeps its state too:
state.json projects and sessions
settings.json preferences: the chosen theme
worktrees/<project>/<name> session worktrees
notes/<project>.jsonl the shared agent log
sessions/<id>.mcp.json generated coordination config per session
- Claude Code asks you to trust each new worktree. A fresh worktree is a
new folder, so the trust prompt appears once per isolated session. Press
1then↵. Deck does not pre-approve folders on your behalf. - Mouse is wheel-only. Wheel events scroll the attached pane. Clickable nav
items, tabs, and session cards need
bubblezone, which is not wired in yet. - Status is a heuristic for agents that do not report.
claudesessions report real turn boundaries through hooks (see Status below). Anything else —cathode, or a custom agent — falls back to "printed something in the last 900ms", which is wrong exactly when an agent thinks quietly.
Apache License 2.0. See LICENSE.



