cctop 0.28.1

An htop-like terminal monitor for AI coding agent sessions on Linux (Claude Code, Codex, Cursor, Devin, Gemini CLI, OpenCode, Pi, Windsurf)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
# 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:

| The session runs… | How | Requirements |
|---|---|---|
| under `cctop run <agent>` | cctop owns the pty and typing goes through a unix socket | none |
| inside rmux | `rmux send-keys` into the pane holding the agent | rmux |
| 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 rmux
or the root path applies.

## 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 sandbox could not use is listed with why, dimmed
— `~/src/cctop  not empty here — the mount would hide it` — so you can still go
into it. `Enter` asks the host whether the path is there before taking it.

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

When [rmux](https://github.com/Helvesec/rmux) is installed, every tab's agent
runs inside an rmux session of its own 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 rmux-backed 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 turns the status bar off in the sessions it creates, and only in those. Its
row is duplicate chrome inside a pane that already has a border and a footer, and
its clock repaints on a timer — which, to anything watching for the screen to
stop changing, is indistinguishable from an agent still at work.

Without rmux installed, none of this applies and tabs behave as they always did:
the agent runs on a pty cctop owns and goes when cctop goes. cctop offers to
install rmux the first time a tab would have used it — `brew install rmux`,
`cargo install rmux --locked`, or rmux.io's install script, whichever this
machine can run — and one "no" holds for the run.

#### Why rmux and not tmux

cctop drove tmux until 0.8, and could be pointed at rmux with `CCTOP_MUX=rmux`.
It now drives rmux only. rmux reimplements tmux's command surface, so everything
above is the same set of commands it always was, and it answers something tmux
has no answer for: browser sharing.

The cost lands on upgrade, and is worth saying plainly: the two keep separate
daemons and separate sessions. Agents still running under a tmux server are
still running — nothing here kills one — but cctop stops being able to see them.
`tmux attach -t cctop-<provider>-<id>` reaches them, and `tmux ls` lists them.

### Sharing an agent to a browser

**`W` shares the selected agent's terminal to a browser.** cctop runs
`rmux web-share -t <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 over cctop's own TryCloudflare quick tunnel, the
same kind `cctop serve --tunnel` opens, handed to rmux as `--tunnel-url` rather
than letting it raise a second one through a provider of its own. One way out of
the machine, 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, and end it when you are done — `rmux web-share list` shows what is
currently shared and `rmux web-share off` ends all of it.

For the QR code and the read-only spectator link, run
`rmux web-share -t <session>` in a terminal yourself — it draws a card per role
that only renders to a tty. cctop reads only the operator link, and reads it from
stderr because that is the stream rmux puts it on; the spectator link goes to
stdout, and the two are the same shape, so the stream is the only thing telling
them apart.

### 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.

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.

| `--until` | Met when |
|---|---|
| `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.