Reading the table
One row per session, whether it is running now or ended weeks ago. Columns drop
from the right as the terminal narrows, worst-informing first, so the ones that
say which session this is survive to the last. Hover any header for what it
means, click one to sort by it, or press F6 for the sort list.
Hide columns you never read with $CCTOP_COLUMNS_HIDE, a comma-separated list
of the keys below — CCTOP_COLUMNS_HIDE=tok_rate,mem.
The columns
| Column | Key | What it says |
|---|---|---|
| (dot) | status | Whether the session is running, and what it is doing — see below |
LAST | active | Time since the session last did anything |
DUR | duration | First to last activity |
$ | cost | Estimated cost — see What the cost figures mean |
$/1H | cost_hour | Estimated cost in the last 60 minutes, rolling |
$/24H | cost_today | Estimated cost since local midnight |
CTX% | ctx | Context window used, as a share of the auto-compact threshold. COMPCT while one is happening |
CPU% | cpu | CPU across the session’s process tree |
MEM | mem | Resident memory across the session’s process tree |
TOOLS | tools | Tool invocations |
ERR% | errors | Share of those calls that failed — see below |
TOKENS | tokens | Input plus output |
TOK/m | tok_rate | Token rate, smoothed |
MODEL | model | Model in use |
HARNESS | harness | The host application — Cursor, a terminal CLI — as distinct from the model. ─ rather than a guess |
PERM | perm | How much it asks before acting — see below |
! | conflict | Another agent is on the same ground — see below |
HOST | host | Which machine, when reading more than one. Hidden otherwise |
USER | user | Whose session it is, when watching every user. Shown only while more than one user’s sessions are on the table |
PROFILE | profile | Which Claude or Codex login the session was read from, blank for the harnesses that have no such thing. Shown only when there is more than one |
BRANCH | branch | Branch checked out in the working directory, @<commit> when detached, ─ when not a repository |
PROJECT | project | The session’s title if it has one, otherwise its working directory |
The status dot
The left status dot is green while an agent is working, amber after its latest
response is waiting for your input, and red when the newest transcript event is
an API error. A hollow grey dot is a stopped session, and a filled ◉ is the
session that rang in the last 30 seconds.
A live session whose turn ended while you were not looking at it wears a ✓
in the accent colour instead of its amber dot: done, unseen. Amber says the
prompt is yours, which is true of most agents most of the time; the check says
this one has news since you last looked. It is cleared by looking, which means
one of two things — its row is the selected one while the dashboard is on
screen, or its pane is the focused pane of the tab you are on. A split’s other
pane does not count, for the same reason it still lights up the tab bar: you
are not reading it. The tab bar carries the same ✓ after the tab’s label,
and Alt+b goes there once nothing is blocked on a question.
The mark belongs to this cctop alone. A tab’s name and colour follow it into every cctop on the machine, but having read a reply on your laptop says nothing about the cctop on your phone, so seen-ness is kept in memory and not written anywhere. It also only marks a turn it watched end: a cctop started after an agent went quiet has no news to report about it.
A session past one of the alert_* thresholds you have set wears that alert
in place of its dot for as long as it stays past it: $ for its cost or burn
rate, a red ! for an error loop, ◌ for a working agent that has written
nothing for a while. See alerts.
Is it me, or is it them?
A red dot says a request failed. It cannot say why, and the answer is often somewhere else: a vendor incident, a dead proxy, an expired credential.
So cctop reads Anthropic’s and OpenAI’s status pages
(status.claude.com,
status.openai.com) every two minutes, in the
background, and sets what they say against the running sessions that are
failing. The footer says something only when there is something to say:
⚠ Anthropic incident: Elevated errors on Claude Opus — 2 of yours failing · !when the vendor reports an incident, with the count when it touches yours;⚠ 2 failing, Anthropic reports all clear: probably this machine · !when sessions fail while the page says all systems are operational.
! opens the vendor’s own account: each open incident, where they are with it,
since when, their latest update, and the components affected. r in it checks
again now. Sessions are matched to a vendor by model first and harness second,
so a Cursor session running Opus counts against Anthropic, and one running a
model cctop cannot place is left out rather than blamed on a guess. When a page
cannot be reached — offline, say — the footer stays silent and the panel says
the page was unreachable. footer_hide = "provider" leaves the line off.
PERM — how much a session asks
PERM is how much a session asks before it acts: ask, edits (writes files
unasked), plan (cannot act at all), or a red BYPASS for one started with
--dangerously-skip-permissions. Read from the transcript, and kept current by
the session’s own hooks when it has them. ─ means the harness does not record
it — today Claude Code and Devin do. Devin’s is its --permission-mode, read out
of the session row in its database, and its smart mode reports nothing rather
than picking the nearest bucket.
ERR% and compaction cadence — sessions that are not getting anywhere
A session can be busy and expensive without getting anywhere, and the columns that measure how hard it is working all read higher when that happens. Two that read the other way:
ERR% is the share of a session’s tool calls the transcript reported as
failed. A few is ordinary — a grep that found nothing, a build that caught a
mistake. A quarter of them is an agent retrying something that will not work,
paying full price for each attempt. Sort by it with F6 to put those at the
top; the Info panel gives the two numbers behind the rate, and Tool Activity
marks the individual calls with ✗.
It reads ─ where the harness records no per-call outcome — Cursor, Pi and
Windsurf — rather than 0%, which would claim a clean run cctop cannot see.
Claude, Codex, Devin, Gemini and OpenCode all report one. Devin’s comes from its
database rather than its transcript: a step records that a call was made, and the
call’s own row records how it ended.
Compaction cadence is under the Context panel’s chart. The sawtooth in that
chart is already the shape of a session living on compactions, but three of them
over two days is a long conversation while three in twenty minutes is a session
that will spend the rest of the day rebuilding a window it keeps refilling — and
the chart draws those identically. From three compactions on, the panel says how
often: ↺ one compaction every 15m. Claude Code only, since no other transcript
records that a compaction happened.
! — when two agents are in one repository
Two agents editing one checkout is not a merge conflict. Git would at least announce that. It is one of them writing a file the other is still holding in context, and the loser finds out when the work is already gone. cctop is the only thing on the machine that can see both of them, so it is the only thing that can say so while it still helps.
The ! column is that warning:
⚠ | another running agent has written a file this session also wrote |
· | another running agent is in the same repository, and has not touched your files |
| (blank) | nobody else is here |
Sort by it with F6, and the Info panel names the peer and lists the files. The
footer carries the ⚠ case only — agents share repositories all day and nothing
has gone wrong yet, whereas two of them writing one file means an edit has
already been lost or is about to be.
Some files are exempt, on one rule: nothing exempt is work that can be silently lost. Being plain text is not the test — source code is plain text, and losing a line of it is why the warning exists.
| prose | .md .markdown .rst .adoc .org .txt |
| machine output | .lock .sum .log, plus package-lock.json, npm-shrinkwrap.json, pnpm-lock.yaml |
Prose is what two agents are supposed to be writing at once — a changelog, a
notes file, a memory index — edited by section rather than rewritten whole. A
⚠ that fires every time both of them append to MEMORY.md is one that gets
read past, and it takes the src/ui.rs case with it.
Machine output is nobody’s handwriting. Two agents in one checkout both running
cargo add or npm install write a lock file every time, and the fix for a bad
one is to regenerate it; a real disagreement about a dependency surfaces in git,
which announces it properly. npm and pnpm spell their lock files .json and
.yaml, so those two are named outright — the extensions themselves stay
contested, because two agents editing one package.json is exactly the case the
warning is for.
The exemption is per file, not per session: share a notes file and a source file and the source file is still reported.
The unit of comparison is the repository root, not the working directory. A
linked worktree carries its own .git, so two agents in two worktrees of one
repository are editing two sets of files on disk and are not reported; two
agents started from different subdirectories of one checkout are. Comparing
directories gets both of those backwards, and the second is the arrangement
git worktree exists to provide.
The repository is the unit for the · case — the neighbourhood. A shared file
is stronger evidence and is not limited by it: two sessions holding one path
on disk are racing for it wherever they were launched from. An agent working
a parent checkout can write into a nested checkout another agent owns, and an
edit landing outside the repository has no ground to compare at all.
Four limits worth knowing. Two agents rewriting one README whole can lose an
edit, and the prose exemption above will not say so. Only running sessions are compared — a session that
has stopped may well have left uncommitted work behind, but nothing it does from
here can race anyone. Only the last 32 files each session wrote are watched, so
a path it finished with an hour and forty edits ago is not treated as contested.
And a Codex apply_patch covering several files summarises as
first.rs (+3 more) in the transcript, so only the first of them is recovered.
Agents can ask this themselves through check_conflicts — see
Letting agents see each other.
Telling the second agent
The ! column warns you, and by the time you look the second agent has
usually made its edit. With the hooks installed, cctop can tell that agent
instead, at the moment it reaches for the file. It is off by default; turn it on
on the settings tab (,) or in config.toml:
[settings]
warn_agents = true
From then on every file write a hook sees is noted in a small ledger beside the hook sockets — which file, which session, when, and the agent process that made it. When a session is about to write a file that a different agent, still running, wrote in the last 30 minutes, its hook answers with a note the model reads:
cctop: another agent that is still running on this machine wrote this file recently.
- /home/you/proj/src/ui.rs — 3m12s ago, by Claude Code session 1a2b3c4d working in /home/you/proj
Two agents editing one file do not merge: whichever writes last silently replaces
the other's work. Re-read the file before changing it again, and settle who finishes
first — or move one of you into a separate git worktree.
| harness | told on | through |
|---|---|---|
| Claude Code | PreToolUse of Write, Edit, MultiEdit, NotebookEdit | additionalContext, which reaches the model with the tool result |
| Gemini CLI | AfterTool of write_file, replace | additionalContext — BeforeTool has no way to add context |
| Codex | not told | its apply_patch writes are recorded, so the others hear about them |
It is advice and nothing more: the answer carries no permission decision, so it cannot block, prompt or deny, and the hook still exits 0 inside its 250ms deadline. Anything that goes wrong on the way — no ledger yet, a mangled one, another hook holding its lock — is the same silence as the setting being off.
It needs no cctop running: the hooks keep the ledger between themselves. It
makes the same exemption as the ! column (prose and lock files are never
mentioned), a session is never warned about its own writes, and a writer whose
process has exited no longer counts. Codex is recorded but never answered, and
Cursor neither, because neither documents what it does with a hook’s JSON on a
tool call — a guess there is how a monitor becomes an error on every edit.
Finding a session
/ filters the table as you type, on everything a row is: its label or title,
the full working directory (not just the abbreviation the column has room for),
the git branch, the model, the harness, the provider and the session id. The
cell that matched is underlined, so it is clear why a row survived the filter.
n and N step through matches, Esc clears, and ↑/↓ inside the prompt
bring back a search you ran before — the last twenty are remembered across runs.
user:<name> narrows to one person’s sessions when cctop is
watching every user, without the
name also matching every title that happens to contain it. Part of a name is
enough, the rest of the query still applies (user:ana flaky test), and two
user: terms show either user’s rows.
Tab widens the search to the transcripts themselves, which is how you find the
session where something was actually discussed rather than one whose name
happens to mention it. Transcript matches are added to the metadata matches
rather than replacing them, and the line each one was found on is shown under
the prompt.
A row found by a transcript match opens at the match: i on it puts the term
on screen with the reader’s own highlighting, a third of the way down rather than
at the top of a conversation it may be thousands of lines below. The footer says
“term” 3 of 12 · n N, so n and N walk the rest. If the reader cannot see
the term — the table matches a word with separators folded away, or corrects a
typo, and the reader matches what is drawn — the footer says so rather than
moving you anywhere.
This reads every transcript on disk, so it is opt-in, it waits for a pause in
typing and for a query of at least three characters, and it runs on cctop’s
background thread pool — the table stays live throughout, and the footer says
+transcripts… while a scan is out. Results are remembered per query, so
refining a search re-reads only what it must: a session cleared for refacto
cannot hold refactor, so each letter of a query narrows what there is left to
read. Two limits are worth knowing: transcripts store their text as JSON, so a
phrase containing a quote or a newline is escaped on disk and will not match;
and a single transcript file is scanned up to 64 MiB.
The tree view
T groups the table the way htop’s tree mode groups processes: each
repository gets a heading, each checkout of it a heading beneath that, and the
sessions hang off the checkout they were started in. When the rows shown belong
to more than one user — root watching every user
— each user gets a heading above their repositories, so two people in the same
repository are two groups that fold separately.
LAST $ BRANCH PROJECT
● 4s $18.40 ▾ ~/cctop 4 sessions, 1 waiting, 3 running
● 4s $11.02 main ├─ ▾ cctop (main) 2 sessions, 1 waiting, 1 running
● 4s $9.10 main │ ├─ Improve super cctop
○ 3h $1.92 main │ └─ Release 0.17.8
● 12s $7.38 tree └─ ▾ .claude/worktrees/agent-a9c7 2 sessions, 2 running
● 12s $5.01 tree ├─ Tree view for the table
● 1m $2.37 tree └─ Search tiers
The glyphs are drawn in the PROJECT column, so every other column stays under its header and sorting by one still works.
A repository is recognised by its git common directory, which every worktree of it shares — so a linked worktree lands under the repository it was taken from rather than as a stranger. A repository seen through only one checkout skips the second level. A directory outside any repository is a group of its own, and a row from another machine is grouped by host and directory, since the far filesystem is not there to ask.
F starts new parallel work from any of those rows. It asks for a branch
name, runs git worktree add at <repo>/.claude/worktrees/<branch> — where
Claude Code’s own --worktree puts them — and opens the launcher in the new
checkout, so the next agent you pick starts there and shows up under the
repository’s heading. A new branch forks from the checkout under the cursor; a
branch that already exists is checked out instead. .claude/worktrees/ gets a
.gitignore of its own, so the checkouts never appear as untracked files in
the repository they came from.
A heading carries what adds up: how many sessions are under it, how many of those are waiting on you and how many are running, their total cost, the latest activity, and for a checkout the branch it has out. Its dot is the loudest state beneath it, so a folded heading still shows red when something inside is asking a question.
Enter, Space or e on a heading folds and unfolds it. They do nothing else
there — a heading has no menu and cannot be marked — while ←/→ stay on the
bottom panels on every row, heading or not. Subagents still expand under their
session with e, one level further in. Sessions are never nested under each
other: no harness records that one session started another.
Filtering shows the sessions that match with the headings above them, and a
heading’s totals count only what is shown. The sort applies inside each group,
and groups are ordered by the same column applied to the group. Where the
column adds up — $, $/1H, $/24H, TOKENS, TOK/m, TOOLS, CPU%, MEM —
that is the group’s total, so a cost sort puts the repository that has cost
the most first, three $4 agents ahead of one $9 one. Where it does not — age,
context, a model name — it is the group’s best-placed session. Either way a
group is never split.
b unfolds whatever is hiding the session that rang. The view and the folds
are remembered across runs.
Idle sessions, and the memory they hold
An agent left open keeps its whole process tree: a claude process is a few
hundred megabytes, and an MCP server it started is often another hundred. On a
shared machine that is where the RAM goes — twenty sessions a few days old is
ten gigabytes that nobody is typing into.
I narrows the table to those: sessions that still have a process but have
written nothing for idle_after hours (6 unless , says otherwise). They are
sorted by the memory of their process tree, the root and every child — the
figure the Processes panel adds up — and the MEM column stays on screen however
narrow the terminal. LAST is how long each has been quiet. The table’s title
says what a stop would give back:
╭ Sessions (14/340) — idle ≥6h: 12 sessions, 5.4G reclaimable ──────╮
K in this view stops them. With rows marked it stops the marked ones; with
none marked it stops every one the view shows, since the view is already the
selection. A confirmation lists each with its idle time and memory, and the
total. Some are left running, and are named with the reason:
- working — its own hooks say a turn is under way;
- asking a question — a permission prompt or an elicitation is waiting on you;
- busy (N% CPU) — nothing in the transcript, but the process tree is using at least 5% of a core, which is what a long build or a background shell looks like from outside;
- on host — a row from another machine, which cctop reads and does not signal.
Stopping is SIGTERM to the agent, the same as Ctrl+K, never SIGKILL: the
agent writes its transcript out and takes its children with it, so R resumes
any of them later. What cctop cannot see is a prompt typed into one and not
sent, which goes with it — the confirmation says so.
I again, or Esc, puts the table back the way it was sorted. When a stop
would give back more than a gigabyte the overview says so beside the agents’
total, as Agent mem 10035 MB · 5.4G idle (I), so the view does not have to be
remembered to be found.
The row menu
Every action below has a key, and the keys are worth learning. But you have to know they exist first, and a key that quietly declines — because the row is on another machine, or is a subagent, or has no process left to signal — teaches nothing about why.
Enter on a row opens everything you can do to it:
╭ Improve super cctop ─────────────────────────────────────╮
│ Resume in a tab R │
│ Restart it in its tab it is not running in a tab here │
│ Attach to it a │
│ Type into it no local process to type into │
│ Hand off to another agent O │
│ Show its subagents e │
│ Mark for a batch action space │
│ ──────────────────────────────────────────────────────── │
│ Terminate the agent it is not running │
│ Delete the transcript d │
╰ ↑↓ Enter · Esc ──────────────────────────────────────────╯
Entries that cannot run stay on the list, greyed, with the reason where their key would be — one or the other, never both, since pressing the key of a refused entry only repeats the refusal. They are not hidden: a menu that changed shape from row to row would teach you less than one that says why.
The cursor never lands on a refusal, so Enter always does something. The
letters stay live inside the menu, so Enter R and a plain R are the same
two keystrokes — the menu shows the shortcuts rather than replacing them.
Clicking works too. Esc, or a click outside, closes it.
The settings tab
Everything cctop can be told to do differently is on one tab, at the right end
of the workspace bar. , goes there, clicking Settings goes there, and
Alt+←/Alt+→ step onto it like any other tab. It is not a popup: it is a tab,
so it holds the whole frame below the bar rather than a small box over the
table, and the bar stays clickable while you are on it.
It is in three parts, because the values live in three places:
| Part | Lives in | What is on it |
|---|---|---|
config.toml · [settings] | the file you wrote | the theme, the alerts, how long a session counts as idle, hidden columns |
Remembered · view | ui-prefs.json | the tree, the filters, the cost floor, which panel opens, the account a new tab launches under |
config.toml · [keys] | the file you wrote | every dashboard keybind |
/ filters the page, matched against a row’s name, what it does and its
current value — so alert, quit and 83.5 all find something. Enter
changes the row under the cursor: a toggle flips, a choice turns to the next
one, a keybind waits for the key you press next, and a number opens a field.
⌫ puts it back to its default, and e opens config.toml in your editor.
The middle part is the one that is not in config.toml. Those are choices cctop
remembers for itself in ui-prefs.json, and they are on the page because a
setting you cannot see is a setting you cannot find: until now the only way to
reach them was to remember which key pressed which one.
Every key
| Key | Action |
|---|---|
Enter | Everything you can do to this row, in one menu (see above) |
↑, ↓/j | Move between sessions |
PgUp, PgDn | Page up / down |
Ctrl+U, Ctrl+D | Half a page up / down |
g, G | Jump to first / last |
Home, End | Jump to first / last |
n, N | Next / previous search match (wraps) |
w | Toggle notifications (see below) |
W | Share the agent’s terminal to a browser (an agent in a cctop tab, see Driving agents) |
b | Jump to the session that rang last |
←, →, Tab | Move between bottom panels, Shift+Tab the other way |
1–9 | Jump to a panel directly; 9 is Preview, the selected row’s tab live (see The bottom panels) |
Shift+↑/↓ | Scroll inside the active panel |
Shift+Home/End | Jump to the top / bottom of that panel |
f | Follow mode: keep the selection centered |
/ or F3 | Filter sessions by text (see below) |
S, F6, >, < | Sort-by panel |
F7 | Filter by age (1d / 1w / 1mo) |
# | Cost floor: only sessions costing ≥ $X |
, | Settings and keybinds (see below) |
` | Show only running sessions |
I | Idle view: live sessions quiet for idle_after hours, most memory first (see above) |
[, ] | Move through the Tool Activity tool filter |
v | Toggle inline diffs for edits |
L | Toggle the Tool Activity live filter |
T | Tree view: group by repository and worktree (see above) |
F | Fork a git worktree and launch an agent in it (see above) |
+ | Add a Claude account, by running its login or setup-token in a tab |
Space | Mark / unmark the selected session |
D, K | Delete / terminate all marked sessions (with confirmation); in the idle view K stops the idle ones |
U | Clear all marks |
h or F8 | Agent integration: what reports to cctop, and install it |
! | Provider status: whether a vendor outage explains the failing sessions (see above) |
i | Read the conversation, full-screen (see Panels) |
y | Copy resume command or transcript path |
e, E | Show its subagents / show all subagents; on a tree heading, e folds and unfolds instead |
d | Delete the selected session (not running) |
Ctrl+K | Terminate the selected live session (with confirmation) |
s | Type a line into the selected session’s terminal (see below) |
R | Resume the selected session in a tab of its own (see below) |
Ctrl+R | Restart every agent tab on the same session, skipping any mid-turn |
O | Hand the selected session’s context off to a different agent (see below) |
a | Open that session’s terminal in a tab and drive it |
A | Open the agent this cctop launched itself, rather than one it found on disk |
t | New tab: run an agent, a shell, or Claude on an ssh host (see below) |
r or F5 | Refresh now |
o, c | What the spend was and not got back (optimize), and how each model did (compare) — see what it cost you for |
Y | Your year with the agents, a full-screen card at a time: hours, streaks, the clock you keep, top models and projects, spend at list price. ← → step, [ ] change year. Hours, days and projects reach back through Claude Code’s prompt history (~/.claude/history.jsonl), which is kept; spend, models and files only as far as the transcripts, which Claude Code deletes after cleanupPeriodDays (30 by default) |
B | Serve this table to a browser while cctop keeps running — see In a browser |
Esc | Clear the active filter |
? or F1 | Help: every key on one page, as [keys] has bound them, with the version and commit on its border; / in it narrows the page (see below) |
q or F10 | Quit |
Tabs and splits, from anywhere including inside a running agent:
| Key | Action |
|---|---|
t or Alt+n | New tab: an agent, a shell, an ssh host, or one still running |
Alt+v / Alt+s | Split the current tab right / down |
Alt+← / Alt+→ | Previous / next tab |
Alt+1–9 | Jump to a tab; Alt+1 is the dashboard |
Alt+t | Pick a tab from a list, typing to narrow it and Tab to pick by state (see below) |
Alt+b | Jump to the next tab whose agent needs you, then to one whose turn ended unseen (✓) |
Alt+r | Rename or recolour the tab you are on |
Alt+o | Move focus to the next pane |
Alt+z | Zoom the focused pane to fill the tab, or put the split back (see below) |
Alt+w | Close the focused pane and stop its agent |
Alt+Shift+W | The same thing, by a name that says so |
Alt+Shift+R | Restart the pane’s agent on the same session, after an update; on the dashboard, the selected row’s tab |
Alt+Shift+C | Record the focused pane to an asciinema .cast; again to stop (see Recording a pane) |
F9 | Paste the clipboard’s image (see below) |
F12 | Back to the dashboard, leaving everything running |
Alt+z zooms the focused pane over the whole tab, and the bar marks the tab
⤢ while it is. The other panes keep running out of sight — their output is
still read, so none of them stalls — but they are not resized: each agent goes
on at the size it had in the split, so zooming costs the hidden ones no
redraw. Only the zoomed agent is resized, once each way. Alt+o while zoomed
moves the zoom to the next pane; splitting again unzooms, so a new agent is
never started out of sight. An agent another cctop is also showing is drawn at
the smaller of the two sizes, as always, so zooming cannot grow it past that.
Every function key is cctop’s, inside a pane as much as on the dashboard: none
of them is passed to the agent. F10 (quit) and F5 (refresh) act where you
press them; F9 pastes into the pane you are in; F12 returns to the
dashboard; F1, F3, F6, F7, and F8
bring the dashboard forward and then do what they do there, since a search box
or a sort order over a pane would be drawn on a screen the agent is repainting.
An unbound function key does nothing rather than reaching the agent as an escape
sequence.
The Alt+t picker narrows two ways at once. Whatever is typed matches tab
names — every printable key is the name’s, j and k included — and Tab
cycles which tabs are listed by what their agents are doing: all of them, the
ones that need you, the ones working, the ones idle; Shift+Tab goes back. The
state in force is in the picker’s title (Go to tab · needs you), and it opens
on all of them every time. The tab you are on is filed by what it is doing too,
though the bar leaves it uncoloured, because with the picker over it you are not
looking at it. The dashboard is only listed under all.
In the help, / starts a filter: the page narrows as you type to the entries
that mention it, under their headings, with the query on the top border. It is
a plain case-insensitive match that takes in the key column, so ctrl+u finds
keys and half a page finds what they do; a heading that matches keeps its
whole section. Enter stops typing and keeps the filter, so the arrows and
j/k scroll the narrowed page, and / again goes back to editing it. Esc
takes the filter off first and the page away second.
Typing in a box
Every box that takes text — the / filter, the cost floor, the s send box,
the tab name, the launcher’s directory, the add-account name, a setting being
changed, the Alt+t picker and the help filter — edits the way a shell prompt
does, with readline’s keys:
| Key | Does |
|---|---|
← / →, Ctrl+B / Ctrl+F | Move a character |
Ctrl+← / Ctrl+→, Alt+B / Alt+F | Move a word |
Home / End, Ctrl+A / Ctrl+E | To the start / end |
Backspace or Ctrl+H, Delete or Ctrl+D | Delete a character before / after the cursor |
Ctrl+W, Alt+Backspace | Cut the word before the cursor |
Alt+D, Ctrl+Delete | Cut the word after it |
Ctrl+U / Ctrl+K | Cut to the start / to the end |
Ctrl+Y | Put the last cut back at the cursor |
A word is letters, digits and _, so in a path each component is one. Cuts in a
row add up — Ctrl+W twice and Ctrl+Y gives both words back — and they go to
the box’s own slot, not the system clipboard, so cutting never loses what you
copied to paste. A paste goes in at the cursor, with its line breaks flattened
to spaces. The cursor steps over an accented letter or an emoji as one thing,
and a value wider than its box scrolls to keep the cursor in view, with …
where it runs off.
These keys are the box’s only while one is open: on the table Ctrl+U and
Ctrl+D still page, and Alt+B still jumps to the tab that needs you. Each box
keeps the keys it already had — ↑/↓ for search history, Tab to complete a
directory, the plain arrows to pick a colour in the rename box (its name moves
by Ctrl+B/Ctrl+F, Home/End and Ctrl+←/→ instead), digits only in the cost floor.
Mouse works too: click session rows, column headers, and panel tabs; scroll anywhere. In Tool Activity, click any row to expand the full untruncated argument, and click the sidebar to filter by tool.
Anything that scrolls shows a scrollbar on its right border while it has more than fits, and only then. Click or drag along the table’s bar to jump the selection to that point of the list, top to first row and bottom to last; the bottom panel’s bar does the same for the panel’s text.
Settings and keybinds
, opens every setting and every key on the session table at the value it has
now, a * beside the ones you have changed. Enter changes the row under the
cursor — a toggle flips, a choice such as the theme turns to the next one (←
and → turn it either way), and a key waits for you to press the combination
it should move to — and Backspace puts it back.
A key another action was on is still taken; the status line says which action
lost it. e opens the file itself in $VISUAL or $EDITOR, and an edit saved
there applies at the next keypress.
It all lives in config.toml under your config directory, beside any account
tokens, and only what you changed is written:
[settings]
theme = "light" # auto / light / dark / mono
graph_glyphs = "octant" # braille / octant / sextant: what the charts are drawn in
tab_bar = "left" # top / bottom / left / right: where the tab bar sits
notify = true
compact_threshold = 90 # context % the agent compacts at
idle_after = 12 # hours quiet before `I` counts a live session idle
[keys]
quit = "x"
bottom = "shift+down" # ctrl+, alt+ and shift+ all work
Octants (Unicode 16) and sextants draw the charts as solid blocks rather than
braille dots, but only in a font that has them. The graph_glyphs row draws a
sample in each set, so a font without them shows its boxes there, before you
choose.
tab_bar moves the tab bar. bottom puts it on the row above the key hints;
left and right make it a column, one tab to a row, as wide as the longest
label within 16 to 28 columns. A column with more tabs than rows keeps the tab
you are on and counts the rest as +N, and a window under 60 columns wide
draws it on top, so the agents keep their width.
Only the session table’s keys move. A modal’s keys are the letters on its own buttons, and inside a pane the keyboard is the agent’s.
The help page follows what you bind: a moved key is shown where it now is, and
an action whose key you gave to something else, without giving it another,
reads unbound rather than naming a key that now does something different.
A line the file has but cctop could not use — listed as a problem at the top of
the , panel — moved nothing, and the help says so by not moving either.
A paste that arrives twice
Some layer between a terminal and an agent can deliver one paste twice when the
machine is loaded, so a sentence reaches the agent doubled. cctop drops a paste
that is byte for byte the last one and starts within paste_debounce_ms (250 by
default) of the last having been handed on. That holds in a tab, under
cctop run and under cctop attach. A key pressed in between means you meant
the second, so it is kept; a click is not a key, so the one that comes with a
right-click paste does not count. paste_debounce_ms = 0 turns it off.
Each dropped paste is a duplicate-dropped line under CCTOP_LOG, with its
size and the gap. An identical paste kept because it came after the window
is logged as duplicate-kept: many of those say the window is too short for
your machine. Only a bracketed paste is caught. A copy that arrives as plain
keystrokes goes through.
Typing on a slow remote tab
A tab on a sibling machine goes through Cloudflare, and each key waits a round trip — around 120 ms — before its letter appears. So cctop draws the letter first, underlined, and lets the far side’s echo replace it, as mosh does. A guess the far side contradicts, or does not answer in time, is painted back.
It guesses only plain letters typed one at a time at a visible cursor, or at
Claude Code’s own inverse-cell cursor, and only at the end of the text: typing
into the middle of a line is left to the far side. Enter, Tab, Esc, arrows, control keys,
pastes, and / or @ on an empty prompt stop the guessing until the far side
has caught up, and after each of them nothing is drawn until one guess has
come back right. An app that switches on the kitty keyboard protocol is not
guessed for at all.
predictive_echo sets it: auto (the default) draws only once the round trip
is over 60 ms, and stops below 30; always draws whatever it is; off never
guesses. Nothing extra is sent to the other machine either way.
Pasting an image
A terminal cannot carry one. A bracketed paste is text, so a screenshot copied
with the system’s own shortcut reaches an agent as nothing at all — which is
why Ctrl+V in a pane appears to do nothing when the clipboard holds a picture.
F9 is the way in. It reads the image off the clipboard, writes it to
~/.cache/cctop/pastes/paste-<when>.png, and types the path at the agent,
followed by a space — the form every one of these harnesses already reads an
image in. No image bytes go near the pty. On the dashboard the same key opens
the type-into box with the path in it, so a session you are not attached to can
be sent one too. For a few seconds after either, the image itself sits in the
bottom-right corner — the confirmation is the picture, not only its name.
Inside a pane Ctrl+V does it too, but only when the clipboard actually holds
a picture: with text on it the key goes to the agent untouched, as it always
did. Whether it arrives at all is the terminal’s decision — Windows Terminal
binds Ctrl+V to its own paste and never tells the application, so there F9
is the way in, or unbind it in the terminal’s settings.
The right button is deliberately not a third way. It was for a day: in Windows Terminal it copies when there is a selection and pastes only when there is none, so selecting some output and right-clicking to copy it pasted an image into the agent instead. cctop cannot see the terminal’s selection, so it cannot tell the two apart.
It needs something that can read the clipboard: wl-clipboard or xclip on
a Linux desktop, and powershell.exe under WSL, where the clipboard being read
is Windows’. Under WSL it is also looked for at
/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe, because a
session nobody logged into by hand — an ssh into the distribution, a cron job —
has none of the Windows interop entries on its PATH, and the same machine
would otherwise report having no way to read its own clipboard. cctop says which of the two things went wrong: an empty clipboard
and a machine with no such tool need different answers.
Pasting an image into a cctop you sshed to
No helper on the far side can reach it — the clipboard is on the machine you
typed the ssh on. So F9 goes the other way and asks the terminal that is
showing you cctop, over the escapes the connection already carries: kitty’s
clipboard protocol can hand back image/png itself, and any terminal that
answers the classic OSC 52 read gives up its clipboard as text, which files
the same when the text is a base64 image. Most terminals refuse the read —
the answer to “what is on the clipboard” is something no program should get
unasked — so kitty wants read-clipboard in clipboard_control before it
will say, and where the terminal stays silent F9 still says so rather than
telling you to install xclip.
For a terminal that will never answer — Windows Terminal does not, and no
Ctrl+V or right-click there can put an image on the wire — the way in is the
connection itself. ssh carries sockets back down the link it opened, so a
small listener on the machine you ssh from can hand the clipboard over.
tools/clipboard-bridge.ps1 is that listener, on Windows:
powershell -ExecutionPolicy Bypass -File clipboard-bridge.ps1
with the port forwarded — ssh -R 8377:127.0.0.1:8377 <host> on the command
line, or RemoteForward 8377 127.0.0.1:8377 in ~/.ssh/config to make it
permanent. cctop asks by connecting; the bridge answers with the clipboard’s
image, or with a closed connection when there is none. F9 uses it, and so
does Ctrl+V in a pane — the ask is a localhost connect that costs nothing
where no bridge is listening — so from Windows Terminal an image pastes the
way text does. The port is CCTOP_CLIPBOARD_PORT on the far side and -Port
on the bridge when 8377 is already taken.
The way that needs nothing of the terminal or a script is the browser. cctop serve on
that machine puts the table on an HTTP port; reach it from your own machine —
ssh -L 7788:127.0.0.1:7788 <host>, or --tunnel — and paste the screenshot
straight into the box that answers a waiting session. A browser can take a
real image off the clipboard where a terminal cannot, so the bytes travel over
the connection cctop already has, land in pastes/ on the far machine, and
the box fills with the path. Then write the sentence around it and send.
The other way needs no browser. What crosses a terminal is text, so send the
image as text: cctop reads any paste that is an image in base64 — bare, or as
a data:image/…;base64,… URI — writes it to the same pastes/ directory, and
gives the agent the path. The decoded bytes are sniffed for a real format —
PNG, JPEG, GIF, WebP, BMP — so an ordinary paste is never mistaken for one.
Encoding it is a one-liner where your clipboard is. In PowerShell, which is where you would be if you sshed from Windows:
Add-Type -AssemblyName System.Windows.Forms,System.Drawing
$i=[Windows.Forms.Clipboard]::GetImage(); $m=New-Object IO.MemoryStream
$i.Save($m,[Drawing.Imaging.ImageFormat]::Png)
Set-Clipboard ([Convert]::ToBase64String($m.ToArray()))
That replaces the clipboard’s image with its own base64, which then pastes into
the agent as an image. On a Mac or a Linux desktop the same thing is
pngpaste - | base64 | pbcopy and wl-paste -t image/png | base64 -w0 | wl-copy.
The images are kept, not cleaned up: the path is in a transcript by then, and a conversation resumed next week may read it again. The directory is yours to empty.
Right-click a row for its menu, the same menu Enter opens. The footer’s key
hints are buttons — clicking ? Help opens the help, clicking R Resume
resumes — and so are the [y] and [n / Esc] in every confirmation, where a
click beside the dialog is also a cancel. q Quit is the one that takes two
clicks: it is the only key on the footer with nothing behind it to ask again,
and the first click says so in the status line.
More than one account
Claude Code and Codex both let one machine hold several logins — $CLAUDE_CONFIG_DIR
for the first, $CODEX_HOME for the second — and a second subscription usually
exists because the first one runs out of window before the day does.
cctop finds them the same way for both: the directories beside the conventional
one that are actually signed in, so ~/.claude-work and ~/.codex-work are both
an account called work. Where there is more than one, the new-tab launcher
carries an as <name> line and p cycles it — the choice is remembered, because
somebody working out of their work login is working out of it all afternoon.
Everything downstream follows the account rather than assuming the default: rows
from every login appear in the table, the PROFILE column says which one each came
from, the limits panel reports each subscription’s own 5h and weekly figures, and
R resumes a session under the account whose directory the transcript lives in —
without which a resumed Codex session would come back blank, its id being unknown
to any other home. A tab launched or resumed under one names it on the bar —
claude (work) — so two agents on two subscriptions are not the same label twice;
the default account is the one left unqualified, as it is everywhere else.
An account whose limits cctop cannot read from its directory can be given a
token instead. claude setup-token prints a long-lived one, and
cctop --add-account work # reads the token from stdin, so it stays out of history
keeps it in config.toml under your config directory, readable only by you:
[accounts.work]
token = "sk-ant-oat01-…"
The name is the account’s. Where a ~/.claude-work exists it is that profile’s
token, winning over the credentials in the directory — being explicit is the
point of typing it in. Where no such directory exists, the token is the
account: it gets its own column in the limits panel, and its sessions stay in
the one ~/.claude with everything else, sharing the history, the settings and
the project trust rather than splitting them across a second directory. Start
one with CLAUDE_CODE_OAUTH_TOKEN set and that is the subscription it spends.
A setup-token token is made for model requests, and the usage endpoint may
refuse it. When it does, the panel says no limits for a token rather than
calling the account expired, and asks again only every few hours: the account
works, cctop just cannot measure it. A login into a ~/.claude-<name> of its
own is the way to see that account’s limits.
+ in the TUI is the same thing without leaving the dashboard: it asks for the
name, then offers claude auth login into a ~/.claude-<name> of its own or
claude setup-token, and runs whichever you pick in a tab of its own so you can
watch it and see the link when it prints.
Such an account is not offered in the launcher’s profile picker, and cctop’s
R never resumes under it: both work by putting the account in front of the
command, and the only thing there is to put there is the token itself — which
would be in every ps on the machine. Set the variable in the shell you launch
from instead.
CLAUDE_CODE_OAUTH_TOKEN wins over both, for the default profile only: Claude
Code prefers that variable too, so a session started with it spends the account
the variable names rather than the one the directory holds. One variable is one
value per machine, which is why it answers for one profile rather than being
reported as every account’s usage.
In the tab bar, drag a tab to move it along the bar — the arrangement is
written onto the rmux sessions, so it is still there after F10 and in every
other cctop on the machine — and right-click one, or press Alt+r on it, to
name it or paint it: 3:claude-4 says nothing about what that agent is doing,
and the name and colour you give it follow the tab into every cctop on the
machine. ←/→ walk the colours in the same prompt; the leading ○ stop is
no colour at all. A painted tab wears its colour in the bar and on the title
of every pane border it owns.