Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Driving agents from cctop

← back to the README

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:

The session runs…HowRequirements
under cctop run <agent>cctop owns the pty and typing goes through a unix socketnone
in a cctop tabsend-keys into the pane of cctop’s own rmux holding the agentnone
in a plain terminalTIOCSTI pushes bytes into the tty’s input queueLinux, 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).

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 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 Includes) 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 — 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.

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 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 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 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, 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 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 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, “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.

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

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.

--untilMet when
any-stop (default)it is not working — at once, if it already is not
idleits turn is over and the prompt is yours
waitingit is blocked on a question: a permission prompt, an MCP elicitation
doneit 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.

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 — which cctop installs itself on start, unless they were removed by hand. Without them 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:

[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:

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.