# Driving agents from cctop
[← back to the README](../README.md)
cctop starts as a viewer, but it can also answer a waiting agent, reopen an old
session, and hold several agents on one screen. Everything here is optional —
the table works without any of it.
## Typing into a session
`s` opens a one-line prompt (prefilled with `continue`) and types it into the
terminal driving the selected agent, as if you had typed it there — useful for
the sessions whose status dot has gone yellow or red waiting on you.
An agent reads its keyboard from a pty, and only whoever holds that pty's master
side can put bytes into it — normally the terminal emulator, which offers no way
in. (Writing to `/proc/<pid>/fd/0` or `/dev/pts/N` reaches the *output* side and
just paints the screen; it does not reach the agent.) So cctop needs one of these
to be true, and tries them in this order:
| under `cctop run <agent>` | cctop owns the pty and typing goes through a unix socket | none |
| in a cctop tab | `send-keys` into the pane of cctop's own rmux holding the agent | none |
| in a plain terminal | `TIOCSTI` pushes bytes into the tty's input queue | Linux, and cctop as root — `CAP_SYS_ADMIN` clears both of the kernel's gates. Without root it also needs `sysctl -w dev.tty.legacy_tiocsti=1` (off by default since 6.2) *and* cctop sharing the agent's controlling terminal, which in practice it doesn't |
The first is the one worth adopting — no root, no multiplexer, and the session
looks and behaves exactly like one started directly. cctop sets it up for you: on
its first interactive run it aliases `claude`, `codex`, `devin`, `opencode` and
`pi` to `cctop <agent>` — a marked block appended to `~/.zshrc` and `~/.bashrc`, and
`~/.config/fish/conf.d/cctop.fish` for fish (each only where that shell is
already configured; the fish file is unverified, having been written on a machine
without fish).
```bash
cctop --remove-alias # take it out again; deleting the block by hand also works
cctop --install-alias # put it back
```
Every alias is guarded by a command-exists test, so it defines nothing
unless both cctop and the agent are installed — uninstall cctop and `claude`
goes back to meaning `claude`. The block is written once: remove it and cctop
won't add it again.
The `run` is optional: any first argument that names an executable is launched
this way, so `cctop claude --dangerously-skip-permissions` works and the flags
reach the agent rather than cctop. A word that isn't a command still gets cctop's
usage error, so a typo doesn't silently try to run something.
`cctop run` proxies your terminal byte-for-byte (including resizes) and exits
with the agent's own exit code, so it is a transparent stand-in. Sessions started
any other way still show up in cctop; they just can't be typed into unless they
are in a cctop tab or the root path applies. An agent you started inside your
own rmux or tmux is yours, and cctop does not reach into it: the alias above is
how it becomes typeable.
## Resuming a session
`R` reopens the selected session in a tab of its own, running the harness's own
resume command — `claude --resume <id>`, `codex resume <id>`,
`devin --resume <id>`, `opencode --session <id>`, `pi --session <id>` — in the
directory the session was working in. This is the way into a session cctop did not start: `a` can only
show the terminal of an agent cctop is already hosting, while resuming starts a
fresh agent from the transcript and so works for any session in the table,
however it was launched and however long ago it ended.
The tab is named after the session, not the command that reopened it — `claude ·
Improve super cctop` rather than `claude --resume 4ebf1ab4-2ef8-4fb2-a7d5-…`,
whose only variable part is a uuid nobody reads.
Cursor, Gemini and Windsurf keep their conversations inside an editor and have
no such command, so `R` says so rather than guessing at a flag; `y` copies their
transcript path instead. Resuming a session that is *still running* asks first —
two agents appending to one transcript is not something the harnesses
coordinate.
## Stopping the sessions nobody is using
The other end of resuming. An agent left open for days still holds its memory,
and on a shared server those add up to gigabytes. `I` shows the live sessions
that have been quiet for `idle_after` hours (6 by default), biggest first, with
the total a stop would give back; `K` there stops them — the marked ones, or all
of them — after a confirmation that names each, its idle time and its memory.
It skips any session that is working, asking a question, or still using CPU, and
says which and why. The stop is SIGTERM, so each agent exits cleanly and `R`
brings it back whenever it is wanted again. See [idle
sessions](the-table.md#idle-sessions-and-the-memory-they-hold) for the details.
## Tabs and splits
The session table is tab 1. `t` opens another: pick an agent — whichever of
`claude`, `codex`, `devin`, `opencode` and `pi` you have installed — or your
shell, and it starts on a pty cctop owns and draws in the window.
The launcher says where the agent will start, and `c` makes that line editable:
type or paste a path, `~` included, `Enter` to take it and `Esc` to keep the one
it had. An empty field means the directory cctop itself was started in. A path
that does not name a directory is refused in the field rather than at launch —
by then the launcher is gone, and the failure arrives from inside the pty as a
message about spawning.
You do not have to remember the path. The field lists what it can see under
itself: with nothing typed, the projects cctop has seen agents run in, most
recently used first; type part of a name and the list narrows by substring, so
`api` finds `~/work/api` without the `~/work`. A session that ran inside a
repository offers the repository rather than the directory it was sitting in, so
an agent that started in `~/work/api/src` lists `~/work/api` — the thing you
would name. Two checkouts of one repository are two answers, because they are
two directories to start an agent in.
Mixed in with those, one for one, come the repositories cctop found on disk:
your `git pull` of this morning is offered even though no agent has run in it
yet, newest first, and a long history of projects cannot push every repository
off the list. The scan looks three levels down from your home directory, which
covers `~/project`, `~/code/project` and `~/src/github.com/project`, skips
hidden directories, and never walks into a repository. It happens on a worker,
so the repositories appear a moment after the field opens rather than holding it
up — on the first opening too.
The field opens holding the directory the launch was headed for, and still
shows that list. Once you edit the text and it reads as a path — anything with
a `/` in it — the list comes off the disk instead: the directories under it,
hidden ones only once you type the dot. `Tab` fills in as far as the entries
agree (one match completes outright and adds the `/`, so you can walk down a
tree a keystroke at a time), `↑`/`↓` move into the list and back out into the
text, and `Enter` on a highlighted entry takes it. Clicking works too: once to
pick, again to take it. Reattaching to a running agent does not offer this: that
agent is already somewhere, and a path typed for it would be ignored.
The field also takes a place on another machine. Type `host:path` the way `scp`
reads it — `procdb:~/proj`, `me@10.0.0.5:/srv/api`, `[::1]:/srv` — or pick a
host: the hosts in your `~/.ssh/config` (and the files it `Include`s) are offered
under the directories as `procdb:`, matched by any of their names, one line per
`Host` line with its other names beside it; patterns such as `Host *` are
skipped. The moment a host is in the field, cctop connects to it in the
background — never asking anything, see [an agent here, working
there](integrations.md#an-agent-here-working-there) — and then completes paths on
it exactly as it does here: its directories under what you typed, and, before a
`/`, the git repositories three levels under its home. While it connects the
field says `connecting to procdb…`; a host that would need a password or a key
prompt says so (`procdb: needs a password or key prompt — the tab will ask`), and
`Enter` still takes the path, unchecked, for the tab to connect to
interactively. A directory the agent could not work in is listed with why,
dimmed — `~/src/cctop read-only on the host` — so you can still go into it.
`Enter` asks the host whether the path is there before taking it. Any directory
you can read and write on the host will do, whatever this machine has at that
path: it is mounted at the same path when it can be, and under
`~/.cache/cctop/remote/<host>/` when it cannot — see [where the mount
goes](integrations.md#where-the-mount-goes).
The launcher's line then reads `in procdb:~/proj`, and the agent you pick starts
there: Claude and opencode through `cctop sandbox` (their commands run on the
host, their file tools on a mount of it), your shell over `ssh -t` into the
directory. An agent with no way to run its commands on a host — codex, devin, pi
— is greyed out with the reason (`codex can't run commands on a remote host
yet`), and `Enter` on it says so instead of starting it here. The tab is called
`<agent> ⇄ <host>`. If `sshfs` is not installed the tab asks to install it
before connecting. A new tab starts on this machine again; a split keeps its
tab's location. `Alt+v` and `Alt+s` split
the tab you are in, side by side or stacked, so `claude` and a shell for
`git diff` are one keystroke apart. Fresh tabs start in the directory where you
started `cctop`; use `R` to reopen a session in that session's project directory.
Agent tabs also show their subscription-window usage and time until reset when
that provider reports it.
Each pane is resized to the rectangle it is given rather than cropped to it, so
a split is two agents each drawing a real screen — not two crops of one. The
Overview stays above the panes, so cost and alerts remain visible while you
answer an agent.
`a` does the same for a session already running: it puts that agent's own
terminal in a tab, live — its spinner, its permission prompts, whatever it is
drawing. That needs cctop to hold the pty, so it works for sessions started with
`cctop <agent>` and no others; the aliases above make that every session you
start from a shell. On attaching, the shim replays its recent output so the
screen is rebuilt immediately rather than at the agent's next repaint.
A tab you are not looking at is marked **green** when its agent has stopped
drawing — its turn is over and the prompt is yours — and blinks **amber** only
when it has explicitly asked a question and is blocked on the answer. Amber wins
when a tab has both. The tab you are on never blinks, since its focused pane is
already in front of you.
Green is most tabs most of the time, so a turn that ended while you were
somewhere else says so a second way: the label turns the accent colour and
carries a `✓` until you focus that pane, or select its row on the dashboard.
`Alt+b` walks every amber tab first and then the `✓` ones, so pressing it until
it says "Nothing is waiting for you" is a round of everything with news. See
[the status dot](the-table.md#the-status-dot) for what counts as looking, and
why the mark is this cctop's own rather than shared with the others.
`Alt+z` zooms the focused pane over the whole tab — for reading a long diff in
one half of a split — and again puts the split back. The tab wears a `⤢` while
it is zoomed. The panes you cannot see keep running and are not resized, so
zooming costs their agents nothing; see
[Every key](the-table.md#every-key) for the details.
A tab holding the login shell rather than an agent is left alone. Both colours
describe a turn, and a shell has no turns — it sits at its prompt drawing
nothing, which is exactly the silence the green is read from, so a tab opened
for `git diff` used to go green two seconds after the last keystroke and claim
an agent was waiting in a tab that held none. The bell still gets through: a
build that finishes with a `\a` is the thing in the pane asking for you
outright, not cctop reading its pixels.
The two are measured differently, because only one of them is in the transcript.
The question is: an agent that calls an ask-the-user tool records it. "Finished
its turn" is not — in a transcript, answering you and still thinking look the
same. So idleness is read off the pane's own screen instead: a working agent
repaints constantly (a spinner's elapsed counter alone ticks every second), so
two seconds of a still screen means the agent is waiting on you. That needs no
per-harness parsing and works for anything you open in a tab, shells included.
### Recording a pane
`Alt+Shift+C` records the focused pane to an [asciinema](https://asciinema.org)
v2 `.cast` file, and pressing it again stops and says where the file went. The
tab carries a red `● REC` on the bar for as long as any of its panes is being
recorded, so one left running is seen from every other tab. Closing the pane, or
its agent exiting, stops the recording too, and says so the same way.
Play one back with `asciinema play <file>`, or upload it to wherever asciinema
casts go. What is recorded is the terminal's own output — the bytes the agent
wrote to its pty, at the times it wrote them, before cctop's emulator read them —
so a player redraws the pane exactly rather than cctop's rendering of it. The
file opens on the screen as it stood when you pressed the key, since a recording
started mid-session would otherwise begin blank and fill in only as the agent
happened to repaint. The header carries the pane's size, and a resize — the
window changing, a split opening beside it — is recorded as one, so the replay
reflows where the pane did.
Recordings go to `~/.local/share/cctop/casts` (`$XDG_DATA_HOME/cctop/casts`),
named after the tab and the time. Not the cache directory, which
`--clear-cache` empties, and not the session's own directory, which is usually a
repository: a cast there turns up in `git status` and in the agent's own view of
the project it recorded.
In a split each pane is recorded on its own, because a cast is one terminal of
one size. A tab that is being recorded keeps its rmux client when you switch
away from it, rather than handing it back as tabs otherwise do — giving it up
would end the recording.
### Tabs outlive cctop
Every tab's agent runs in a session of cctop's own
[rmux](https://github.com/Helvesec/rmux), which is built into cctop — there is
nothing to install — and what cctop hosts is only the rmux client. Quitting
cctop detaches: the agent does not notice and carries on. On the way out cctop
says how many it left behind.
Opening cctop again restores those tabs automatically, with their scrollback
intact — and, since a tab's shape is recorded on its sessions, a **split comes
back as the one tab it was**: same panes, same order, divided the same way. A
tab's name and colour are stored the same way, so renaming or painting a tab
(`Alt+r`) and dragging it along the bar survive the restart too. Closing a pane
is the other thing entirely: `Alt+w` ends the agent, because a window you closed
should stay closed rather than come back at the next launch. The launcher (`t`)
still lists any running agents that are not already open, so you can attach to
them on demand. `R` on a session's row does the same thing by another route — a
resumed session's rmux session is named after it, so pressing `R` twice
reattaches rather than starting a rival agent on one transcript.
Each of those agents is listed by what the dashboard calls it, the directory it
is working in, and what it last reported through its hooks — `asking`, `working`,
`idle` — so which one to go back to is a decision rather than a guess. A ringed
dot (`◉`) means something is already attached to it, in another terminal or
another cctop; attaching a second client works, but the two then share one
window's size.
This is the same hook feed the dashboard uses, and it reaches these agents for
the same reason: cctop looks up the process *inside* the rmux session rather than
the client in front of it. So a tab whose agent is in rmux still blinks when that
agent asks a question, and still keeps quiet while it is only thinking. `a` on a
session's row uses that lookup in reverse and opens the agent's own terminal,
whether cctop is holding its pty or rmux is.
#### cctop's rmux is its own
The daemon is the cctop binary run as `cctop mux daemon`, on a socket under
cctop's runtime directory (`cctop mux socket` prints it). It starts with the
first tab and exits when its last agent does. It reads no config of yours —
not `~/.rmux.conf`, not `~/.tmux.conf` — and starts every session with cctop's
settings: a 50,000-line scrollback, the mouse on so the wheel scrolls, and no
status bar, whose clock would make an idle agent's screen look busy forever.
Nothing else on the machine knows about it. If you use rmux yourself, `rmux ls`
does not list cctop's agents and your `rmux kill-server` does not end them, and
cctop never lists, attaches to or changes a session of yours.
`cctop mux ls` lists its sessions and `cctop mux kill-session -t NAME` ends one;
`cctop mux attach NAME` attaches your terminal to one, as `rmux attach` would.
**Upgrading from cctop 0.31 or older.** Those versions kept their agents in
the rmux you installed. Agents still running there carry on, but this cctop
does not see them: it only looks in its own daemon. `rmux ls` lists them and
`rmux attach -t cctop-<name>` reaches one; `cctop doctor` says the same when it
finds an `rmux` on your PATH. They end when you end them, and new tabs go to
cctop's own daemon.
An agent you start inside your own rmux or tmux pane by hand, not from a cctop
tab, is also yours: cctop still shows it on the dashboard, but types into it
only through [`cctop run`](#typing-into-a-session) or the root path, never
through your multiplexer.
### Sharing an agent to a browser
**`W` shares the selected agent's terminal to a browser.** cctop asks its rmux
daemon for a web share of the agent's session and puts the operator link on your
clipboard — open
it on a phone and you are typing into that agent. The pairing code goes on
cctop's status line; the link never does, because it grants input to a live
coding agent and a status line survives into a screenshot. When the share
reaches off this machine, `W` also opens a panel with the link as a QR code and
the pairing code under it, for pointing a phone at; any key closes it. The code
is the link — the panel says so — and on a terminal too small to hold it the
panel says that instead.
The link reaches this machine through a Cloudflare tunnel of cctop's, handed to
rmux as `--tunnel-url` rather than letting it raise one through a provider of
its own. With [your own Cloudflare account](serve.md#your-own-cloudflare-account)
connected and a serve running in this cctop — `cctop serve --tunnel`, or `t` in
the dashboard's serve panel — it is that tunnel, on the `-share` hostname setup
made beside the page's: one tunnel, two hostnames, and no second one opened.
The page's hostname is never used for a share, because everything there wants
the page's token and a share link must not carry it. Stop that serve and the
share links on the `-share` hostname stop with it; the next `W` mints a new one.
On that hostname the link is the address itself —
`https://cctop-share.example.com/#…` rather than a page on `share.rmux.io` —
because cctop serves rmux's browser app there too, from the same pinned copy the
page frames its terminals with. What answers a share hostname is a server of its
own with two things on it, the app and the share's socket, and no route of the
dashboard's at all: a share link opens a terminal and never the page, whatever
token anyone adds to it.
**Choosing an agent's address.** The panel `W` opens shows the hostname the link
goes out on (never the link). On your own domain, `e` or a right-click on that
line asks for a name: `myagent` puts the agent on `myagent.example.com`, mints a
fresh link there, copies it and redraws the code. The name sticks to the agent
— the next `W`, after a restart too, goes out on it — and two agents can be
shared at once on two names. An empty field sends it back to the `-share`
hostname. The old name's DNS record is deleted, so links on it stop answering.
The session page in the browser has the same rename on its `Address` row. A
quick tunnel's address is Cloudflare's pick, and an account connected with a
tunnel token cannot write DNS, so neither offers one; the panel says which.
Otherwise it is a TryCloudflare quick tunnel, the same client `cctop serve
--tunnel=quick` uses: opened on the first `W` of a run and closed when cctop
exits. If the tunnel cannot be registered the share still happens and the status
line says `this machine only` — a link that works from this browser is worth
more than no link, and a link that silently reaches nothing is worth less than
either.
rmux ships six tunnel providers of its own and deliberately not this one: a
`trycloudflare.com` hostname, [its docs say](../rmux/docs/web-share.md), "can
take an unpredictable amount of time to become reachable" and carries no uptime
guarantee — so it points anyone who wants one at `--tunnel-url`, which is the
door cctop comes through. Expect a link to 502 for a moment after it is minted,
and reach for a named tunnel or your own ingress for anything that has to stay
up. rmux also cannot tell viewers apart by source IP once traffic arrives
through a tunnel, so its per-viewer caps are not a control you have here.
What the tunnel carries is not what `serve` puts through one. rmux encrypts the
share end to end and pairs it with a PIN, so Cloudflare moves ciphertext it
cannot read; the served page is ordinary HTTPS that Cloudflare terminates. Both
are still a credential to a live coding agent: share the link the way you would
share a shell. A share lasts six hours at most, and ends with its agent's
session — closing the agent (`Alt+w`) ends every share of it.
rmux mints a read-only spectator link beside the operator one. cctop does not
hand it out: the two are the same shape and grant different things, and only
the operator link is asked for, as its own field.
### What cctop keeps, and what goes to the agent
Panes cctop started are cctop's to end: closing one (`Alt+w`, or the agent
exiting on its own) takes the agent with it, rmux session included. Quitting
cctop is the opposite and leaves them running. A pane opened onto someone
else's session with `a` only stops watching.
Everything cctop does not claim goes to the focused agent, `Ctrl-C` included:
inside a pane, that interrupts the agent rather than quitting cctop. The
function keys and `Alt` are what cctop keeps — the function keys because they
are the ones agents never want, and `Alt` because `Ctrl` is the agent's. No
function key is passed on: `F10` quits and `F5` refreshes where you pressed
them, `F12` goes back to the dashboard, and the rest bring the dashboard
forward and act there. The full list is in
[Reading the table](the-table.md#every-key).
One pair of keys is translated rather than kept or passed through. Claude Code
puts the top and bottom of the chat on `Ctrl+Home` and `Ctrl+End` — the right
action, a chord too many for keys already named after where they go — so in
one of its panes the bare `Home` and `End` are promoted on the way in.
Everywhere else, a shell or another harness, they keep their line-editing
meaning, and the shifted forms are never touched: `Shift+Home` is still
"select to the start of the line".
### Shift+Enter, and the keys a terminal cannot spell
Enter is a carriage return, and Shift+Enter is the same carriage return: the
difference exists only in a protocol invented to carry it. So it has to be
asked for at both ends. cctop asks its own terminal for the disambiguating
form on the way in, where the terminal says it can send one, and sends
Shift+Enter on to the agent as `CSI 13;2u` — but only to an agent that turned
a keyboard protocol on, or through rmux, which rewrites it to a plain Enter
when the pane's program did not. A shell in a tab is never handed a sequence it
would print as text. The daemon answers the kitty negotiation itself (the fork
carries Helvesec/rmux#227), so a pane that asked only for kitty, as Codex does,
gets `CSI 13;2u`, and one that also turned on `modifyOtherKeys`, as Claude Code
does, gets xterm's `CSI 27;2;13~`.
Which agents ask, as of writing: Claude Code turns on both the kitty protocol
and xterm's `modifyOtherKeys`, and Codex turns on kitty and explicitly turns
`modifyOtherKeys` off. If your terminal has no extended keyboard protocol at
all, nothing changes: `Ctrl+J` is the newline every agent also accepts.
### The questions an agent asks the terminal
An agent asks the terminal what it can do before it draws anything — Codex asks
five questions, Claude Code two. Under `cctop run` the real terminal is behind
cctop and answers for itself. A pane is different: cctop rebuilds the screen
from the agent's output, and a picture of a terminal has nothing to say back,
so a hosted agent's questions used to go into the dark and it assumed the least
about them.
The shim now answers the ones with an honest fixed answer: device attributes,
device status, synchronized output, and the foreground and background colour —
which it reports from cctop's own palette, so an agent set to follow the
terminal's theme follows the pane it is drawn in. The cursor position is
deliberately not answered; the shim relays bytes rather than parsing them, so
it does not know where the cursor is, and a made-up position would put an
agent's first frame in the wrong place.
## Waiting for an agent to finish
```bash
cctop wait 3f2a # a session id prefix
cctop wait reviewer --until done # a tab's name
cctop wait 48213 -j --timeout 30m # a pid anywhere in the agent's process tree
```
blocks until that session stops working, then exits — so a script, or another
agent, can hand work to one agent and pick up when it is done. It reads what
the dashboard reads: the transcripts, the agents' own hooks (it listens for them
the way a running cctop does, so a finished turn is heard the moment it
happens), and the state a cctop recorded on the agent's rmux session.
| `any-stop` (default) | it is not working — at once, if it already is not |
| `idle` | its turn is over and the prompt is yours |
| `waiting` | it is blocked on a question: a permission prompt, an MCP elicitation |
| `done` | it has worked since the wait began, and stopped since — the one to use after typing it a prompt, which may not have landed yet |
It exits 0 when the condition is met, 1 when the session ended first without
meeting it, 2 when the target names no session or more than one, and 124 on
`--timeout` (default `10m`; `0` waits forever), `timeout(1)`'s own code. `-j`
prints the outcome as one line of JSON: `session`, `pid`, `label`, `state`,
`until`, `met`, `timed_out`, `waited_secs`. An agent can ask the same thing
over MCP with `wait_for_session` — see
[Letting agents see each other](integrations.md#letting-agents-see-each-other).
A Claude Code transcript cannot say that a turn is over — answering you and
still thinking look the same on disk — so for Claude that answer comes from its
hooks. Install them (`cctop --install-hooks`), or a Claude session that went
quiet before the wait started reads as working until its next event, unless the
agent is in a cctop tab and a running cctop recorded its state there.
## Getting pinged when a session needs you
cctop is a monitor you look away from, so `w` turns on the other direction:
when a session that was working starts waiting for your input — or the agent
exits — cctop rings the terminal bell and raises a desktop notification. The
setting is off by default and remembered between runs.
Both are the terminal's own mechanisms, so nothing has to be installed. `BEL`
is what rmux turns into a window's bell flag; the desktop notification
is OSC 9, which iTerm2, Ghostty, kitty, WezTerm and Windows Terminal raise as a
real notification and everything else quietly ignores.
It rings on the *crossing*, never on the state: a session that is waiting for
you is still waiting on the next refresh, and ringing for that would be an
alarm clock. For the same reason, turning notifications on doesn't ring for
every session that happens to be idle at that moment — cctop tracks the states
whether or not the bell is on, and starts from what it already knows.
The session that rang keeps a bell marker (`◉`) on its row for 30 seconds, and
the footer keeps naming it (`Bell: ◉ cctop · waiting for input · 12s ago`)
until you select it. `b` jumps straight there. A bell out of a dozen panes is
never a mystery, even if you were away when it rang.
One thing it deliberately does *not* ring for: an agent that has simply
finished its turn and is sitting at its prompt. In the transcript that looks
the same as an agent still thinking, and a timer would fire in the middle of
every long reasoning turn.
### Alerts on spend, error loops and stalls
The same machinery can watch a session's numbers as well as its state. Each
alert is off until you give it a threshold in `[settings]` (or with `,`, where
Enter on the row opens a field), and `0` turns it back off:
```toml
[settings]
alert_cost = 20 # a session's COST passes $20
alert_burn = 30 # a session burns more than $30 an hour
alert_today = 100 # today's spend across every session passes $100
alert_errors = 25 # 25% of its tool calls in the last 10 minutes failed…
alert_error_calls = 10 # …once there are at least this many calls in them
alert_stall = 10 # a working agent has written nothing for 10 minutes
```
When one fires, cctop says so in a toast, and — with `w` on — rings the bell and
raises the desktop notification, one ring per refresh however many fired in it.
With `$CCTOP_NOTIFY_URL` set, each is POSTed as `{"event": "alert", "text": …}`.
The row takes a marker in place of its status dot for as long as the reading is
still past the threshold: `$` for cost or burn rate, `!` in red for an error
loop, `◌` for a stall. The day's spend has no row, so it is the toast alone.
Each fires on the *crossing*, like the bell, and not on the refresh after. It
can fire again only once the reading has come back below four-fifths of the
threshold, so a burn rate hovering around its limit is one alert rather than
one a refresh. A session already over a threshold when cctop starts — or when
you lower the threshold under it — is marked but not rung for: nobody watched
that crossing happen. The day's spend starts over at local midnight, and so
does its alert.
The error loop is measured over the last ten minutes, not the session's whole
life: `ERR%` barely moves when a session that made three hundred good calls
starts failing every one, so the alert takes the difference of the two counters
the transcript already records across the window. The window only holds calls
this cctop saw — it starts when cctop first sees the session.
A stall needs the agent's hooks (`cctop hook --install`), and cctop will not
guess without them. A transcript cannot tell three silences apart: a turn that
is over, a tool call still running, and an agent that has stopped getting
anywhere all leave the newest record where it was, and for most harnesses a
finished turn reads as work in progress. So a stall is a session whose row and
hooks both say it is working, with no tool call open, and no new record for the
threshold — counting a running subagent's transcript, since a parent waiting on
its subagent writes nothing of its own. A tool call that has not come back is
never called a stall: a hung command and a half-hour build look identical from
outside, and an alert on every long build is one people learn to ignore.
Alerts only ever tell you. cctop never stops, pauses or signals a session for
crossing one — an agent over budget may be a minute from finishing the job the
budget paid for.
### When the agent is the one ringing
That is cctop's own bell, rung about a session's state. An agent can also ring
for itself — Claude Code with `preferredNotifChannel: terminal_bell` does
nothing else, and several harnesses send an `OSC 9` notification — and a tab
whose agent rang turns the attention colour immediately, ahead of every
inference the tab bar otherwise makes. If the notification carried text
("Claude needs your permission to use Bash"), the status line shows it once,
when you switch to the tab.
This matters because it is the only signal that arrives *in time*. Everything
else the tab bar has is inferred: a hook event on a six-second timer, or a
screen that stopped moving. An agent blocked on a permission prompt keeps its
spinner turning and reports itself as working, so it used to be drawn as busy
for as long as it sat there.
The bell survives the whole stack — the agent rings, rmux passes it to its
client, the shim relays it, and the pane's parser keeps it instead of parsing it
away. cctop does not pass it on to your own terminal: the bell is answered by
looking at the pane, and a beep per agent per prompt is the alarm clock this
page already argues against.
## Handing a session to a different agent
`O` takes the selected session's context across to another harness. Where `R`
puts the *same* agent back on the *same* transcript, a handoff carries what the
session was doing over to a different agent entirely — the one thing no harness
can do for itself, since each can only read its own transcripts.
cctop writes a markdown brief, opens the launcher, and starts whichever agent you
pick with a line pointing it at the file. Where the harness takes an opening
prompt on its command line — `claude`, `codex`, `opencode` — that line is part of
the argv, so the agent opens on the brief. Anything else is typed at once it has
had a moment to start listening.
The distinction matters more than it looks. Every one of these CLIs asks the
terminal what it can do as it starts, and reads its own input looking for the
answer; a line arriving in that window loses however much of itself was in the
queue at the time. Handing a Claude session to Codex used to produce a prompt
beginning halfway through the path, so Codex went looking for a file that had
never existed and asked for the brief to be pasted instead. An argument cannot
be eaten that way.
The brief holds the task, the plan the
session was working to, the files it changed and read, the commands it ran, what
it delegated, and what it looked up — with paths relative to the project, and
every list bounded so a long session hands over its most recent and most-touched
entries rather than all of them.
It is deliberately not a transcript. Replaying a conversation into a fresh
window spends the context it is supposed to save, and most of what it spends it
on — tool output, file contents the new agent can read itself — is what the
receiving agent should gather first-hand anyway. What does not survive a restart
is the intent, and that is what gets carried.
Because it is built from the normalised session data rather than from any one
transcript format, it works from and to all eight harnesses.
### Claude to Claude, the conversation goes whole
The brief exists because no harness can read another's transcripts. Between two
Claudes that limit is not there, so handing a Claude session to `claude` copies
the transcript instead: the file is written into the receiving account's
`projects/` directory under a fresh session id, and the new agent is started with
`claude --resume` on the copy. It opens knowing everything the first one knew,
not everything a summary could carry.
It is a copy, not a second agent on one transcript. The two diverge from the
moment the fork is taken, and the session that was handed over is left exactly as
it was found — still listed, still resumable, still the only writer of its own
file. That is also what makes this different from `R`, which refuses to put a
second agent on a transcript for precisely that reason.
### Claude to Codex, and back
Between two harnesses that keep a file of JSON lines, the copy can be made by
reading one and writing the other instead, and cctop does that: handing a Claude
session to `codex` writes a rollout into `~/.codex/sessions/`, and `codex resume`
picks it up. The reverse works the same way.
That is the same trade as the Claude-to-Claude fork, for the same reason and
with the same cost — the whole window goes across, tool output and all — but
between two harnesses whose vocabularies differ, so something is left behind.
The conversation crosses: what was asked, what was said, and every tool call with
its result. The rest does not, because the receiving harness can work it out for
itself and the sending harness's figures are about a window the receiver is not
using. That means token counts, costs, the model, and reasoning — the last
because it is signed for the model that wrote it and is unreadable to any other.
The copy keeps the session's **own id** where the receiving store has it free,
which is what lets cctop recognise the two as one piece of work rather than two
sessions that arrived in the same directory. Where the id is taken, a new one is
minted instead — a handoff never overwrites a conversation — and the transcript
records which session it came from either way, so `cctop --converted` can list
the copies and `--remove` clear them out.
OpenCode is not in this set. It keeps its transcripts in SQLite tables beside a
project's real state, and writing rows into a live store another agent is using
is a different kind of risk from adding a file to a directory of rollouts. It
gets the brief, which is the only form it can read today.
The brief is available without the UI, as is a conversion:
```bash
cctop --handoff # the most recently active session, as markdown
cctop --handoff 2abd15fe # a session id, or any unique prefix of one
cctop --convert 2abd15fe codex # the conversation, in Codex's own store
cctop --converted # the copies on this machine, and where they came from
cctop --export 2abd15fe # the whole conversation as markdown, to paste anywhere
cctop --export 2abd15fe --tool-output # with each tool result, cut at 800 characters
```
Where the brief is a summary, `--export` is the transcript: every turn, the
words verbatim and quoted so a heading or a code fence inside a message cannot
break the document around it, each tool call on one line, thinking left out.
The session page's ⋯ menu copies or downloads the same document, and the brief
beside it.
`--convert` writes the transcript and stops there; it does not start an agent.
Use the UI's `O` for the launch, which resumes the copy as it goes.