- Rust 83.8%
- JavaScript 8.2%
- CSS 6.7%
- HTML 1.3%
These windows ran `sh -c 'claude ...; exec $SHELL'`. That sh has no job control, so Ctrl-Z stopped Claude with no shell to run `fg` in, and the window was stuck. Now the window starts the user's shell and types the claude command into it once it waits at its prompt, as restored devenv shells already did: Ctrl-Z suspends Claude to that shell, `fg` brings it back, and the command is in the shell's history. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|---|---|---|
| docs | ||
| src | ||
| web | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| LICENSE | ||
| README.md | ||
sirkus
A terminal multiplexer in the spirit of GNU Screen, built for running many Claude Code sessions at once. Your windows are listed in a sidebar on the left, and each Claude session shows its title, its state, and its project directory and git branch:
- ● red: needs input (a permission prompt or a question). Dismissing it with Esc makes the session idle again.
- ⠹ yellow: working. That includes a session whose turn is over while commands it started in the background still run: it is waiting for their results. Once they've ended, Claude normally picks the results up, and the session finishes when that turn does. If Claude hasn't picked them up within 30 seconds, it counts as finished.
- ● green: finished. A session that finished while you were on another window turns idle once you've been on its window for 2 seconds (flicking past keeps it marked). One that finishes in the window you're on stays marked until you type into it.
- ○ dim: idle (the LCARS and ocean buttons leave idle sessions unlabelled)
Ctrl-a Tab jumps to the next session that needs you. Sessions that are blocked come
first, then finished ones you haven't seen, oldest first. Plain shells, progress bars,
docker compose and other coding agents show their state too (see
Other programs).
Install
cargo install --path .
sirkus install-hooks # adds sirkus hooks to ~/.claude/settings.json (a backup is kept)
The hooks report Claude's state to sirkus. Outside sirkus they do nothing.
sirkus uninstall-hooks removes them again.
Use
sirkus # start a session (named main, main2, …)
sirkus -S work # start a session called "work"
sirkus -ls # list sessions
sirkus -r [name] # reattach (detaching other clients)
sirkus -x [name] # attach alongside other clients
sirkus -R [name] # reattach, or start one if none is running
sirkus web [name] # use a session from a web browser (see below)
In a sirkus window, start claude as usual.
Several terminals can be attached to a session at once (sirkus -x). As in screen,
each one has a view of its own: its own window, its own size, its own overview and
prefix key. Each window has a size of its own too. A window that only one place shows
takes that place's size. A window shown in several places keeps its size: it is cut
off where it doesn't fit and leaves room where it's smaller. Ctrl-a F fits the
window to the terminal you press it in. The windows, the sidebar's width, the theme
and the draft belong to the session, so every terminal shares them.
Keys
All keys start with the prefix Ctrl-a:
Windows are numbered by their place in the list, so numbers change as windows come
and go. A new window (c) goes at the end, a branch (B) right below the window it
branches, and the windows after it move down one.
| Key | Action |
|---|---|
Tab |
jump to next Claude session needing input |
c |
new window |
n / p |
next / previous window |
1–9, 0 |
select window (0 = window 10) |
Ctrl-a |
previous window |
< / > |
move the window up / down the list |
A |
rename window |
k |
kill window (in a split, the terminal with the keyboard) |
d |
detach |
o / e |
split the terminal: a shell below / beside it |
| arrows | the terminal that way in a split window; otherwise previous / next window |
w |
write text to paste into the window (an editor) |
O / " |
overview of all windows (fullscreen) |
b |
toggle sidebar |
B |
branch the Claude session into a new window |
r |
reopen a recently closed Claude session |
T |
reopen the last closed Claude session |
N |
toggle desktop notifications |
t |
next theme (saved as the default) |
F |
fit the window to this terminal |
[ |
scroll mode |
a |
send a literal Ctrl-a |
? |
help |
\ |
quit (Enter or y confirms) |
Mouse
- The wheel scrolls back through history, like in Claude Code. Typing jumps back to the bottom. Programs that use the mouse themselves, Claude Code included, get the wheel events.
- Click a sidebar entry to switch to that window. Drag an entry up or down to move the window there in the list; the windows are renumbered to match.
- Double-click the empty part of the sidebar, below the entries, to open a new window.
- Drag the divider between the sidebar and the terminal to resize the sidebar. The width is saved in the config file.
- Drag to select text; releasing the button copies it to both the clipboard and the primary selection. Double-click selects a word, triple-click a line. Hold Shift to select inside programs that capture the mouse.
- Middle-click pastes the primary selection (whatever was last selected, here or in another program) into the terminal under the pointer. Programs that capture the mouse get the click instead; Claude Code pastes the primary selection itself.
- Copies go out both as OSC 52, which works over ssh, and through
wl-copy(Wayland) orxclip/xsel(X11) when the server runs on a desktop. That covers terminals that ignore OSC 52, such as GNOME's. Without those tools, middle-click pastes the last text copied in sirkus.
Branching a Claude session
Ctrl-a B branches the Claude session in the current window, like Claude Code's
/branch: a new window opens in the session's project directory and runs
claude --resume <id> --fork-session, which continues a copy of the conversation under a
new session ID. The branch opens right below the window it came from. The original
session carries on untouched. When the branched Claude
exits, the window drops to a shell. The branch includes the conversation as Claude Code
has saved it, so wait for a turn to finish if you want all of it.
Reopening the last Claude session
Ctrl-a T resumes the Claude session closed last with claude --resume, in a new window
in its project directory, whether Claude exited, its window was killed, or /clear
replaced it. A session whose window was killed goes back to that window's place in the
list, and gets its name back. Press it again for the one closed before that. With no session
closed since sirkus started, it takes the newest one from the recent list (below).
Recently closed Claude sessions
Ctrl-a r lists the Claude sessions that were closed, most recent first, with the
title, directory, branch and when each was closed. A session counts as closed when Claude
exits, its window is killed, sirkus quits, or /clear replaces it with a new one.
Sessions that never got a prompt aren't listed. Choose one with the arrow keys (or j/k)
and press Enter to resume it with claude --resume in a new window, in its project
directory; x removes it from the list and Esc closes the menu. The list is kept in
~/.local/state/sirkus/recent.jsonl and shared by all sirkus sessions; a session that
runs again drops off it.
Split windows
Ctrl-a o splits the terminal with the keyboard, opening a shell below it in the same
directory; Ctrl-a e opens one beside it. Each press splits again, so a window can hold
any number of terminals, stacked and side by side. A new terminal beside takes half the
room of the one it split; a new one below takes 40% of the window's height, at most 24
rows (and at most half the terminal it split). The new terminal gets the keyboard. Drag a
dividing line to move it. Ctrl-a and an arrow moves the keyboard to the terminal that
way, as does a click; the lines around the terminal with the keyboard are brighter, with
arrows pointing at it. In a window that isn't split, Ctrl-a and an arrow switches
windows as before.
All the terminals belong to the same window and its sidebar entry. When you start
claude in one of the others, it becomes a window of its own, with its own entry, and
the rest share its room. When a shell exits, its neighbours get its room; when the
window's own terminal exits, the next one takes the window over. Restoring a session
brings its splits back, with their sizes and the terminal that had the keyboard.
Writing text
Ctrl-a w opens an editor over most of the screen, for writing a longer text, such as a
prompt of several paragraphs, before sending it. Its buttons:
- Submit (or Alt-Enter) pastes the text into the window, or into the terminal with the keyboard in a split, and clears it. It goes in as a paste, without pressing Enter, so Claude Code takes all the lines as one message; press Enter in the window to send it.
- Submit & keep pastes it the same way, but keeps the text for next time.
- Clear empties it.
- Close (or Esc, or
Ctrl-a wagain) closes the editor and keeps the text.
Tab moves between the text and the buttons. In the text, the arrows, Home/End (with
Ctrl: the start and end of the text), PgUp/PgDn, Ctrl or Alt with the arrows (a word at
a time), Backspace/Delete, Ctrl-W (delete a word), Ctrl-U and Ctrl-K (delete to
the start or end of the line) and pasting work, and clicking places the cursor. The
session has one text, whichever window you send it to; the web page edits the same
text, and quitting with Ctrl-a \ keeps it for sirkus -S.
Overview
Ctrl-a O replaces the terminal with a fullscreen list of all windows. Each window gets
a large state badge, its directory, git branch
and Claude session, and what it is doing (the current tool, the first prompt, or for
plain shells the last line on screen). Keys:
- ↑/↓ or
j/k: move the selection - Enter or a number: open a window
</>: move the selected window up / down the listTab: select the next session needing inputB: branch the selected window's Claude sessionx: kill the selected windowq/Esc: go back to the terminal
Clicking a card also opens it.
Themes
Add -t <name> (or --theme <name>) when starting or attaching to change the session's
look, for example sirkus -t lcars or sirkus -r work -t matrix. The session keeps its
theme until an attach picks another; theme = "..." in the config sets the default.
Ctrl-a t cycles through the themes in a running session and saves the one you land on
as the default. All
themes except classic need a terminal with 24-bit colour.
| Theme | Look |
|---|---|
classic |
the default |
lcars |
Star Trek LCARS: elbow-shaped bars and a column of coloured buttons, one per window, in orange and pink |
ocean |
the LCARS layout in blues and steel, with titles as written and without window numbers (they show while Ctrl-a waits for its key), roomier buttons when there is space, the directory line in a darker shade and a cyan bar marking the active window (lcars2 still works) |
qbasic |
the QBasic editor: a menu bar, blue windows, a scroll bar and a status bar |
c64 |
a Commodore 64 screen: light blue border, start-up banner and READY. |
matrix |
green phosphor monochrome, with digital rain in the empty part of the sidebar that falls faster the more your terminals print |
amber |
an amber monochrome CRT |
synthwave |
neon pink and cyan on deep purple, with a sunset over a grid in the empty part of the sidebar; the grid rolls toward you while Claude works |
gameboy |
four shades of green on a light screen, the window list in a pixel box with a ▶ on the active window |
ocean |
matrix |
|---|---|
![]() |
![]() |
synthwave |
lcars |
![]() |
![]() |
The themes also recolour what runs in the windows, so Claude Code fits the look: its
24-bit colours (the orange, diff backgrounds, syntax highlighting) and other programs'
256 colours take the theme's own hues at about the same brightness: the ocean blues,
teal and coral, the LCARS oranges, golds and blues, the synthwave neons. In c64 and
qbasic they snap to the machine's 16 colours (with QBasic backgrounds from the 8 dark
ones), keeping a red or green diff background red or green. Text is kept readable
against its background. classic leaves colours alone; the monochrome themes and
gameboy reduce everything to their own shades anyway.
In the LCARS themes, a button's colour and label show the window's state. In qbasic,
click a menu entry (New, Overview, Notify, Sidebar, Help) to use it, and click the scroll
bar's arrows or track to scroll. In c64, READY. turns into an error when sessions
need input.
Notifications
Ctrl-a N turns desktop notifications off and on. The choice is saved in the config
file and applies to sessions started later.
When a session that isn't on screen needs input or finishes, sirkus sends a desktop
notification (notify-send, falling back to OSC 777) and rings the bell.
So does a shell command that ran for 10 seconds or more and ended off screen.
Other programs
Titles
Programs that set the terminal title (vim and neovim with set title, ssh sessions,
htop, …) show it in the sidebar while they run. The shell's own title is left out:
plain shells keep their name.
Shell commands
With shell integration, the shell tells sirkus when a command starts and ends (OSC 133). Add this to the end of your shell's startup file (after a prompt framework like starship, which sets up the prompt itself):
eval "$(sirkus shell-init bash)" # ~/.bashrc
eval "$(sirkus shell-init zsh)" # ~/.zshrc
sirkus shell-init fish | source # ~/.config/fish/config.fish
Outside sirkus it prints nothing. Fish 4 reports commands without it.
The sidebar then shows:
- ⠹ cyan: a command runs and prints (▸ while it's quiet, or a full-screen program)
- ✔ green / ✘ red: the command ended, or failed (the overview and the LCARS buttons show its exit code), while you weren't looking. Like a finished Claude session, the mark goes once you've been on the window for 2 seconds, or type into it.
A command that ran for 10 seconds or more notifies when it ends off screen, and
Ctrl-a Tab visits failed commands, then successful ones, after the Claude sessions.
Progress
Programs that report progress with OSC 9;4 (as cargo, winget, and some build tools do) get a progress bar in place of the directory, with the percentage.
docker compose
While docker compose up (or watch) runs in the foreground of a window, sirkus asks
docker about its services every few seconds and lists them in the sidebar, coloured by
state: green running, yellow starting or not healthy yet, red exited with an error or
unhealthy, grey stopped. The window is named after the project's directory, and its
mark shows the worst of them (▲). The same options (-f, -p, --profile, …) are
used to ask, so it finds the right project.
Other coding agents
sirkus notify reports another agent's state from that agent's hooks, the way
sirkus install-hooks sets it up for Claude Code. The window then shows the agent like
a Claude session (needing input, working, done, idle), notifies, and is on
Ctrl-a Tab's list, until the agent exits.
sirkus notify --state input|working|done|idle|end [--agent NAME] [--message TEXT]
--agent names the agent (the program's name otherwise); --message says what it's
doing or asking. done is the default. Outside sirkus it does nothing.
Codex CLI (~/.codex/config.toml) calls notify when a turn is complete; sirkus reads
the event it passes and shows the last message:
notify = ["sirkus", "notify", "--agent", "codex"]
Gemini CLI hooks (~/.gemini/settings.json):
{
"hooks": {
"BeforeAgent": [{ "hooks": [{ "name": "sirkus", "type": "command", "command": "sirkus notify --agent gemini --state working" }] }],
"AfterAgent": [{ "hooks": [{ "name": "sirkus", "type": "command", "command": "sirkus notify --agent gemini --state done" }] }],
"Notification": [{ "hooks": [{ "name": "sirkus", "type": "command", "command": "sirkus notify --agent gemini --state input" }] }]
}
}
Aider runs a command when it's waiting for you:
aider --notifications --notifications-command "sirkus notify --agent aider --state done"
Other agents with hooks or a notification command (OpenCode plugins, GitHub Copilot CLI
and Cursor's agent hooks) work the same way: call sirkus notify with the state that
fits each event.
Web
sirkus web # the running session, or a new one if none runs
sirkus web work # session "work", started if it isn't running
sirkus web work --port 8080 # another port (the default is 7380)
sirkus web prints an address such as http://127.0.0.1:7380/?token=…; open it in a
browser. The page has the window list on the left, with the same states as the
terminal sidebar, and the window's terminal on the right. Terminal clients and any
number of browser tabs can use the session at once. Each browser tab picks its own
window, like each terminal does, and windows are sized the same way (see Use):
Ctrl-a F fits a window to the terminal or tab you press it in. Browser tabs in the
background don't count. Finished marks clear in a browser tab as they do in the
terminal. sirkus web serves until you press
Ctrl-C or the session ends; the session keeps running when it stops.
The page takes the prefix keys c, n/p, 1–9, </>, Tab, k, A, w, o, e,
the arrows, O, B, b, t (the next colours in the menu below) and F, and Ctrl-a a
sends a literal Ctrl-a. It splits windows the same way, with draggable dividers. The
window's other actions (writing text, splitting below or beside, rename, branch, kill, a
new window, the overview) have buttons too. The editor (w, or Write) takes Ctrl-Enter as
well as Alt-Enter to submit. Copy with Ctrl-Shift-C (or Ctrl-C while text is
selected) and paste with Ctrl-Shift-V. Choose the colours at the bottom of the window
list: light or dark (following the system by default), or one of the sirkus themes,
whose colours the programs in the windows then take, as they do in the terminal. The
synthwave page goes further than the terminal: neon glow, a sun setting over a grid
that rolls faster while Claude works, and a glowing screen. Drag the edge of the window
list to resize it (double-click the edge for the default width); each browser keeps its
own width. Drag an entry in the window list to move the window up or down the list; on
a touch screen, hold the entry for a moment, then drag. The overview shows each window as a card with its full title (up to three
lines), its directory, branch, program and session; the Terminals switch at the top
right (or t) adds each window's last lines, and the browser remembers it. The address names the view (#window=3, or #overview), so reloading the
page keeps it, and you can bookmark a window. On a
phone the window list slides in from ☰, and a row of keys (Esc, Tab, Ctrl, the prefix
and the arrows) sits below the terminal.
Security. By default the server listens on 127.0.0.1 only, so only this computer
can reach it, and the page needs the random token in the address, which changes each
time sirkus web starts. Anyone with the address can type into your terminals. To use
the session from another device, prefer an SSH tunnel
(ssh -L 7380:127.0.0.1:7380 host, then open the address on the device). --bind 0.0.0.0 listens on every network interface instead; the connection is not encrypted,
so only do that on a network you trust.
Configuration
~/.config/sirkus/config.toml (all keys optional). sirkus updates notifications and
sidebar_width itself when you change them with Ctrl-a N or by dragging the sidebar,
leaving the rest of the file, comments included, as it is.
prefix = "C-a" # prefix key
sidebar_width = 32
scrollback = 10000 # lines per window
shell = "/bin/zsh" # default: $SHELL
notifications = true # desktop notifications at startup (toggle with Ctrl-a N)
theme = "classic" # or lcars, ocean, qbasic, c64, matrix, amber, synthwave, gameboy
bell = true
Persistence
sirkus records each session's windows in ~/.local/state/sirkus/. Quitting with
Ctrl-a \ keeps that record, and so does the server dying (a reboot, for example).
Starting the session again with sirkus -S <name> (or sirkus -r <name>) offers to
restore its windows in their directories, and Claude sessions are resumed with
claude --resume, with the options Claude was started with (--model,
--permission-mode and so on; not the prompt, or which session to resume). A Claude
session that never got a prompt has no conversation to resume, so a new one starts there
instead, with the same options. A session that had finished without you seeing it comes
back marked done (●). sirkus starts your shell and types the claude command into
it, as you would, so Ctrl-Z suspends Claude to that shell and fg brings it back; the
same goes for branched and reopened sessions. Split windows come back split, each terminal in its directory,
and the window that was showing shows again, with the same terminal in it focused.
sirkus -ls lists the sessions that can be restored. When every window in a session
exits on its own, the record is removed.
Quitting with Ctrl-a \ also saves what each shell had on its screen and in its
scrollback, colours included, and a restored shell starts below it. A full-screen
program's screen (an editor, less) isn't saved, and Claude windows get their
conversation back from claude --resume instead. If the server dies without quitting,
shells come back empty.
A window that was in a devenv shell starts devenv shell again,
in the directory it was started in and with the options it had. Once devenv's shell is
up, sirkus types into it what ran there: going back to the directory the window was in,
and claude --resume for a Claude session that ran in devenv. Leaving devenv's shell
leaves you in your own, as before. A shell started with --no-reload doesn't show its
other options, so it comes back with the project's defaults.
To move running sessions to a new version of sirkus: install it, quit each session with
Ctrl-a \, then start it again with sirkus -S <name>.
License
MIT. See LICENSE.




