Expunge a Session
Status: implemented (openbus/expunge.py, the pane’s ⋯ menu).
Why
“Kill window” ends a process but leaves everything the agent wrote behind: the transcript, its subagents, file snapshots, the prompt history, and agent-history’s index of what the human typed. Sometimes a session should not survive at all, such as one that saw a secret or one the user simply wants gone. Expunge is that action: it kills the window, then deletes that one session’s local files.
The caveat, stated up front
Expunge is local. A session’s content may also exist where this machine cannot reach: spans and logs already exported through OpenTelemetry, and whatever Anthropic or OpenAI keep on their side under their own retention. The confirmation says so in plain words, because a button called “Expunge” otherwise implies a guarantee it cannot give.
Exactly one session, found by its id
The design rule is that Expunge only deletes what is named by the session’s id. It never deletes by date, project directory, or a broad glob. Deleting a project directory would take every other session in that repo with it, and a mistake here cannot be undone. So the hard part is the identity, not the deletion.
- Claude Code registers each running session as
sessions/<pid>.jsonunder its config dir. The daemon walks the pane’s process tree and takes the agent whose registration matches its pid and kernel start time, so a stale file left by a reused pid never counts. A registration that can’t be read refuses the whole expunge, since it could be this pane’s own session. The config dir comes from that process’s ownCLAUDE_CONFIG_DIR, not the daemon’s, so a pane running a second profile is handled correctly. - Codex keeps no registry, and its shared app-server, not the terminal client,
holds the rollout open, so no process proves which thread a pane shows. The one
reliable signal is the thread id on the status line, which is the same evidence the
Live resume path already trusts (
live._codex_pane). The id is the segment atsession-id’s place in Codex’s[tui] status_lineorder, never just any UUID-shaped segment, since a branch can look like one. Codex leaves out empty items, so that place is trusted only when every item shows orsession-idcomes first. Otherwise Expunge refuses and says, in the sheet, which line ofconfig.tomlto change.
The walk stops at the first agent down each branch, so an agent’s own subprocesses
(a headless claude -p it ran) are not a second session. Two agents side by side, a
Codex screen showing zero or two thread ids, or an id that isn’t a UUID: each refuses
with the reason.
Some panes have nothing to expunge: no agent at all, Codex’s sign-in (codex login),
or an agent that has an id but no transcript yet (a fresh session, or Claude’s own
sign-in) and no agent-history entry either, since that entry outlives the transcript. That is not a fault, and a refusal there read as one: a sign-in window got
the status-line config hint, which was both jargon and beside the point. So the
preview answers “nothing”, the same way for both harnesses, and the sheet offers to
kill the window instead, through the ordinary kill and its confirmation. Treating a
missing transcript as nothing also means a configured but not-yet-started Codex
thread is no longer refused as an unknown id. A Codex without session-id configured
can’t name its thread, but if it is not a resume and no rollout has been written since
it started, it has no thread to name, so it is nothing too. Any rollout written since
counts against that, even another pane’s, because a wrong “nothing” would hide a real
conversation, while a wrong hint only asks for a setting. A fresh agent still counts
as an agent: beside a saved session it makes “more than one”, never a quiet pick. None of them falls back to anything broader. The UUID check
also means an id can never smuggle a path separator or glob character into a pattern.
The confirmation is bound to the id the preview showed, and both preview and POST
carry the pane incarnation the menu was drawn for (its birth, the pane pid in
/api/state). tmux reuses %N, so a stale menu, or a pane now running a different
session, is refused.
If the connection drops before the answer arrives, the UI says the expunge may have completed rather than calling it a failure. There is no operation-status endpoint: the window is gone either way, and whether the session still resumes is the check that matters.
What is deleted
Claude, under the agent’s config dir: the projects/*/<id>.jsonl transcript and its
sibling <id>/ (subagents, tool results), file-history/<id>, session-env/<id>,
tasks/<id>, todos/<id>-* and debug/<id>.txt, the agent’s sessions/<pid>.json
registration if it outlived the process and still names the session, plus the
session’s lines in history.jsonl.
Codex, under CODEX_HOME: the sessions/…/rollout-*-<id>[_<segment>].jsonl files (a
resumed thread has several), archived_sessions/, shell_snapshots/<id>.*, and the
thread’s lines in history.jsonl and session_index.jsonl.
agent-history: the session’s index entry and its subagents’ entries, under the agent’s own
AGENT_HISTORY_DIR (or the default). That index exists to outlive the harness’s own
retention, so leaving it would defeat the point.
tmux-rc itself: the pane’s checkpoint row (its card, summary and recent events), which would otherwise stay on disk until the next restart prunes it. A tombstone stops any tick in flight from writing it back. If the database is busy, the deletion is retried every tick, and the response reports the failure instead of claiming success. The structural pane history (counts and states per minute) holds no session content and is left alone.
Order and safety
- Identify the session and resolve every target while the agent is still alive. If any path, through a symlink, resolves outside its root, refuse before anything is killed.
- Kill the window, using the same path as “Kill window”, but only while the pane
still belongs to the process that was identified. tmux reuses pane ids, and a
recycled
%Nmust never take a newer window down with it. The tmux server makes that check and the kill in one command (if-shell -Fonpane_pid), so the id can’t change hands between the two. - Wait for the agent process to exit. If it has not exited after a few seconds, delete nothing, since a live agent would only write the files again.
- Resolve the targets again and delete them. Shared logs are rewritten through a temp file and an atomic rename, keeping their mode. A line that can’t be parsed, such as one a crash cut short, is dropped as well if it still shows the id as its own key. It is already corrupt, and keeping it could keep this session’s words. The result reports counts and basenames for the UI, never contents.
The audit line records the pane and the session id, and nothing else.
Not covered
- Other panes in the same window are killed with it, as with “Kill window”. Their sessions are not expunged.
- omp, Gemini and other harnesses: there is no reliable pane-to-session mapping yet, so they refuse rather than guess.
- Codex threads the session spawned have their own ids and are left in place. Only their agent-history entries go, since those sit under the parent.
- Codex’s SQLite stores (
state_*.sqlite,thread_history_*.sqlite,logs_*.sqlite) keep their own copy of the thread’s items, and the confirmation says so for Codex. tmux-rc deliberately doesn’t write to them. They belong to Codex’s app-server, which outlives the client and has a schema that changes between versions, andcodex-native-context.mdalready rules out a second writable owner of that state. Removing those rows is Codex’s job, through its own interface. - The harnesses append to their shared logs without a lock. Expunges are serialized within the daemon, and the rewrite keeps the old file open. Anything appended to it while the new file is written, or as it is renamed into place, is read back and carried over. One gap remains: a write still in progress at that last read loses its remainder to the old file.
- Claude’s hook-driven agent-history indexer can race the deletion and rewrite the entry. Deleting the transcript first and the index entry last keeps that window small.
- Codex’s shared app-server, not the closed client, holds the thread. If it writes to the thread again after the deletion, a fragment can reappear.
- OpenTelemetry exports, provider-side retention, backups, and copies in shell scrollback or other tools are out of reach.