The cicoria guide
cicoria runs coding agents in pseudo-terminals and draws, around the one on screen, its sessions, files, changes and message queue. This guide covers every feature, and how cicoria learns what an agent is doing.
Install
curl -fsSL https://henomis.github.io/cicoria/install.sh | sh
The script downloads the latest release for your system (Linux and macOS, x86_64 and arm64), checks it against the checksums of the release and puts cicoria in ~/.local/bin. Running it again updates cicoria.
| Variable | What it changes |
|---|---|
| CICORIA_INSTALL_DIR | The directory the binary goes in, instead of ~/.local/bin |
| CICORIA_VERSION | The release to install, such as v0.0.4, instead of the latest |
You can also take the archive from the releases yourself and put the cicoria in it on your PATH, or build it with Go 1.26 or later:
go install github.com/henomis/cicoria@latest
Requirements
git, for the changes panel, the diffs and worktrees.- The agents you want to run:
claude(the default),copilot,codex, or any other command line. - A Unix-like system. cicoria is developed and tested on Linux; the macOS builds have not been tried on a Mac yet.
- For desktop notifications,
notify-send, orosascripton macOS. They are skipped where neither is there.
Windows
cicoria runs in WSL2, with the archive for Linux. Install it, git and the agents inside the distribution, and keep the project in its file system rather than under /mnt/c, where git is slow. o opens a file with its Windows program through explorer.exe. Notifications need notify-send and a notification service in the distribution.
Run it
cicoria # a workspace on the current directory cicoria ~/src/app # or on another one cicoria -config ./my.toml ~/src/app # with another settings file cicoria -version # which version this is
The first time, one session starts with the default agent. Everything you type goes to the agent on screen. To talk to cicoria instead, press the prefix, ctrl+a, then a command key. Clicks in a panel select it, clicks in the agent pane go to the agent, and the wheel scrolls what is under it.
The binary has a second command, cicoria hook, which the agents' hooks call. You never run it yourself; see How status is known.
Concepts
| Term | Meaning |
|---|---|
| Workspace | The directory cicoria was opened on. Its sessions are saved for it and come back the next time it is opened. |
| Session | One agent working in one directory, in a terminal of its own. One workspace can hold sessions from several projects. |
| Agent | The command line a session runs: Claude Code, Copilot CLI, Codex CLI, a shell, or anything you configure. |
| Turn | What an agent does between a message and the moment it waits again. Integrated agents report when a turn ends; queued messages go out then. |
| Base | The commit the session's work tree was on when the session was created. The changes of a session are counted from it. |
| Prefix | The key, ctrl+a by default, that makes the next key a cicoria command instead of input for the agent. |
cicoria is an interface for you, not an orchestrator. Every message an agent gets is one you write or queue, agents never contact each other on their own, and they stop when cicoria closes.
Keys
Press ctrl+a, then:
| Key | Command |
|---|---|
| c | New session: pick the agent, its directory and a name |
| 1 to 9, n, p | Go to a session by number, the next, the previous |
| a | Go to the next session that needs input |
| , r x | Rename the session, restart it once its agent has exited, close it |
| - | Split: show another session below, or close the split |
| tab | Go to the other pane of the split |
| w | Open another saved workspace in place of this one |
| s f g | Focus the sessions, files, changes panel |
| l | Focus the commits panel |
| u | Focus the queue panel |
| m | Queue a message for the agent |
| k | Send a message to another session, or ask it and get the answer here |
| d | Open the diff of the selected file |
| t | Send your review notes to the agent |
| h | Count the session's changes from the last commit |
| / | Find a file by name |
| b | Show or hide the side panels |
| [ ] | Show or hide the left, the right panels |
| space | Command palette: every command, by name |
| ? | Help. j k, space b, g G and the wheel scroll it; any other key closes it |
| q | Quit |
| ctrl+a | Send ctrl+a itself to the agent |
The prefix works in the full-screen file and diff view too. t, /, space and ? open over the file. a, d, r, x, q, , and m keep it on screen while they reply, ask, restart the session or, for d, show a diff in its place. The other commands go back to the workspace.
The same letter means the same thing everywhere: d opens a diff, x deletes or closes, o opens with the system's program, m queues, t sends notes, , renames, r restarts. Moving follows vim and less, so q and esc both go back.
Scrollback
The wheel and pgup/pgdn scroll back through what has left the agent's screen, and the pane's title says how many lines back you are. Typing returns to the end. An agent that reads the mouse itself, or takes the whole screen as an editor does, gets the wheel and the keys instead.
Sessions
ctrl+a c asks three things:
- The agent, when more than one is configured.
- The directory, starting from the one of the session on screen. tab completes, ctrl+w deletes a path component, ctrl+u clears;
~and relative paths work. - A name. A free one, such as
claude-2, is offered and enter accepts it.
The title of the agent pane names the session, its agent, its status, its directory and the git branch that directory is on, or the commit when it is on no branch. It follows a branch the agent switches to. In a narrow pane a long branch is cut in the middle and the directory keeps its end.
Closing a session whose agent still runs, or that has queued messages, asks first. Quitting asks when an integrated agent is still working.
When the agent exits
A session closes when its agent or shell exits. It is kept, marked ✕, when the agent exits within three seconds of starting, so that its error can be read; when it has queued messages; and when a file, the palette or a question is open at that moment. close_on_exit = false keeps every session. ctrl+a r restarts a kept one.
Worktrees
The files and changes panels read the directory, not the agent, so two sessions in one work tree see each other's changes, and their agents write over each other. When you create a session where another one already works, cicoria asks whether to give it a git worktree: y makes one, n or enter shares the directory and warns you, esc gives up.
| For a session fix-login on ~/src/app | |
|---|---|
| Directory | ~/src/app.worktrees/fix-login |
| Branch | cicoria/fix-login, from the commit the directory was on |
| Name | Anything but letters, digits, - and _ becomes - |
The session works there from then on, after restarts too. Closing it offers to remove the worktree. Its branch is always kept, and a worktree with uncommitted changes is kept too: the status bar says where it is. Merging the branch is up to you, or to an agent you ask.
A worktree holds only what git tracks: .env, node_modules and other ignored or untracked files are not copied. Claude Code asks whether to trust the new folder, as it does for any. A shell session is never asked and is no reason to offer the next session a worktree.
Split
ctrl+a - splits the agent pane: the session on screen stays above, and the one you pick shows below. ctrl+a tab or a click moves the keys between them; the pane that has them has the lit border, and the side panels show its project. Going to a session puts it in the pane that has the keys, or moves the keys when it is already in the other one.
ctrl+a - again, or closing one of the two, goes back to one pane. A terminal too short for two keeps one, and the split is not saved.
Status
| Mark | Meaning |
|---|---|
| ● | Working |
| ▲ | Needs input: the agent asks a question or a permission |
| ✓ | Finished its turn while you were looking elsewhere |
| ○ | Idle |
| ◌ | Starting |
| ✕ | The agent exited and the session was kept |
Each row shows for how long the session has had its status. A session that needs input and is not on screen is named in the status bar and raises a desktop notification when notify is on.
The window title, set with the xterm escape OSC 2, names the session on screen and its status, after a ▲ name for each other session that needs input: ▲ api · web · idle · cicoria. A tab or a window in the background tells you that way too. cicoria saves the title it found with CSI 22 t and puts it back with CSI 23 t when it quits; a terminal without that stack ignores both, and the shell's prompt usually sets its own title again.
An integrated agent that says it is working but shows no sign of life for 30 seconds reads working?, with its mark dimmed. That happens after you interrupt a turn, and when a Copilot CLI or Codex CLI turn fails, since neither runs a hook then.
Side panels
Sessions and files are on the left, changes and the queue on the right. In a panel j/k move, enter opens, tab goes to the next panel, esc gives the keys back to the agent. While a panel has the keys, the right of the status bar lists the others it takes.
| Panel | Keys |
|---|---|
| Sessions | enter go, , rename, r restart, x close, c new session |
| Files | enter open, h l close and open a folder, d diff, e editor, o open with the system, @ mention to the agent, i show or hide ignored files |
| Changes | enter or d diff, v the file, e editor, o open with the system, @ mention, s uncommitted or since the session started |
| Commits | enter narrow the changes to the commit, or show them all again, d its diff, @ mention the commit to the agent |
| Queue | See Queue |
In the file tree folders are blue, files an integrated agent edited in this run are highlighted, and changed files carry git's letter. Ignored files are listed in grey and an ignored folder is read when you open it. o uses xdg-open, open on macOS or explorer.exe in WSL, so an image goes to the image viewer and a folder to the file manager.
ctrl+a [ and ] hide one side, b both. A terminal too narrow for the agent and the panels shows the agent alone.
Review
The changes panel shows either what is not committed, or everything since the session's base, committed or not. In the second view its title counts the commits made since. The directory on screen is read again every two seconds, and again when the terminal gets the focus back.
- s in the changes panel switches between the two. The choice is not saved.
- ctrl+a h moves the base to the last commit, once you have looked at everything so far.
changes = "session"opens sessions on the second view.
Outside a git repository the panel says so and the file tree lists the directory as it is.
Commits
In either view, the commits panel under the changes lists the commits made since the base, newest first: short hash, subject and age. It is there only once there is one, and takes at most a quarter of the column. It reads git log base..HEAD, up to 200 commits, and is not a history browser: other branches and older commits are not listed.
- enter narrows the changes panel to the files of that commit, and its diffs to what the commit did against its first parent, switching to the second view if need be. The title then names the commit. enter on it again shows all the changes.
- d shows the whole commit as one diff on the full screen, as
git showdoes; tab and shift+tab jump from file to file. A note belongs to one file, so this view takes none: narrow to the commit with enter and open a file's diff to write one. - @ types the short hash at the agent's prompt, for a message such as
revert a1b2c3d
.
Switching the view or the session, or ctrl+a h, shows all the changes again, and so does a commit that is no longer since the base, after an amend or a reset: the status bar says so. Notes on a commit's diff are notes like any other: they quote the lines, so they read right even once the file has moved on.
The full-screen view
| Key | What it does |
|---|---|
| j k | A line down, up |
| space b | A page down, up |
| g G | First, last line |
| / n N | Search, next match, previous match |
| [ ] | Previous, next hunk of a diff, or heading of a document |
| tab shift+tab | Next, previous changed file |
| d | Switch between the file and its diff |
| w | Wrap long lines, or cut them again |
| h l ← → | Scroll the lines that are cut sideways |
| m | Switch a Markdown file between the document and its text |
| e | Open in $VISUAL or $EDITOR |
| o | Open with the system |
| @ | Mention the file to the agent |
| v c x t | Select lines, write a note, delete it, send the notes |
| q | Back to the workspace |
A Markdown file opened from the tree or by name is shown as a document: headings, lists, tables and coloured code. Paragraphs wrap to the screen; tables and code keep their shape and, when wider, are cut with a › and scroll sideways with h l. m shows its text at the same place, and the choice holds for the next files. Notes are written on the text; the document marks the passages that have one. A file opened from the changes panel, or next to its diff, is always text.
Notes
A note is a remark on one or more lines of a file or a diff. In the full-screen view, move to a line, or press v and move to cover several, then c. c on a noted line rewrites the note, x deletes it, and noted lines carry a * in the margin.
t, in the view or after the prefix, sends every note as one message, at once or through the queue. If the agent is asking a question when you send them at once, they are queued instead, so they are not taken for the answer.
Review notes: api/auth.go:42-43 > return nil > } return an error here, the caller ignores nil api/token.go:7 > +const ttl = 3600 make this a duration
Queue
ctrl+a m asks for a message; the line edits with the arrows, ctrl+w, ctrl+u and the like. Messages go to the agent one at a time, each when it ends a turn, and an idle agent gets the first at once. Nothing is sent while the agent waits for a permission, or after a turn that ended on an error.
| Key, in the queue panel | What it does |
|---|---|
| enter | Send the next message now |
| p | Pause and resume. Pause when a turn ends with a question you want to answer first |
| m e x | Add, edit, delete a message |
| J K | Move a message down, up |
Only Claude Code, Copilot CLI and Codex CLI report the end of a turn. Other agents keep a queue marked manual: each message goes when you press enter. Queues are saved and come back paused.
Asking a session
ctrl+a k picks another session, then one of:
| Choice | What happens |
|---|---|
| Send a message | It is queued there, as ctrl+a m would from that session |
| Ask, and get the answer here | The question is queued there, shown as from <session>. What its agent says at the end of the turn the question starts is queued back for you, with the question and, if you gave one, what to do with it |
I asked the session "api": > What does GET /orders return when the cart is empty? It answered: > A 200 with an empty `items` array, never a 404. Update the client to handle it.
The answer is the agent's last message in the Stop hook's payload, so only Claude Code and Codex CLI can be asked. Copilot CLI's hooks do not carry it, and cicoria does not read transcripts, so for Copilot the choice says not supported.
Only the turn the question starts counts. When another message was taken first, which is how an interrupted turn shows, or the turn ends on an error, or the agent says nothing, no answer goes back and the status bar says so. Claude Code names the message a turn is for, which settles this exactly. A question still being answered when cicoria closes is lost.
How status is known
cicoria never scrapes an integrated agent's screen for its status. It gives the agent process hooks, and the hooks report back:
- The agent runs a hookOn a prompt, a tool call, a notification or the end of a turn, it runs
cicoria hookwith a JSON payload on stdin. - The hook forwards itIt sends
{session, payload}to the socket named inCICORIA_SOCK, for the session inCICORIA_SESSION, and always exits 0. - The adapter reads itEach agent's adapter turns the event into an update: status, turn ended, file touched, conversation ID, answer.
- The workspace actsThe session's mark changes, a notification may fire, and on a turn's end the next queued message goes out.
The hook command never fails, so that a stopped or missing workspace never gets in the agent's way. The socket is $XDG_RUNTIME_DIR/cicoria-<pid>.sock, or in the temporary directory when that is unset, and is removed when cicoria quits.
For agents without hooks the status is guessed from the terminal: output means working, quiet means idle, and the bell means it needs input.
Agent integrations
| Agent | How the hooks are given | Hooks listened to |
|---|---|---|
| claude | --settings carrying the hooks, on top of your own settings | SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, Notification, Stop, StopFailure |
| copilot | --plugin-dir pointing at a plugin written under the user cache directory, in cicoria/copilot. Its hooks run "$CICORIA_BIN" hook <name>, since the payloads do not say which hook they are from | sessionStart, userPromptSubmitted, preToolUse, postToolUse, postToolUseFailure, notification, errorOccurred, agentStop |
| codex | One -c hooks.<Event>=… option per hook, running "$CICORIA_BIN" hook | SessionStart, UserPromptSubmit, PreToolUse, PermissionRequest, PostToolUse, Stop, Interrupt |
| Agent | Ready before the first message | Reports turn end | Can be asked | Highlights edited files |
|---|---|---|---|---|
| claude | Yes | Yes | Yes | Yes |
| copilot | No: idle once its screen is quiet | Yes | No | Yes |
| codex | No: idle once its screen is quiet | Yes, once its hooks are trusted | Yes | Yes |
| any other | Guessed | No: the queue is manual | No | No |
Codex CLI runs a hook only once you have trusted it, and remembers the answer by the hook's command. That is why the command names $CICORIA_BIN rather than the binary's path: it stays the same when cicoria moves or is updated.
A plain shell is always offered as a session type, and is not a session at work.
Conversations
Agent processes stop with cicoria: each is told its terminal is gone, as when a terminal window closes, and killed if it is still there two seconds later. The next time, each integrated agent continues its conversation:
| Agent | How it resumes |
|---|---|
| claude | Started with --session-id <id>, restarted with --resume when a transcript for that ID exists in Claude Code's config directory. After /clear the new conversation ID arrives through the hooks and is resumed instead |
| copilot | --session-id <id>, which both names a new session and continues one. Transcripts are under $COPILOT_HOME/session-state, by default ~/.copilot |
| codex | Codex names its conversations itself; the ID it reports is saved, and the session restarts with resume <id> when a transcript exists under $CODEX_HOME/sessions |
Transcripts are only looked for, to know whether a conversation can be resumed. They are never read.
Configuration
~/.config/cicoria/config.toml, or $XDG_CONFIG_HOME/cicoria/config.toml, or the file given with -config. Every key is optional; an unknown key or an invalid value is an error, not a silent default.
| Key | Default | What it does |
|---|---|---|
| prefix | "ctrl+a" | The key that starts a command |
| default_agent | "claude" | The agent of the first session of a new workspace |
| notify | true | Desktop notification when a session not on screen needs input |
| left_panel | true | Open with sessions and files shown |
| right_panel | true | Open with changes and the queue shown |
| changes | "uncommitted" | "session" opens the changes panel on everything since the session started |
| ignored_files | true | false leaves ignored files out of the file tree until i |
| close_on_exit | true | false keeps a session whose agent exited, to restart or close by hand |
| animations | true | A second of ASCII art when cicoria opens and quits and a session is created or closed |
prefix = "ctrl+b" right_panel = false # Any command line can be an agent. Its status is guessed. [agents.aider] command = ["aider"] # "claude", "copilot" and "codex" change how those are started, # and "shell" which shell is run. [agents.claude] command = ["claude", "--model", "opus"] # cicoria appends its own options, so a launcher that passes # the rest on ends with "--". [agents.codex] command = ["ollama", "launch", "codex", "--model", "gemma4:cloud", "--"]
Files and environment
| Path | What it holds |
|---|---|
| ~/.config/cicoria/config.toml | Settings, under $XDG_CONFIG_HOME when set |
| ~/.local/state/cicoria/<hash>/sessions.json | One workspace: its directory, and each session's name, agent, directory, base, worktree, conversation, notes and queue. Under $XDG_STATE_HOME when set. The hash is the first 8 bytes of the SHA-256 of the workspace directory |
| …/<hash>/lock | Held while a cicoria has the workspace open, so a second one asks before running the same agents twice |
| $XDG_RUNTIME_DIR/cicoria-<pid>.sock | The socket hook events arrive on |
| <cache>/cicoria/copilot | The plugin that gives Copilot CLI its hooks |
| <dir>.worktrees/<session> | Worktrees cicoria creates, on branches cicoria/<session> |
A session belongs to the workspace it was created in, wherever it works: one created from ~/src/app to work in ~/src/lib comes back with ~/src/app. ctrl+a w lists the other saved workspaces, which of their sessions work in this directory, and whether another cicoria has them open. Opened on a directory with nothing saved, cicoria says in the status bar where other sessions are.
Environment given to agents
| Variable | Value |
|---|---|
| CICORIA_SOCK | The socket path of the running workspace |
| CICORIA_SESSION | The session the process belongs to |
| CICORIA_BIN | The cicoria binary, for the hooks of Copilot CLI and Codex CLI |
Troubleshooting
| What you see | Why, and what to do |
|---|---|
| A Codex session never changes status | Codex asks to trust the hooks cicoria gives it the first time. Pick Trust all and continue; until then it works but reports nothing |
working? | The agent said it was working and then went quiet for 30 seconds: an interrupted turn, or a failed Copilot or Codex turn. Send a message or check the pane |
| Queued messages wait | The queue is paused (it is after a restart, press p), the agent waits for a permission, the last turn ended on an error, or the agent does not report turn ends and the queue is manual |
| A session closed at once | It did not if the agent exited within three seconds: it is kept with ✕ so the error can be read. Set close_on_exit = false to keep every session |
| No notifications | notify-send (or osascript on macOS) is missing, or, in WSL, there is no notification service |
| Slow changes panel in WSL | The project is under /mnt/c. Move it into the distribution's file system |
| Tests misbehave inside a session | Its agent has CICORIA_SOCK and CICORIA_SESSION set. Run them with env -u CICORIA_SOCK -u CICORIA_SESSION, as the Makefile does |
Develop
git clone https://github.com/henomis/cicoria cd cicoria make build # build ./cicoria make check # vet, tests and gofmt: run it before a commit make lint # golangci-lint, as the CI runs it make race # tests with the race detector make test-real # the test that drives the real Claude Code; needs a trusted folder make help # every target
cicoria is Go with Bubble Tea. Dependencies point one way, from main.go to internal/app to the rest:
| Package | Role |
|---|---|
| internal/app | The one model: sessions, focus, modes, keys, commands, drawing, the full-screen view and the queue |
| internal/agent | One adapter per agent: its command line, its hooks, how to mention a file, how an event becomes an update |
| internal/session | An agent and its terminal: status, queue, notes, base; and the saved workspace |
| internal/term | A command in a PTY, with an emulated screen so the agent's interface can be drawn in a pane |
| internal/ipc | The unix socket hook events arrive on |
| internal/git | The work tree, read through the git command line |
| internal/ui | Widgets: pager, tree, palette, input, syntax highlighting. It knows nothing of agents or git |
End-to-end tests in e2e_test.go build the binary and drive it in a PTY against a scratch git project, with shell scripts standing in for the agents and emitting real hook events. Bugs and ideas go to the issue tracker.