Skip to content

Scripts and agents

Besides opening the window, the cornercase command drives the server that is already running. A script can open a tab and run the tests in it, a git hook can leave a note, and a coding agent in one tab can start others in their own worktrees, wait until they finish or need an answer, read what they wrote and send them the next step, or follow all of them at once as things happen.

Terminal window
pane=$(cornercase new-tab --name tests -- cargo test)
cornercase wait --pane "$pane" --until shell --timeout 900
cornercase read --pane "$pane" --lines 40

These commands never start a server, except start-server. Without one, they say no cornercase server is running and exit with status 1.

A server waits for its first window before it restores your session, and until then commands that open terminals refuse. A script, a cron job or another UI that never opens a window starts the server with start-server first:

Terminal window
cornercase start-server
pane=$(cornercase start claude --prompt "fix the failing test" --worktree fix/test)

It starts the server if none runs, restores the saved session once (or opens the folder you ran it from), and returns once its terminals exist; run again, it does nothing and says the server is already running. Until a window attaches, terminals are 160×48, or --cols and --rows. A window that attaches later finds the same tabs, with the same ids, takes over the size and gives the terminals its colours, unless you gave them with --theme FILE. See Command line for its output.

cornercase status lists every project, workspace, tab and pane with its id:

project 1 web-shop ~/code/web-shop (shown)
workspace 2 main ~/code/web-shop branch main (shown)
tab 3 claude working (shown)
pane 4 claude working Opus 5.5 · 23% ~/code/web-shop (shown)
tab 6 zsh
pane 5 zsh ~/code/web-shop (you)
tab 14 claude working
pane 13 claude working Opus 5.5 · 4% ~/code/web-shop (parent 5)
workspace 7 fix/login ~/.cornercase/worktrees/web-shop/fix-login branch fix/login worktree
group 9 personal collapsed
project 10 blog ~/code/blog
  • Ids come from one counter and last as long as the server runs: after a restart, the same tabs get new ids. Take them from status, or from what a command printed.
  • Every pane’s shell has CORNERCASE_PANE set to its id. Inside a pane, commands act on that pane, its tab, workspace and project unless you name another one; (you) marks it in status. Run from anywhere else, they act on what the window shows, marked (shown).
  • A pane opened by start, new-tab or split run inside another pane remembers that pane as its parent, marked (parent 5) in status (and (collapsed) once its children are collapsed in the agents section): an agent that starts others finds them as its children, and the agents section lists them under it. A flow’s members have its orchestrator as parent, and (role coder) in status says which role each plays. Run from anywhere else, the new pane has none. Once the parent pane closes, the mark goes.
  • Where a command takes --workspace ID, --worktree BRANCH names the workspace on that branch instead, looked up in the same project: the one the command runs in, else the shown one. cornercase start claude --worktree fix/login made it, so cornercase close --worktree fix/login --remove-worktree can take it down without reading its id from status. A branch that no workspace there is on, or that several are on (a workspace without a worktree of its own shares its project’s branch), exits with status 2 and lists the workspaces to pick from.
  • The pane is what send, keys, read and wait work on. Where they take --tab ID, it stands for the tab’s agent pane, or its active pane when no agent runs in it.
  • Nothing changes what the window shows except focus, go and the --focus option. Whoever is looking at the window keeps seeing what they were looking at while a script opens tabs next to them. With --view ID they move that view instead, and the window stays where it is.
  • With --view ID, the defaults are what that view shows: its project, workspace, tab and that tab’s active pane, even inside a pane.
Command What it does
status Lists groups, projects, workspaces, tabs and panes with their ids, or one line per pane with --panes; --follow keeps printing the whole state as it changes.
open PATH Opens a folder as a project, or finds the open one. Prints the project’s id.
new-workspace NAME Adds a workspace to a project, or a git worktree with --worktree. Prints its id.
new-tab Opens a tab, optionally typing a command into it. Prints the pane’s id.
split Splits a pane right, or down, optionally typing a command. Prints the new pane’s id.
start [AGENT] Starts a coding agent in a new tab and gives it a prompt, or with --issue the issue’s. Prints the pane’s id.
issue sources, issue list, issue read KEY, issue people SOURCE Read the issue trackers through the server: which are connected, their lists with the Issues dialog’s filters, an issue with its comments, and whom the filters can name.
flow list, flow run NAME, flow export NAME, flow import SOURCE, flow delete NAME, flow hide NAME, flow restore NAME Lists the flows, starts one (a team of agents in a worktree, run by an orchestrator agent), prints one as text to share, adds one someone shared, deletes one of yours or the project’s, or deletes a built-in flow and brings it back.
flow runs, flow messages, flow files Where each running flow stands, what its roles sent each other, and the files of its folder: see Follow a flow run.
send [TEXT] Pastes text into a pane, and presses Enter with --enter.
keys KEY… Presses keys in a pane.
search TEXT Finds the panes where some text was said: in the agents’ prompts and replies and in every pane’s history.
read Prints a pane’s screen, its last lines, or the last messages of one or several agents.
wait Waits until an agent stops working, a program ends, a line shows up or the output stops, in one pane or in the first or every one of several.
events Prints what happens as it happens: agents changing state, programs ending, panes, tabs, workspaces and projects opening and closing, with --host programs’ clipboard writes and bells, and with --notices the notices about agents.
surface Streams a tab’s terminals as cells with their panes and dividers, for a program that draws them, optionally as patches.
changes Lists, reads and marks the changes of a workspace, as the changes panel shows them, for a program that draws it.
view Opens a view of your own: its own tab and size, for a program that draws cornercase itself.
view-set Changes a view: its size, whether it is active, or takes back the tab it shows.
input --pane ID EVENT… Types keys with modifiers, text, pastes and mouse events into a pane, for a program that draws it.
scroll, selection, ratio, resize, link Scroll a pane’s view, select or copy text in it, move a divider of a tab’s split, lay a surface stream out at another size, or say which file a click on a cell opens, all for a program that draws the terminals.
redraw Sends a surface stream a complete line now, for a program that applies patches.
close Closes a pane, tab, workspace or project; --remove-worktree --check says first what removing a worktree would lose.
group, move, collapse, expand, move-pane Arrange the sidebar and the panes: create, style and delete groups, move rows, fold them, swap or move panes of a tab.
rename NAME Renames a tab, workspace, project or group.
focus Shows a pane, tab, workspace or project in the window.
go TOKEN Goes to the agent a notice is about, as a click on its desktop notification does, in the window or with --view in a view.
notify TEXT Shows a message in the window, with a desktop notification.
todo Adds, lists, edits, checks off and back, reorders and removes items of the TODO list (with undo), and sends one to an agent as send --enter would.
config Reads every setting, connection and project’s commands, and changes them with the Settings dialog’s own checks: settings for a program that draws its own preferences.
away Has agents message you on Telegram or Discord; --off stops it.
usage Prints your Claude Code and Codex plan limits as the Usage dialog’s Limits tab shows them; --cached prints the last answer without asking the agents again.
plugin actions, plugin run PLUGIN ACTION List the actions your plugins add to the menus, and run one on a project, workspace, tab or pane as its menu entry does, waiting for it to end. See Plugins.
skill Prints the instructions that teach coding agents these commands.
contract Says what the running server supports: its version, the contract it speaks, its boot and its operations and streams.

cornercase --help and cornercase COMMAND --help describe every option, with examples.

Terminal window
cornercase status # the tree above
cornercase status --json # the same, as JSON
cornercase status --panes # one line per pane, for scripts
cornercase status --panes --json # the same, as JSON
cornercase status --follow # the whole state as JSON, again after each change
cornercase status --app # the TODO list, queue, away, update and keep awake as JSON
cornercase status --app --follow # the same, again after each change

Each pane shows its program and folder. Recognised agents that emit OSC 7501 show their reported status, including when no session reader exists. A pane running Claude Code, Codex, Gemini CLI, opencode, Grok Build, Cursor CLI or GitHub Copilot CLI also shows what the agent is doing (working, waiting for an answer, pending review, done while you were elsewhere, failed, idle; without OSC 7501, opencode is never waiting), its model and how full its context is, as the tab icons do. working (background shell) is a Claude Code agent whose turn is over while a shell it started in the background (a test watcher, a dev server) still runs: its tab keeps showing it as working, since Claude wakes up when that shell ends, but a script can tell it apart and stop waiting with --until turn-over. working (2 subagents) is an agent whose own turn is over while subagents it started (Claude Code, GitHub Copilot CLI) still run: it goes on when they report back, so it stays working, and the default, --until idle and --until turn-over waits keep waiting for them. (dialog open) is a Claude Code agent showing something else where its input box should be (a picker, the transcript view, a background shell’s details, its ! shell mode), which send refuses. (survey open) is Claude Code’s feedback survey above its input box, which send refuses too. A workspace shows its branch, worktree when it has one of its own, the commits it is behind, and its pull request with the state of its checks, such as pull request #485 open, checks failing, or pull request #486 open, conflicts with main, checks passing when it has merge conflicts.

A script that coordinates agents mostly needs the panes: which agent runs where, what it is doing, in which worktree. status --panes lists them flat, so nothing has to walk the tree: one line per pane, in the order of the sidebar, under a header line that names the columns, separated by tabs.

pane tab workspace project program agent status background_shell dialog at_prompt model context caller shown active tab_name workspace_name branch worktree project_name path survey resuming parent subagents role held_sends draft
4 3 2 1 claude claude working false false false Opus 5.5 23 false true true claude main main false web-shop /home/ana/code/web-shop false false - 2 - 1 true
5 6 2 1 zsh - - false false true - - true false true zsh main main false web-shop /home/ana/code/web-shop false false - 0 - 0 false
12 11 7 1 codex codex waiting false false false gpt-5.4 41 false false true codex fix/login fix/login true web-shop /home/ana/.cornercase/worktrees/web-shop/fix-login false false 5 0 - 0 false
Column What it holds
pane, tab, workspace, project The ids of the pane and of the tab, workspace and project it is in.
program The program in the foreground, as the tab names it.
agent claude, codex, gemini or opencode when one runs in the pane.
status What the agent is doing: working, waiting, pending, done, failed or idle.
background_shell, dialog, at_prompt As in the JSON: the agent’s turn is over but a background shell still runs; a Claude Code agent shows a dialog instead of its input box; the shell sits at its prompt with nothing running.
model, context The agent’s model and how full its context is, in percent.
caller true for the pane running the command.
shown true for the pane the window shows.
active true for its tab’s active pane.
tab_name, workspace_name, branch, worktree, project_name The tab’s and workspace’s names, the workspace’s branch and whether it has a worktree of its own, and the project’s name.
path The folder of the program in the foreground, in full.
survey As in the JSON: a Claude Code agent shows its feedback survey above its input box, which send refuses until keys --pane … 0 dismisses it.
resuming As in the JSON: the pane’s agent is coming back in its conversation after a restart, and takes no input until it is back.
parent The id of the pane this one was started from, while that pane is open: start, new-tab or split run inside it, or a flow’s orchestrator for its members.
subagents How many subagents the pane’s agent started that still run (Claude Code, GitHub Copilot CLI), 0 otherwise. While it is above 0, subagents keep an otherwise idle (or background-shell) agent’s status at working, even when its own turn is over; a question (waiting) and an OSC 7501 report still take precedence, so scripts must still handle the reported status.
role The role the pane plays in a flow run (orchestrator, coder…) while the run lives, - otherwise.
held_sends, draft As in the JSON: how many messages send is holding for this pane, and whether someone is typing a draft in it.
  • The values are the ones --panes --json gives: true or false, numbers without a unit, full paths. A - stands where there is no value (no agent, no branch, a field the server is too old to know), so a column never goes missing and read keeps them in place. A tab or a line break in a name shows as a space.
  • The columns keep their order, and new ones only ever go at the end, so $7 stays the status.
  • --panes --json prints {"panes": [...]}, one object per pane with the columns as keys, in the same order, and null where the text shows -.
Terminal window
# the agents that wait for an answer
cornercase status --panes | awk -F'\t' '$7 == "waiting" { print $1 }'
# each pane with its agent and status, skipping the header
cornercase status --panes | tail -n +2 | while IFS=$'\t' read -r pane tab workspace project program agent status rest; do
echo "$pane $agent $status"
done
# the agents in the worktree of fix/login
cornercase status --panes --json | jq '.panes[] | select(.branch == "fix/login" and .agent != null) | .pane'

Another UI for cornercase, or a script that draws the sidebar somewhere else, needs the whole session and every change to it: names, order, groups, folds and how each tab is split. status --follow prints it as one JSON line, first as it is now, then again shortly after each time something in it changes:

Terminal window
cornercase status --follow | jq -c '[.revision, (.projects | length)]'
cornercase status --follow | while read -r state; do echo "$state" > /tmp/cornercase-state.json; done
  • Each line is what status --json prints, plus revision, a number that grows with every line, except caller, which is always null: find your own pane with CORNERCASE_PANE or status --json instead. It is always JSON, with --json or without, and it can’t be combined with --panes.
  • Each line is the whole state and replaces the one before: there are no differences to apply, so a reader can skip lines and lose nothing but the steps in between.
  • revision only compares within one boot, the start of the server it came from. It ends, with status 0, when the server stops or restarts; to go on, run it again: its first line is the state at that moment, so nothing is missed. The same goes for a connection that ended (status 1).
  • A line comes shortly after a change, not for every change: several changes close together give one line with all of them. Output alone (a program printing in a pane) gives none, but what status --json shows moves too, such as an agent’s status, its context or a pane’s folder, and each of those gives a line, so a working agent can bring one every few seconds.
  • A reader that falls behind isn’t disconnected for it: the server keeps up to four lines waiting for it, which it still reads, older ones first; past those it keeps only the latest, so once it has read them it reads the state as it is now, with a jump in revision. One that stops reading altogether is disconnected once the server has waited a second to write to it, like any client.

The changes panel’s data is there for another UI too: the changed files with their status and counts, each file’s diff with the words that changed and its highlighting, the unchanged lines between two hunks, the branch it compares with and the files you marked as viewed.

Terminal window
cornercase changes list # the workspace you are in, uncommitted
cornercase changes list --every --follow # every project's uncommitted changes, as JSON lines
cornercase changes list --worktree fix/login --mode commits --json
cornercase changes file src/main.rs --json # one file's diff
cornercase changes gap src/main.rs --hunk 1 # the unchanged lines before its second hunk
cornercase changes base origin/main # compare Commits and All with another branch
cornercase changes viewed src/main.rs # mark it viewed; --clear clears it
  • changes (cornercase changes list --follow) is a stream, one per connection. It answers a first summary at once, which may say "state": "loading" while git reads, then sends a whole new one each time the files change, with a higher revision; a line is only sent when its content differs from the last. Each summary replaces the one before it, and a reader that falls behind reads the latest, as with status --follow.
  • A summary has boot, stream (its number, for changes-mode), revision, scope (project or every), mode (uncommitted, commits or all), state (loading, ready or failed, with error) and workspaces: each with project, workspace, base (what Commits and All compare with, null in Uncommitted), added, removed and files. A file has path, old_path (for a rename), status (modified, added, deleted, renamed or untracked), added, removed, fold (open, lockfile, binary or large: lockfiles show their lines, binary and large files have none), viewed and digest. Summaries have ids only; the names are in status.
  • A project stream has one workspace entry, with no files when there are none. An Every project stream (--every, only for Uncommitted) has one entry per workspace with changes, in the sidebar’s order, and never ends by itself. A project stream ends when its workspace or project closes. A workspace nobody follows costs nothing: git runs only for what a stream or the window shows.
  • The mode belongs to the stream: changes-mode changes a running stream’s mode, and the window’s tabs don’t move. It only reaches a stream the same connection opened, so it is a request for a program holding the stream’s connection open over the socket: a separate command invocation uses a new connection and gets not-found, which is why cornercase changes has no command for it (start another changes list --mode … instead). A second changes on one connection is refused with conflict. The base and the viewed marks are shared with the window: choosing a base here changes what its panel compares with, and a mark set here shows as its ✓.
  • changes-file answers a file’s hunks: old_start, new_start, context and lines, each with kind (context, removed or added), old and new numbers (null where the line is not on that side), text (tabs as four spaces), emphasis (the words that changed, as [start, end) pairs in characters of text) and syntax (runs of style and text, left out where the line isn’t highlighted). styles is the table the runs point into, in the same shape as a surface’s: palette indices, never RGB, so the client’s own theme and tints apply; entry 0 is plain.
  • changes-gap answers lines with their old and new numbers and syntax, for the lines before hunk hunk (counted from 1: hunk 0 has no gap before it). changes-base without arguments lists branches, the default branch and the current base; with base it sets it (the default branch goes back to following the default) and with clear it clears it. changes-viewed sets a mark with the file’s digest, or clears it with clear: a mark set with the digest of a version the file no longer has does not apply, as when the file changes under the window’s mark.
  • digest is a string of 16 hexadecimal digits that changes whenever the file’s change does. status --json also gives each workspace changes (files, added, removed and mode), for a badge like the button’s, while cornercase keeps it current: the window’s workspace, one a stream follows, or any while a stream follows every project; it is left out otherwise.

The session is not all of cornercase’s state. The TODO list, the queue, whether you are away on each chat app, the update found and keep awake live outside it, and another UI needs them too. status --app prints them as one JSON object, and --follow prints it again each time one of them changes:

Terminal window
cornercase status --app
cornercase status --app --follow | jq -c '[.revision, (.todos | length)]'
  • todos: the items as todo list --json has them, in the panel’s order. queue: the entries queue list --json has, prompts included.
  • away: one row per chat app, telegram then discord, each with ready (paired, and the bot’s token not refused) and away.
  • update: release (version and its notes, each with a version and text), or none saying why there is no release: disabled (a development build), off (check_updates is off and nothing was checked), unchecked, up-to-date or failed. Also checking, checked (UTC time of the last answer), error (why the last check failed), install ({"method": "replace", "path": …} when cornercase can replace itself, {"method": "command", "command": …} when you must run a command, as with Homebrew), installing, installed (restart to use it) and install_error. update-check and update-install drive it.
  • keep_awake: as in status --json, or null while the setting is off. version and boot are the server’s, as in status.
  • It follows the rules of status --follow: every line is the whole state and carries a revision that grows by one per line, revision only compares within one boot, a reader that falls behind reads at most four older lines and then the latest, and it ends when the server stops or restarts. A line is built at most twice a second, and only while someone follows. It is a stream of its own so that the session’s lines do not carry the queue’s prompts and the release notes every half second.

A flow run is state of its own: the stage the work is in, the loops, each role’s pane and status, what the roles sent each other, the files they wrote. Another UI needs it to draw the Flow panel, and a script needs it to know whether a team is done.

Terminal window
cornercase flow runs
cornercase flow runs --follow | jq -c '[.revision, (.runs | length)]'
cornercase flow messages --worktree fix-login --follow
cornercase flow files --worktree fix-login
  • flow runs prints one block per running flow: its workspace, the stage (counted from 1 for people), the rounds of its loops, the task’s first line, each role’s pane and status (closed once its pane is gone), the last step and the last one outside the plan, and whether the orchestrator marked its pane for review, which is how a run ends. --follow prints the same as one JSON line each time something changes, until the server stops. The request is flow-runs, a stream like status --app: every line is the whole state with a revision (see status --follow for what that guarantees), built at most every half second and only while someone follows.
  • A run in the JSON has run (its id, which flow run answers too and which a restore changes), project, workspace, flow (the plan the team was briefed with, as in Manage flows, frozen at the start: editing or deleting the flow does not change it), task, dir (the run’s folder), roles (the orchestrator first, then the flow’s roles in order; each with role, pane, tab, status, closed, and worktree and branch for a role with a worktree of its own; a role with no pane, after a restore that could not bring it back, has null for all), stage, rounds, holder, last, outside, ended and messages.
  • Stages here count from 0, the index in flow.stages, unlike a loop’s back.to, which counts from 1 as in the flow’s file. stage is the stage the work is in (null before the orchestrator sends anything), rounds has one number per stage (how many times its loop went back), and holder is the role that last got the work. A step is {"edge": …, "from": …, "to": …, "time": …}, where edge is {"kind": "forward", "stage": N} (it leaves stage N), {"kind": "loop", "stage": N} (stage N’s loop) or {"kind": "outside", "from": N or null, "to": M} (a role of a stage the plan does not lead to), from is the role that held the work before (not the sender, which is always the orchestrator) and time is UTC. outside is the last step outside the plan, and ended is true once the orchestrator’s pane is marked for review. Only what the orchestrator sends into a role moves the work, as in the diagram.
  • flow messages prints what the orchestrator and the roles of a run sent each other: its sends, and the keys it pressed, the screens it read and the waits it made, each with its time, who, to whom and the text. --workspace ID or --worktree BRANCH names the run (the one in the pane’s own workspace, else the shown one, by default); with no run there it says so. --follow goes on printing until the run ends. With --json each message is one line: run, workspace, seq (counted from 1 for the run), time, from, to, kind (send, keys, read or wait), text (null when there was none), bytes (the real size: a text is kept up to 32 KiB) and edge, the step along the plan it moved the work through (null when it moved nothing); the backlog comes first, expanded into those lines. Only the last 256 messages of a run are kept, in memory, and never logged or saved, so a run that was restored starts with none. The request is flow-messages, a stream of its own on its connection: its answer is {"boot", "run", "workspace", "messages": [...]} with every message kept so far, then each new message is one line, in order, with no gap in between, and it ends (Detached) when the run does. A client that cannot keep up gets a {"event": "dropped", "count": N} line, as in Follow events, and reconnects to read what is kept. A second flow-messages on one connection is conflict.
  • flow files lists the files of the run’s folder, newest first, at most 200, as a table (name, bytes, modified, path; --json prints workspace, run, dir, files and more). After the run ended it lists the last folder the workspace’s runs made, for as long as the workspace lives; a workspace that never ran a flow is conflict, and a folder that was deleted failed. The request is flow-files, one answer. To read a file, open its path: reading it over the socket is left out, as it is for the files panel.

A program that draws cornercase’s terminals itself, such as an editor extension or a GUI, needs more than read’s plain text: the cells with their colours, where the cursor is, which modes the program turned on, and how the tab is split. surface streams all of it for one tab, laid out at the size you give, as one JSON line:

Terminal window
cornercase surface --tab 3 --cols 120 --rows 40
cornercase surface --tab 3 --cols 120 --rows 40 | jq -c '[.revision, [.panes[].pane]]'

The first line is the tab as it is now, the next ones come when something it shows changes, at most about 60 a second and never twice the same. Each line is complete and replaces the one before, unless you ask for patches. Find the tab’s id with cornercase status. The tab doesn’t have to be the one the window shows, and streaming it doesn’t switch to it or change anything the window shows. It works on a server started with start-server, with no window.

A line has these keys:

  • boot, session, surface and revision: the server’s start (see Contract and compatibility), the revision of the latest status --follow line sent before this one was drawn (0 if none was sent since the server started), this stream’s id (the one resize takes) and its own counter, from 1, up by one per line. A line from another boot is stale. To get a complete baseline again, run the command again: its first line is always complete.
  • tab, cols and rows: what you asked for.
  • layout: the tab’s whole split tree, as in status --json, hidden panes included. dividers: each divider with its dir, its path through layout (false for the first side, true for the second), the region it divides (area) and its cells (line).
  • panes: the panes the surface shows, each with pane (its id), active, rect (x, y, cols, rows: where to draw it in the surface), size (its terminal’s own columns and rows), cursor (x, y, shape as block, bar, underline or block_hollow, and blinking, or null when hidden or scrolled out of view), modes, scroll, colours and lines.
  • modes: mouse (none, press, press_release, button_motion or any_motion), mouse_encoding (default, utf8 or sgr), alternate_screen, alternate_scroll, bracketed_paste, application_cursor, focus_events and kitty_keyboard (the flag bits). They are what you need to turn keys and the mouse into what the program expects.
  • scroll: total rows of history plus screen, offset of the view’s first row, and rows in the view. What is under the view is total - offset - rows. A pane scrolled up shows its history in lines.
  • hyperlinks: the table the runs’ hyperlink numbers index, shared by all the panes, left out when no cell has a link: [{"uri": "https://example.com"}], from OSC 8 (ls --hyperlink, compilers, gh). A link is its address: cells with the same address are one entry, so a link that wraps onto the next line is one link, and two separate spans to the same address open the same place. OSC 8 ids aren’t sent.
  • colours: the colours the pane’s program changed, left out when it changed none: fg, bg and cursor as [r, g, b] (OSC 10, 11, 12) and palette, such as {"1": [255, 0, 0]} (OSC 4). Resolve a colour {"index": n} through palette before your own palette, and draw default-coloured cells with fg and bg when they are there. Your theme is never listed: only what the program set, until it resets it.
  • lines: one entry per row of the pane’s terminal (size.rows), each a list of runs, {"style": 1, "hyperlink": 0, "cells": ["o", "k"]}: consecutive cells of one style and one hyperlink (hyperlink is left out when the run has none). A blank that is part of a link is kept at the end of a line. A cell is a string, the character with any combining marks. The right half of a wide character is "": don’t draw it, the cell before covers it. The cells after the last one that isn’t a blank in the default style are left out, so an empty line is [] and the rest of a line is blank.
  • styles: the table the runs’ style numbers index, shared by all the panes. Entry 0 is {}, the terminal’s default colours. Others have fg and bg (absent for the default colour, {"index": 0-255} for a palette colour, which your theme decides, or {"rgb": [r, g, b]}) and the attributes that are on: bold, dim, italic, underline, blink, inverse, invisible, strikethrough, overline, and for underlined cells underline_style (double, curly, dotted or dashed; a plain underline has none) and underline_colour (like fg). A client that knows only underline draws every style as a plain one. A selection made in a window shows as inverse.

A long-lived program that parses every line pays for the whole tab each time, and so does a link that carries it. With --patches the first line is complete and the next ones carry only what changed:

Terminal window
cornercase surface --tab 3 --cols 120 --rows 40 --patches

A server from before patches ignores the option and sends complete lines, which you apply the same way, and cornercase contract lists the feature surface-patches to check for it. A line with no base is complete, as above. A line with base is a patch, and has boot, session, surface, revision, tab, cols and rows as before (and view and projection for --view), then:

  • base: the revision it applies to, always the one before it. Apply a patch only when base is the revision of the last line you applied; otherwise ask for a complete line with cornercase redraw --surface ID, or connect again.
  • styles and hyperlinks: tables for this patch’s runs only. A run’s style and hyperlink in a patch index these, not the tables of the line you hold, so resolve them as you apply a row and keep what you resolved.
  • panes: only the panes that changed, each with pane and cursor (always: null is a hidden cursor), and only what changed of active, modes, scroll, colours ({} once the program resets them), copy and lines. A pane that is not listed did not change.
  • copy: from, to and count. Before you apply lines, take rows from to from + count - 1 of the pane as you held them and write them at to. It is how output that scrolls arrives: the rows that moved, not their content. Rows it doesn’t cover keep what they had.
  • lines: row number ("3" is row 3) to that row’s whole line of runs, as in a complete line; [] is a blank row. Replace those rows after copy.

The layout, dividers, the set of panes, their rect and size, and the tab and size of the surface never change in a patch: when any of them does, the next line is complete. It is also complete when the view’s projection changes, after the server held back a line because you were slow to read, and when you ask:

Terminal window
cornercase redraw --surface 4

redraw takes the surface of the lines. The surface’s next line is complete and comes at once, even if nothing changed. It works with or without --patches. An unknown or ended surface is not-found, and no --surface is wrong usage.

Things to know:

  • The panes are laid out in your cols × rows the way the window lays out a tab, including when the tab is too small for all its panes: then only the active one shows, over the whole surface. The terminals keep the size of whoever sizes the tab (the window, or a view), though, so a pane’s size can differ from its rect: lines and cursor follow size. Streaming a --tab doesn’t resize anything.
  • cols is 1 to 1000 and rows 1 to 500. A connection streams one tab; cornercase resize --surface ID changes the size of one given with --tab in place, and another tab needs another connection.
  • It ends, with status 0, when the tab closes or the server stops or restarts (stopping it yourself leaves the shells running). A pane whose shell exits drops out of the next line. A tab that doesn’t exist, or an out-of-range size is an error, with status 1; a missing --tab, --cols or --rows is wrong usage, with status 2.
  • A reader that falls behind gets fewer lines, each complete (with --patches, the line after falling behind is complete), so it never has to repair a picture: the server draws a new line only while little is waiting for it. One that stops reading altogether is disconnected once the server has waited a second to write to it, like any client.
  • Keys added later, such as hyperlinks, colours, underline_style, underline_colour and overline, are left out when empty, so a reader that doesn’t know them keeps working and a terminal that uses none of them sends exactly what it did.
  • Not sent yet: images, OSC 8 link ids, pixel sizes and a terminal size set pane by pane. A pane’s title is in status --json, and its bells and clipboard writes in events --host.

Two programs that draw cornercase, or a program and the window, shouldn’t have to share one selection and one size. view opens a view: its own project, workspace and tab, an optional size, and an active flag. It keeps running and prints the session as one JSON line each time it changes, like status --follow, plus a view object, and the view lives as long as the command does: stopping it closes the view and hands its tabs back. The processes keep running.

Terminal window
cornercase view --cols 120 --rows 40 > view.jsonl &
id=$(head -n 1 view.jsonl | jq .view.id)
cornercase new-tab --view "$id" --focus --name logs
cornercase surface --view "$id"
  • view has id, projection, active, cols and rows (null without a size), shown (the project, workspace, tab and pane it shows) and controls (whether it sizes its tab’s terminals now). It starts on what the window shows, or on --tab, --workspace or --project; the window doesn’t move. Each tab in the lines has controller: the id of the view sizing it, absent when the window does.
  • --view ID works on status, focus, open, new-workspace, new-tab, split, start, send, keys, read, wait, mark, rename, go and surface, also from separate commands. The view’s tab is their default, and focus, --focus move the view, not the window or the other views. focus --view prints the view’s new tab and its projection. Other commands refuse it. A server without views is refused up front, naming the update.
  • When something the view shows goes away, it moves like the window would: a closed tab to the one before it, a closing workspace or project to its neighbour. That never resizes a tab for it.
  • surface --view ID streams the tab the view shows, at the view’s size, and follows the view to other tabs. It takes no --tab, --cols or --rows, and each frame carries view and projection.
  • view-set --view ID changes it: --cols and --rows, --active or --inactive, --hidden or --visible (see below), and --claim to size the tab it shows from now on. A view without a size, or inactive, sizes nothing and streams no terminals, but its lines keep following the session.
  • The tab a view shows counts as seen, like the window’s, while the view is active, has a size and is visible: an agent there gets no ✓, toast or desktop notification. view --hidden and view-set --hidden say its window is hidden or minimised, so agents there notify again, and --visible says it is back. While a view is open, the window’s tab counts as seen only while a window is attached; a program that only shows notices can open a view without a size.

Who sizes a tab. One at a time, so two programs showing the same tab never fight over its size. The window is the default. A view claims the tab it shows when it opens, moves there, turns active, or with --claim; using a window (a key, a paste, a click) takes back the tab it shows, until the window shows another tab. The latest claim sizes the tab’s terminals at the view’s cols × rows, without the window’s sidebar or panels; when it lets go (inactive, moved on, closed) the tab goes to the latest one left, else the window.

projection starts at 1 and goes up each time what the view’s surface shows changes: its tab, its size or whether it is active. Show a frame only when its projection is the latest one you know: an older one was drawn before your last move.

Still shared with the window and every other view: the programs and their output, how a tab is split, which pane of a tab is focused (focus --view --pane moves it for everyone), the scroll position and the selected text. The window’s overlays, side panels and sidebar scrolling are its own.

A program that draws a tab with surface also has to type into it and handle its own mouse. A window’s keys and clicks go through the window, where a dialog or a field can take them and a click is a cell of the window’s layout. These commands act on a pane or tab you name, whatever the window shows:

Terminal window
cornercase input --pane 12 '{"text": "ls"}' '{"key": {"code": "enter"}}'
cornercase input --pane 12 '{"key": {"code": "c", "mods": ["ctrl"]}}'
cornercase input --pane 12 '{"mouse": {"kind": "scroll-up", "x": 10, "y": 5}}'
cornercase scroll --pane 12 --up 10
cornercase selection --pane 12 --from 0,3 --to 79,5 --copy
cornercase ratio --tab 3 --path second 0.3
cornercase resize --surface 4 --cols 100 --rows 30
cornercase link --pane 12 --cell 10,3
  • input takes one JSON event per argument, sent in order (- reads one per line from standard input): {"key": {"code": "c", "mods": ["ctrl"], "kind": "press"}} (code as for keys; mods any of ctrl, alt, shift, super; kind press, repeat or release), {"text": "héllo"} (committed text, no control characters), {"paste": "two\nlines"} (bracketed when the program asked for it) and {"mouse": {"kind": "down", "button": "left", "x": 3, "y": 1, "mods": []}} (kind down, up, drag, moved, scroll-up, scroll-down, scroll-left or scroll-right; x and y are cells of the pane’s terminal, from 0, as in the lines of a frame).
  • Events are encoded the way the program in the pane asked for, the way the window encodes what you type: its keyboard protocol, application cursor keys, bracketed paste and mouse mode and encoding, all in the modes of a frame. Keys, text and pastes bring the pane’s view back to the bottom. They don’t set the shared window size and don’t answer for the agent: unlike send and keys, input types into an agent’s dialogs and answers its permission prompts, because it is a person typing through another UI. Scripts and agents should keep using send and keys, which check first.
  • A mouse event the program doesn’t take in its current mode, or a key release when it didn’t ask for event types, is not sent and is listed in the answer’s skipped ({"event": 0, "why": "mouse"}; the command prints a warning). Everything else is encoded first and written in one go: when one event is wrong, or the program isn’t reading its input, nothing is sent. Add "size": {"cols": 80, "rows": 24} to the request to say which terminal size your coordinates are for: if the pane has another size now, nothing is sent (conflict).
  • scroll moves the pane’s view through its history by --up or --down rows, or to --top or --bottom, and answers with its scroll as a frame has it. The view is the pane’s, so the window shows the same one. On the alternate screen, or when the program reads the mouse, the program scrolls itself: send it keys, or the wheel with input; scroll refuses with conflict.
  • selection with --from X,Y --to X,Y selects between two cells of the pane’s view (columns and rows from 0, as in lines; --rectangle for a block) and prints the text. Without them it prints what is selected, --clear removes the selection, and --copy also copies it through the window, as selecting there does, and prints what it copied (the answer’s text), so a program without a window can put it in the clipboard itself. The selection is shared with the window, which shows it as inverse, and with every client; the last one to change it wins.
  • ratio RATIO sets the share of the first side of a tab’s divider, between 0 and 1. --path names the divider as the path of a frame’s dividers does (first or second at each level down; without it, the outermost). The terminals follow at once, with the window’s geometry. To open or close splits use split and close.
  • resize lays a running surface out at another size without starting it again; ID is the surface of its lines. Its next line comes at the new size, with the next revision. It resizes no terminal: they keep the size of whoever sizes the tab. A surface that follows a view (surface --view) takes the view’s size, so resize refuses it: change the view with view-set.
  • link --cell X,Y says which file a click on that cell of the pane’s view opens in the window (columns and rows from 0, as in lines), so you get cornercase’s detection of the paths programs print without copying it: the path under the cell, looked up from the folder of the program in the foreground, then from the root of the pane’s workspace, with its line or lines. It prints path, path:12 or path:12-30, and nothing, with status 0, when the cell holds no file; --json also says whether the file is inside the workspace. It opens nothing, and OSC 8 links are in the frame’s hyperlinks, not here.
  • Requests on different connections aren’t ordered: send the next input after the answer to the previous one, or put several events in one request. Unlike the other commands, these six log at debug level, since a program sends them as fast as someone types or drags.

Programs in the terminals ask for the colours they are drawn with (OSC 10, 11 and 4, and the light or dark query): Claude Code, Neovim and delta pick their look from the answer. The server takes them from the first window, so a program that drives cornercase without one has to give them:

Terminal window
cornercase start-server --theme dark.json
cornercase theme light.json # the system switched to light
cornercase theme - < dark.json # read from standard input
cornercase theme # client colours, dark
cornercase theme --clear # back to the window's colours, or the defaults
{
"foreground": [192, 202, 245],
"background": [26, 27, 38],
"palette": [[21, 22, 30], [247, 118, 142], [158, 206, 106], [224, 175, 104],
[122, 162, 247], [187, 154, 247], [125, 207, 255], [169, 177, 214],
[65, 72, 104], [247, 118, 142], [158, 206, 106], [224, 175, 104],
[122, 162, 247], [187, 154, 247], [125, 207, 255], [192, 202, 245]]
}
  • The foreground, the background and the 16 ANSI colours are required, as [r, g, b]. palette may go up to 256 entries; entries 16 and up are optional (absent or null keeps the default cube and greys). A theme that misses a part is refused (malformed, and cornercase exits 1 before connecting). Over the socket the request is {"command": "theme", "args": {"theme": {…}}}, {"args": {"clear": true}} or {"args": {}} to read; start-server takes the same object as args.theme.
  • The answer, and theme in status --json, status --follow and the start-server answer, say where the colours come from: {"from": "default" | "window" | "client", "scheme": "light" | "dark" | null}.
  • These colours win over a window’s until you give others, clear them, or the server stops. They are not saved: after a restart give them again. A window that attaches later draws its own chrome with its own colours and leaves the terminals’ alone; clearing gives the terminals the first window’s colours, if one ever gave them. start-server --theme applies even when the server already runs. The last theme given wins.
  • Every open terminal and every one made afterwards takes the new colours at once. Programs that turned on colour scheme reports (mode 2031) are told when light and dark switch. A program that read the colours once at start keeps its guess until it asks again, and a colour a program set itself (OSC 4, 10, 11) wins over these.
  • surface frames keep palette indices: resolve them with the colours you gave, so the program’s guess and your drawing agree.
  • Not covered: the cursor colour, colours per view, and the TUI’s own colours, which stay the window’s.

search answers “which pane dealt with this?” without opening tabs one by one. It looks, ignoring case, in what the coding agents in your panes were told and answered (Claude Code, Codex, Gemini CLI, opencode, Grok Build, Cursor CLI and GitHub Copilot CLI: the records read --last-message reads) and in the history of every pane, shells included. It is the same search as in sessions in the window, and leaves out panes of projects being closed and workspaces being removed.

Terminal window
cornercase search 'migration failed'
cornercase search --json checkout
pane=$(cornercase search --json 'tax bug' | jq -r '.matches[0].pane')
cornercase focus --pane "$pane"

Text is one line per match under a header, separated by tabs, the way status --panes is. Matches come in the order of the sidebar, an agent’s messages first, newest first, then the pane’s history from the bottom up.

pane who source place line text
4 claude reply web-shop › feat/dark-mode › claude - The checkout tax bug is in src/checkout.rs:42, where the rate is rounded twice.
4 claude prompt web-shop › feat/dark-mode › claude - Why is the checkout tax wrong for gift cards?
9 zsh screen orders-api › main › zsh 118 migration failed: duplicate key value violates unique constraint
Column What it holds
pane The pane’s id, for read, send, focus and the other commands.
who The agent’s name, else the program in the foreground.
source prompt or reply when it comes from an agent’s record, screen when it comes from what the pane printed.
place project › workspace › tab.
line The line in the pane’s history, for screen matches; - for an agent’s messages.
text The matching line, cut to about 160 characters around the match.
  • A - stands where there is no value, and a tab or a line break shows as a space, so every row is one line with the same columns. New columns only ever go at the end.
  • A match of a screen that the agent’s record already holds (agents print what they say) is listed once, as the agent’s message.
  • It lists at most 20 matches per pane and 300 in all. With --json capped says some were left out.
  • No match prints the header alone and exits 0, since finding nothing is an answer. An empty search is an error.
  • --json prints {"matches": [...], "capped": false}, each match with pane, tab, workspace, project (the ids), place, who, source, line (null when there is none), text and written (when an agent’s message was written, in UTC, or null).
  • The text you search for is never written to server.log.
Terminal window
cornercase open ~/code/orders-api # prints the project's id
cornercase new-workspace experiments # a workspace in the project's folder
cornercase new-workspace fix/login --worktree # a git worktree on branch fix/login
cornercase new-tab --name server -- npm run dev # prints the new pane's id
cornercase split --down -- tail -f server.log # splits the pane below
  • open finds a project that is already open instead of opening it twice.
  • new-workspace adds to the project the command runs in (--project ID for another one). With --worktree, the project must be at a repository’s root, as in the new workspace dialog: the checkout goes in the worktrees folder from settings, the branch is created from HEAD when it does not exist, and, when the project has no setup, the command returns once git has made it. Each new workspace gets one tab, which runs the project’s setup first when it has one; new-workspace --worktree and start --worktree wait for it and answer an error naming the exit status if it fails; status --json shows a workspace’s setup as running or failed. close --remove-worktree runs the project’s teardown first; if that fails the worktree is still removed and the command prints warning: … on stderr.
  • new-tab opens the tab in the workspace the command runs in (--workspace ID or --worktree BRANCH for another one). split splits the pane it runs in (--pane ID for another one), and refuses a pane too small to split.
  • After --, the words of a command are joined with spaces and typed at the prompt once the shell is ready, as ssh does with its command, so quote what the shell should read as one argument: -- 'cargo test 2>&1 | tee test.log'. The command returns once it is typed.
  • Each prints the new pane’s, workspace’s or project’s id, so pane=$(cornercase new-tab …) needs nothing else. --json prints every id: {"project": 1, "workspace": 2, "tab": 8, "pane": 9}.
  • --focus also shows what was made.
Terminal window
cornercase start claude --worktree fix-login --prompt 'Fix the login form, then commit'
cornercase start codex --prompt-file task.md --wait --timeout 1800
git diff | cornercase start claude --prompt-file -

start does what start does in the issue browser: it opens a tab, types the agent’s command with its arguments and mode from settings → Agents, waits for its folder trust question to be answered (by you, unless trust prompts is on), and once the agent is ready pastes the prompt and presses Enter. It returns then, printing the pane’s id, or fails if the agent never shows up.

  • Which agent: the one you name (claude, codex, gemini or any other kind); without one, the default from settings → Agents, or, when that is auto, the agent running in the pane the command runs in.
  • Where: a new tab in the workspace the command runs in; in another workspace with --workspace ID; or with --worktree BRANCH, in the workspace on that branch, made as a new worktree when there is none.
  • --name names the tab, --prompt gives the prompt and --prompt-file reads it from a file, or from standard input with -. Without a prompt, the agent just starts.
  • --model starts it with that model, through the agent’s own flag (--model opus, --model gpt-5.5), replacing a model set in its arguments. --mode picks another of the agent’s modes, as settings → Agents lists them (--mode plan), instead of the one chosen there. A mode the agent lacks is refused with the list of its modes. --effort sets how hard the agent thinks, through its own switch (--effort high for Claude Code, -c model_reasoning_effort=high for Codex, --reasoning-effort high for Grok Build and GitHub Copilot CLI; Cursor CLI takes it in the model id), replacing an effort set in its arguments; a level the agent does not take is refused with its levels, and Gemini CLI, opencode and Cursor CLI, which have no effort switch, refuse any. A server older than these options refuses them rather than start the agent with the settings’ mode, which could be one that never asks.
  • --issue KEY starts the agent on an issue instead of a prompt, as the Issues dialog’s start does: the branch is the one named after the issue, the prompt comes from settings → Issues → prompt, and it is always submitted, whatever send the prompt says there. A key such as #482, sc-48 or ENG-42, or the issue’s URL; --source picks the tracker when a key could be several. A GitHub or GitLab issue is worked on in the repository of --project ID (default: the one the command runs in, else the shown one), in a worktree on its branch; a workspace already on that branch gets a new tab with the agent. Shortcut, Linear, Jira, Plane and Asana issues aren’t tied to a repository, so with several places open the command lists them and asks for --project ID (a new worktree) or --workspace ID (a new tab). The workspace is named after the issue, and so is a tab unless --name says otherwise.
  • --wait then waits like wait does, and prints how it ended on a second line. --until with one of wait’s states (idle, working, waiting, shell, turn-over) waits for that one instead.
Terminal window
cornercase account add claude work --login
cornercase account list --json
cornercase account use claude work
cornercase account rename claude --system Personal
cornercase start claude --account claude= --prompt 'fix the tests'
cornercase profile new Work claude=work codex=work
cornercase profile use Work
cornercase start claude --profile Home --prompt 'fix the tests'

account and profile manage the accounts and profiles agents start with. Every answer has the same shape (--json): the agents that take accounts, each with its variable, its accounts (name, canonical path, label when you named it, and unshared, the settings that stopped being links), system_label, system_path and system_login for the agent’s own login, and active, the account its new tabs use (left out for its own login); then the profiles with their accounts, and profile, the active one ("" for none). account use AGENT ACCOUNT (account-use) makes new tabs of that agent use an account (--system, "" on the wire, for its own login), keeping the other agents’ and leaving any active profile; account rename AGENT ACCOUNT NAME (account-rename) sets the name the window shows (--system for the agent’s own login, --clear or "" to remove it): a name already used by another account of that agent is a conflict. Both name an account by its folder, never by its name. --account AGENT=ACCOUNT on start, new-tab, flow run and queue add|edit (the accounts field, AGENT= for its own login) starts with that account once, over a profile; --profile NAME on start, new-tab and flow run uses a profile once without changing the active one (--profile '' is every agent’s own login). An unknown agent, account or profile fails with not-found before anything opens. A server without accounts, or without account-switch for --account, would ignore the field, so the command checks the server’s contract first and refuses. profile use --system (profile-use with "") also forgets the accounts chosen with account use, so every agent goes back to its own login. status --app gives accounts, the account each agent’s new tabs use. status --json names a pane’s account, and a pane’s opened event its accounts. account list --check (the check field of account-list, behind the account-login feature) first asks each account whether it is logged in, with the agent’s own claude auth status or codex login status, and answers once they have all replied: each account then has login (state: signed-in, signed-out, unknown or failed; email and plan when the agent gives them; checked, UTC). tokens lists the names, never the values, of the token variables set where the server runs, which win over the accounts. A queue entry keeps its accounts; one whose account was removed before its time fails and says why.

Terminal window
cornercase flow list
cornercase flow run Quick --prompt 'Fix the login form'
cornercase flow run 'Full issue' --prompt-file issue.md --worktree fix-login --json
cornercase flow run 'Full issue' --issue ENG-42 --project 3

flow list prints each flow, yours, the built-in ones (those you have not replaced) and the project’s, sorted by name, with its description, its roles with their agents, models and efforts, and what keeps it from running, if anything; files it cannot read are listed with their error. flow run starts one as the window’s start does: it makes the worktree (from --worktree, else from the task’s first words, with a short suffix when they give nothing, a default branch or a branch already taken), opens a tab per role, starts every agent without permission prompts and answers their trust questions, and starts the orchestrator last with its brief. It answers once the tabs exist, while the agents are still starting: the workspace’s id, then one line per role with its pane’s id and the role’s name, separated by a tab. The run’s folder for its files is printed on stderr (run_dir with --json), and every pane of the team has it in $CORNERCASE_RUN_DIR. To follow a run’s end, watch run.reached in status --follow.

8
10 orchestrator
12 coder
14 reviewer
  • A role set to worktree = "own" gets a worktree of its own on the branch <branch>--<role>, made from the run’s branch, with the project’s setup. --json then adds, for each such role, its worktree (the folder) and branch; the other roles’ entries have neither. The answer waits for every worktree and its setup, and a role worktree that cannot be made opens nothing.
  • The task comes from --prompt, or from a file with --prompt-file (- reads standard input), or from an issue with --issue KEY, as the dialog’s start a flow takes it: its branch, its prompt template and, for an issue that isn’t tied to a repository, --project ID when several projects are repositories (see Issues).
  • The flows of the project the command runs in, else the shown one, else --project ID, are found too. A name is looked up in yours, then the built-in flows, then the project’s: yours replaces a built-in flow of its name, and a project’s flow, which comes with the repository, never stands in for another of the same name; the command says so on standard error when a name is in several places. A built-in flow you deleted in settings → Flows is refused, and keeps its name: a project’s flow named like it does not run either. An unknown name lists the flows.
  • It refuses before anything opens a flow with problems, an agent that is not installed, a project that is not the root of a git repository, a branch that already runs a flow, and a --worktree that is main, master, the project’s default branch or the one its own folder is on: a flow works on a branch of its own.
  • Nothing changes in the window unless you pass --focus, which shows the orchestrator’s tab and opens the Flow panel.

flow export NAME prints a flow as TOML, so cornercase flow export Quick > quick.toml makes a file to send, and --json prints its name, origin, path and text. flow import SOURCE (a file, - for standard input, an https:// address or OWNER/REPO on GitHub, see community flows) adds one to yours, or to the project’s with --to project, and prints where it went. A flow from an address or a repository, or that sets env or args, asks first, or needs --yes without a terminal; a name that is taken is refused unless --replace overwrites it or --name gives the new one another. A text over 256 KiB is refused. See Share a flow.

Terminal window
cornercase flow list --json
cornercase flow delete 'Quick mine'
cornercase flow hide Quick
cornercase flow restore Quick

Each verb means one thing. flow delete NAME deletes the file of a flow of yours or the project’s (--in yours or --in project when both have the name, --project ID, or --path FILE for a file that cannot be read, as flow list --json gives it) and nothing else: a run already going keeps the plan it was started with. It refuses a built-in flow, which has no file, and your version of one, with the command that does what you mean (conflict). flow hide NAME is what the delete button of settings → Flows does with a built-in flow: it hides its name, and deletes your version of it if you edited it. flow restore NAME brings a hidden built-in back and deletes your version of it. Both change nothing when there is nothing to do (changed is false), and name only built-in flows (not-found for others). None asks first, and --json prints name, origin, deleted (the file), hidden and changed.

An app with an editor of its own works on the flow as JSON. flow list --json gives each flow whole as flow: name, description, notes, end (pull_request or commits), orchestrator and roles (each with name, agent, model, effort, args, env, edits, worktree (shared or own) and prompt) and stages (roles and back, null or to counted from 1, as in the file and the editor, when and at_most), with edited for your version of a built-in. The deleted built-ins come as hidden, and builtins lists every built-in name, which stays reserved even while it is hidden. The request flow-save takes that same flow, place (yours or project), project, path (the file of the flow being edited, as flow list --json gave it, when it is renamed or moved: the old file is deleted; any other path is not-found) and check:

{"command": "flow-save", "args": {"flow": {"name": "Quick mine", "description": "", "notes": "", "end": "pull_request", "orchestrator": {"name": "orchestrator", "agent": "claude", "model": null, "effort": null, "args": [], "env": {}, "edits": false, "worktree": "shared", "prompt": "Run it."}, "roles": [{"name": "coder", "agent": "claude", "model": null, "effort": null, "args": [], "env": {}, "edits": true, "worktree": "shared", "prompt": "Write it."}], "stages": [{"roles": ["coder"], "back": null}]}, "place": "yours", "path": null, "check": true}}

With check it answers problems (what the editor shows in red: the flow’s own and a name conflict) and the check text (what a project’s flow runs unattended: its env and args), and writes nothing; without it, it saves, answers the path and is refused as malformed while the flow has problems of its own. A request is not-found for a project that is not open and a path that is no flow file, and malformed for another place. The last write wins over an editor open on the same flow, as with a file edited by hand.

Terminal window
cornercase issue sources
cornercase issue list --source github --assignee me
cornercase issue list --closed --json
cornercase issue read '#482'
cornercase issue people jira
cornercase start claude --issue '#482'

These read the trackers through the connections the server already holds, the ones the Issues dialog uses: GitHub through gh, the others through the tokens saved in settings or set in the environment. They change nothing at a tracker. Another program that wants the issues gets them without keeping a client for each tracker or a token of its own.

issue sources says, for the project (--project ID, default the one the command runs in, else the shown one), which trackers are connected and as whom, in the order of the dialog’s tabs, then the others. Each is one of:

  • connected, with its account and where its token comes from (through gh, saved, or from LINEAR_API_KEY).
  • missing, with what is missing: token (connect it with cornercase config secret SOURCE, the token on standard input), site and email for Jira (cornercase config set jira_site …), workspace for Plane (plane_workspace), or repository (the project isn’t a git repository, or for GitLab none of its remotes is on gitlab_url). An Asana token with several workspaces shows as failing until asana_workspace is set.
  • failing, with the reason the last list failed (a rejected token, a rate limit, an Asana token that belongs to several workspaces). It is a list’s failure only, and the next good list or a change to that tracker’s token or settings clears it.

It ends with the places an issue that isn’t tied to a repository can be worked on: a repository-root project is a new worktree, an open workspace of any other project a new tab, and the one you are in is marked.

issue list prints one line per issue under a header, separated by tabs (source, key, state, author, updated_at, title, url, branch, with - for nothing; new columns only at the end). Without --source it lists every connected tracker the project can use, newest first, as the dialog’s All tab does. --closed adds closed issues, and --assignee and --author (also --owner and --requester, --creator, --reporter, the names the dialog uses) take me, anyone, a handle or, for Jira, Plane and Asana, an id from issue people; a handle needs --source, since a filter names one tracker’s people. --cached answers from the lists the server already holds, without asking any tracker, and an empty answer then means nothing was held for those filters. A tracker that fails does not stop the others: it is named on standard error with its reason, its cached list (if there is one) is still printed, and the command exits 1 after printing the rest.

issue read KEY prints the issue’s title, the dialog’s info line, its URL and the branch start would use, then its description as Markdown and its comments. KEY is a key or a URL, as for start --issue; a URL of another repository, site or workspace than the project’s is refused. issue people SOURCE prints whom that tracker’s filters can name, one per line with the handle, name and id.

With --json, sources prints {"project", "closed", "sources": [...], "places": [...]}, list prints {"project", "issues": [...], "sources": [{"source", "count", "cached", "account", "error"}]}, read prints {"issue", "info", "body", "comments", "prompt"} and people prints {"source", "fields", "cached", "people"}. They are four requests, issue-sources, issue-list, issue-read and issue-people, with the same options as fields (people is the two filters in the order fields names them: "me", "anyone" or {"person": {"handle": "bo", "id": null}}). A request is refused with not-found for an unknown project, malformed for an unknown source or a person without one, and conflict for an issue of a tracker that is not connected or that was reconnected while it was read. start and flow-run take issue and source, which an older server would ignore, so check the issues feature first.

Terminal window
cornercase send --pane 12 --enter 'Now add tests for it'
cornercase send --pane 12 --enter --wait --timeout 900 - < next-step.md
cornercase send --pane 12 --enter --wait --until turn-over 'Run the tests'
cornercase keys --pane 12 ctrl+c
cornercase keys --pane 12 down enter
cornercase read --pane 12
cornercase read --pane 7 --lines 200
cornercase read --pane 12 --last-message
cornercase read --last-message --pane 12 --pane 15
cornercase read --last-message --pane 12 --pane 15 --json
  • send pastes the text in one go, bracketed when the program asked for it, as a paste from your terminal would be. - reads it from standard input. --enter then presses Enter once the screen settles.
  • With --enter, send to Claude Code, Codex, Gemini CLI, opencode, Grok Build, Cursor CLI or GitHub Copilot CLI returns once the agent has recorded the prompt in its own history (Claude Code’s transcript, Codex’s rollout, Gemini’s session file, opencode’s database, Grok Build’s session, Cursor CLI’s chat store), so its success means the prompt was submitted, not just typed. When the agent records nothing, it fails with not confirmed: the prompt may not have been submitted: read the pane before sending it again. Codex, Gemini, Grok Build, Cursor CLI and GitHub Copilot CLI keep a prompt sent while they work until they take it, so send returns then; --timeout gives up earlier, with status 1. Grok Build queues that one Enter. Pressing Enter again sends the queued prompt now and cancels the turn, which send does not do. In any other program, send --enter returns once Enter is pressed.
  • GitHub Copilot CLI’s / and ! lines and a lone ? write no prompt record, so send --enter returns after Enter for them; ? text is a real prompt. A prompt typed while it works is queued, and Copilot records it when it takes it, so the confirmation waits for that.
  • Cursor CLI’s / commands and ! shell input return after Enter without a prompt confirmation too. Cursor’s chat store is read from its code, not a live session, so a prompt it records in a layout cornercase cannot read fails with not confirmed; read the pane then.
  • Gemini local slash commands, ? help and ! shell input return after Enter without a prompt confirmation, so they do not fail with not confirmed; use read to inspect their result. Gemini has no calibrated input-box check, so read its screen before sending into a picker or dialog.
  • --wait then waits until the agent stops working; it fails if the agent does not start working within 10 seconds of the Enter, rather than waiting forever for an agent that never got the text. --until waits for another of wait’s states, as start does.
  • send refuses a pane whose agent is waiting for an answer to a question or a permission prompt: whatever it typed would answer it. Answer such a prompt with keys, after deciding what the answer should be.
  • send also refuses a Claude Code agent that shows a dialog, a panel or its shell mode instead of its input box: in the details of a background shell an x stops the shell and Enter closes the panel, and in shell mode (!) a prompt would run as a command. cornercase tells from the screen, and only once it has seen that agent’s input box, so a Claude Code that draws it differently is never refused. Read the pane, close what is open and send again; --force sends anyway.
  • send also refuses a Claude Code agent that shows its feedback survey above its input box (How is Claude doing this session? (optional), then 1: Bad 2: Fine 3: Good 0: Dismiss): one digit sent alone answers the survey instead of reaching the agent, so send --enter 2, the answer to a numbered question, would be lost. cornercase keys --pane 12 0 dismisses the survey without answering it; then send again. --force sends anyway: a longer text hides the survey and goes to the input box, as when you type over it.
  • send also refuses a pane whose agent is resuming its conversation after a restart (resuming in status): before the agent is back, text would land in front of the resume command or in the agent’s start-up. Wait until status no longer shows it, or pass --force. keys is not refused.
  • send (with or without --enter) and todo send wait while a person is typing in the pane, so your text never joins theirs and they never send half a sentence. It waits while the pane has a draft (text typed or pasted into it from a window, the browser or cornercase input and not yet sent with Enter or dropped with Esc, Ctrl+C or Ctrl+U; a draft lapses after a minute without typing, and for Claude Code as soon as its input box is empty again), while someone typed in it in the last 2 seconds, and while an earlier message to that pane has not gone out. Then it pastes, presses Enter and confirms as usual; messages held for one pane go out one at a time, in the order they came. Meanwhile a bar on the pane’s bottom row says 1 message waiting from pane 40 · goes after yours, with send now (sends it at once, as --force) and wait turn, which only says the person saw it: the bar stays, with the working icon and queued: goes right after yours, and a new held message shows wait turn again. Either way the message goes out right after the person’s prompt. status shows held_sends and draft for each pane. --timeout bounds the wait and its error says whether the pane had a draft or recent typing; --force sends at once. While a message sits between its paste and its Enter (at most about 3 seconds), what a window types into that pane is dropped with a toast. keys never waits.
  • keys presses keys, encoded the way the program in the pane asked for: enter, esc, tab, backspace, space, up, down, left, right, home, end, pageup, pagedown, delete, insert, f1 to f12, or one character, each after any of ctrl+, alt+ and shift+ (shift+tab, ctrl+c, alt+x).
  • A program that draws the terminals itself should type with input instead: it sends keys, text, pastes and mouse events to the pane you name without checking what the agent shows, which is right for a person typing and wrong for a script. Scripts and agents keep send and keys.
  • read prints the screen as text. --lines N prints the last N lines instead, scrollback included (a pane keeps its last 10 MB of output, about 14,000 lines at 80 columns). Lines the terminal wrapped come back as one, and empty lines at the end are left out.
  • A Claude Code screen may end with its feedback survey right above the input box, ● How is Claude doing this session? (optional) and 1: Bad 2: Fine 3: Good 0: Dismiss: that is Claude Code asking you, not something the agent wrote. status marks it (survey open), and keys --pane 12 0 dismisses it.
  • read --last-message prints the last message the agent in the pane wrote, as plain text. It comes from the agent’s own record, not the screen: Claude Code’s transcript, Codex’s rollout, Gemini’s session file or opencode’s database, the ones cornercase already reads for the context line. So the message comes whole even once it scrolled off, without the input box, the status lines or a suggestion Claude Code shows in grey, and nobody has to ask an agent to write its report to a file. While the agent works it is the newest message so far, such as a note on what it is about to do; once its turn is over it is its answer. --json adds written, when the agent wrote it, and turn_over.
  • With --last-message, repeat --pane or --tab to read several agents in one call, using the ids from status --panes. Each message appears under a header with its pane id, agent, turn_over and written; unknown metadata is unknown. --json gives {"panes": [...]}, with the usual individual answer in each entry. Panes come first, then tabs, each in the order given, and a repeated id is read once. A failed target gets its own error block or JSON entry with error and the requested pane (or tab, when its pane could not be resolved). The command reads every target before exiting 1 if any failed. One distinct target keeps the usual output exactly. Reading screens takes only one target.
  • read --last-message fails, with status 1 and a message saying why, on a pane without Claude Code, Codex, Gemini CLI, opencode, Grok Build, Cursor CLI or GitHub Copilot CLI, before the agent has written anything, and while cornercase has not found its record: Codex writes its rollout from its first turn on, and two Codex, two Gemini with the same home, or two opencode, in the same folder can’t be told apart. Read the screen then. A permission prompt or a question the agent is asking is not a message either: it is only on the screen.
Terminal window
cornercase wait --pane 12 --timeout 600 # until the agent stops working
cornercase wait --pane 12 --until waiting # until it needs an answer
cornercase wait --pane 12 --until turn-over # until its turn is over, background shells or not
cornercase wait --pane 7 --until shell # until the program ends
cornercase wait --pane 7 --text 'test result: (ok|FAILED)' # until a line matches
cornercase wait --pane 7 --quiet 5 # until it writes nothing for 5 s

wait blocks until its condition holds, then prints how it ended:

Condition Ends when Prints
(none) the agent stops working: it is idle, done, failed, pending review or waiting idle, done, failed, pending or waiting
--until idle the agent is idle, done, failed or pending review idle, done, failed or pending
--until working the agent is working working
--until waiting the agent waits for an answer to a question or a permission prompt waiting
--until turn-over the agent’s turn is over: it stops working, as with no condition, or only a background shell it started still runs (subagents still running keep it waiting) idle, done, failed, pending, waiting or shell
--until shell the program in the foreground ended and the shell is back, quiet (a second when cornercase never saw the program run) shell
--text REGEX a line on the screen matches, one already there included the line
--quiet SECONDS the pane wrote nothing for that long quiet
  • The agent conditions need a recognised agent reporting OSC 7501, or a reader for Claude Code, Codex, Gemini CLI, opencode, Grok Build, Cursor CLI or GitHub Copilot CLI in the pane. On a pane with no agent status or supported reader, wait for shell, a line or quiet instead.
  • A Claude Code agent that ends its turn while a shell it started in the background still runs counts as working: it wakes up when that shell ends, and may then carry on. So the plain wait keeps waiting, and its timeout says that only a background shell is left. --until turn-over ends there and prints shell; read the pane to see what the agent said, and stop the shell yourself if nothing should wake the agent. An agent whose own turn is over while subagents it started still run (working (2 subagents)) keeps the default, --until idle and --until turn-over waits waiting, since it wakes up for their results; the timeout names them. Codex, Gemini and opencode report no such state: their turn ends when they say so, whatever they left running, so for them turn-over ends as the plain wait does.
  • After a prompt reached the agent (through start, send --enter or keys … enter), a wait for it to stop first gives the agent up to 10 seconds to take the prompt, so it doesn’t end at once on an agent that hasn’t started yet. If the agent shows no change in that time, the wait fails: it may have finished at once, or never got the text, and read tells which. If the agent was already working when the text arrived, the wait ends when it next stops. An agent that only had a background shell left counts as taking the prompt once it works again.
  • Waits run in the server and don’t slow it down. They end with their condition, their --timeout (none by default) or when you interrupt the command. A timeout exits with status 1 and says what the pane was doing. It doesn’t prove that the agent missed what you sent: read the pane before sending it again.
Terminal window
cornercase wait --any --pane 12 --pane 15 --tab 9 --timeout 1800 # until the first of them stops
cornercase wait --all --pane 7 --pane 8 --until shell # until both programs end

Repeat --pane and --tab to wait on several panes, with --any or --all (several without either is wrong usage). The condition and the --timeout apply to each of them.

  • --any returns as soon as one of them meets the condition and prints one line: the id you gave and how it ended, such as 15 idle. When several meet it at the same moment, it prints the first in the order you gave them; wait again on the rest to hear about the others.
  • --all returns once every one of them has met the condition, and prints one such line each, in the order you gave them (--pane ones first, then --tab ones). A pane that met the condition counts as done even if it changed again before the others caught up.
  • The id on each line is the one you gave, so a --tab line starts with the tab’s id; panes and tabs never share an id.
  • A pane that closes while you wait ends as closed (12 closed) instead of failing the wait, so --all goes on with the others and --any returns it. Anything else that fails a wait on one pane (its agent exits, it doesn’t take a prompt) fails the whole wait, naming the pane.
  • A timeout says what each pane still being waited for is doing.
  • --json prints {"panes": [...]}, one entry per line of text.
Terminal window
cornercase events # everything, until the server stops
cornercase events --pane 12 --pane 14 # only these panes, until they all closed
cornercase events --host --json # also clipboard writes and bells of programs
cornercase events --notices --json # also the notices about agents, with their tokens
cornercase events --json | while read -r event; do …; done

events keeps running and prints one line per event, as it happens, so a script that coordinates agents can react to each change, such as an agent asking for an answer or a pane closing, instead of polling status or waiting again after each wait:

tab 15 opened in workspace 2
pane 14 opened in tab 15 from pane 12
pane 14 claude none → idle
pane 14 claude idle → working
pane 12 claude working → waiting
pane 14 claude working → shell
pane 7 cargo exited
pane 7 clipboard 11 bytes
pane 7 bell 3 times
pane 14 closed
tab 15 closed
workspace 2 notice claude needs you in web-shop › main cc-3f2a9c-4
Event When
pane ID AGENT FROM → TO An agent changed state: working, shell (its turn is over, but a background shell it started still runs), waiting for an answer, pending review, done while you were elsewhere, failed, idle, or none before it started and once it left the pane. Every recognised agent reporting OSC 7501, or Claude Code, Codex, Gemini CLI, opencode, Grok Build, Cursor CLI and GitHub Copilot CLI through their readers, as for wait.
pane ID PROGRAM exited The program in the foreground ended and the shell is back, or another program took its place.
pane ID clipboard N bytes Only with --host: a program wrote to the clipboard (OSC 52, such as a yank in Neovim). The text is in --json’s text, never in the line. Reads and the primary selection are not told.
pane ID bell Only with --host: a program rang the bell (a BEL character, not one that ends another escape sequence). It reads bell N times when several rang.
workspace ID notice MESSAGE TOKEN Only with --notices: cornercase told you an agent needs you, failed or finished, as the window’s toast says it, with the same rules (a second’s wait, once per change, never for a tab seen in the window or in a view). --json has token, agent, status, place, message and panes, and sound (needs-you or finished), the sound the server chose for it when sound is on for this agent (left out otherwise), so a program that draws cornercase plays the same sounds the window does. It says which sound was chosen, not that one played: a window in a plain ssh session, the browser or a machine without a player plays nothing. A --pane reader gets the notices naming its panes. cornercase go TOKEN [--view ID] does what a click on the notification does; it fails for a token the server never gave or no longer keeps (the last 64) and when the agent and its workspace are gone. Check the notices feature before sending --notices. notify, the queue’s and a failed setup’s messages are not notices.
KIND ID opened in PARENT A project, workspace, tab or pane opened. A pane started from another pane (see Ids) ends with from pane ID.
KIND ID closed A project, workspace, tab or pane closed. A pane closes when its shell exits, whatever closed it.
  • --pane ID, given once or more, keeps only what happens to those panes, and ends the command once they have all closed. Without it, events ends when the server stops (kill-server, a restart), with status 0.
  • --json prints one JSON object per line instead: event (status, exited, opened, closed), time (UTC, as in the server’s log), the project, workspace, tab and pane ids it concerns, and agent, from and to for a status (null for no agent), program for exited, kind for opened and closed, and parent for an opened pane started from another pane (left out when it has none).
  • --host is how a program that draws the panes learns what their programs ask of the terminal: a UI has no window to receive an OSC 52 write, so it puts text in the clipboard itself. Readers without --host get none of it. Bells come at most one line a second per pane, with how many rang (count). A server without the host-events feature refuses --host; check contract before sending "host": true. A reader that falls behind loses these events like any other, clipboard texts included.
  • It only tells what happens after it starts. Start it before what you want to follow, or read cornercase status once it runs, so nothing falls in between.
  • cornercase looks at the program in each pane every half second while something follows events, so a program that runs for less than that may not show. To wait for one program, wait --until shell is exact.
  • A reader that falls far behind (over a thousand events waiting for it) loses events, and gets a line such as 12 events dropped when it catches up, so it knows to read status again. One that stops reading altogether is disconnected once the server has waited a second to write to it, like any client; the command then says the connection ended and exits with status 1.
Terminal window
cornercase close --tab 9
cornercase close --worktree fix/login --remove-worktree
cornercase close --worktree fix/login --remove-worktree --check
cornercase close --worktree fix/login --remove-worktree --unlock
cornercase group new work
cornercase group style 12 --icon '★' --colour 99
cornercase group delete 12 --close-projects
cornercase move --project 5 --into 12
cornercase move --tab 9 --before 7
cornercase move --worktree fix/login --last
cornercase collapse --group 12
cornercase collapse --pane 12
cornercase expand --worktree fix/login
cornercase move-pane --pane 4 --target 7 --place below
cornercase rename 'review #42'
cornercase rename --workspace 5 ''
cornercase focus --pane 12
cornercase mark --pane 12
cornercase mark --pane 12 --reviewed
  • close takes exactly one of --pane, --tab, --workspace, --worktree or --project and stops its shells, as its × would, but never asks first, not even for a project. A worktree’s workspace stays listed while its worktree is on disk; --remove-worktree removes the worktree too and answers once it is gone. Without --force, a worktree with uncommitted or untracked changes is refused before anything stops; --force throws those changes away. A locked worktree (git worktree lock) is refused before anything stops, even with --force; the error gives the reason and the git worktree unlock command to run, and --unlock does what the dialog’s unlock and remove does. It does not imply --force: changes still need it. The branch is never deleted.
  • close --remove-worktree --check only says what removing the worktree would lose, as the Remove workspace dialog does, and stops nothing: whether it has changes that are not committed, its lock with the reason and whether the process the reason names still runs, the worktrees of a flow’s roles that go with it, and whether the project’s teardown runs first. --json prints workspace, path, branch, changed, lock (reason, path, role, pid, running, or null), roles (role, path, branch) and teardown. Run it before removing a worktree you did not just make.
  • group new NAME creates a group with the next icon and colour, as new group does, and prints its id. group style ID sets --icon (one of ● ◉ ◐ ◆ ■ ▲ ▼ ★ ✦ ♥ ♣ ♠) and --colour (a palette index: 1 9 208 214 3 11 2 10 6 14 4 12 99 5 13 205); with neither it prints the group’s style, and --json also lists both sets. group delete ID keeps the group’s projects, outside any group; with --close-projects it closes them and deletes the group once their shells have stopped.
  • move puts a group, project, workspace or tab (--group, --project, --workspace, --worktree, --tab) --before another of the same kind, or --last at the end of its list. A project moved before another joins that project’s group; --into GROUP puts it after the projects of that group and --ungroup after the ones that have none. A workspace stays in its project and a tab in its workspace. Programs keep running, and the window keeps showing what it showed.
  • collapse and expand fold or unfold a group, project or workspace (--group, --project, --workspace, --worktree), or an agent’s pane (--pane, refused with conflict for a pane that runs no agent cornercase lists), whose agents then hide under it in the agents section, whatever it was.
  • move-pane --pane ID --target ID --place swap|left|right|above|below swaps two panes of a tab or moves one to that side of the other, as dragging a pane with the right button does. It is refused (conflict) when the tab would have no room for every pane at the size it has, or when the panes are in different tabs; the moved pane becomes the tab’s active one. --json prints the tab’s new layout. Over the socket the requests are group-new, group-style, group-delete, move, collapse, expand, move-pane and removal-check, and close takes unlock; each change shows in the next status --follow snapshot.
  • rename renames the tab it runs in, or the --tab, --workspace, --worktree, --project or --group you name. An empty name goes back to the default one (a group always keeps a name).
  • focus shows a pane, tab, workspace or project, as picking it in the search does.
  • mark marks the agent in the pane it runs in, or in the --pane or --tab you name, as pending review: its tab shows a blue ● until it is marked --reviewed or the agent starts working again on a new prompt or an answer, and status and events give it as pending. A pane without Claude Code, Codex, Gemini CLI, opencode, Grok Build, Cursor CLI or GitHub Copilot CLI is refused. An agent that ends its turn with work for you to check can run it on its own pane.
Terminal window
cornercase notify 'the release build is ready'
cornercase todo add review the login fix # prints the item's id
cornercase todo list
cornercase todo done 3 # checks it off, or back on
cornercase todo undone 3 # puts a done item back among the ones to do
cornercase todo edit 3 review the login fix again
cornercase todo move 3 --before 5 # or --last
cornercase todo send 3 --pane 5 # types it into the agent and checks it off
cornercase todo rm 3 # prints a number that restore takes
cornercase todo clear-done # the same, for every done item
cornercase todo restore 7

todo does what the TODO panel does. An item moves among its kind: before an item of the other kind it stops at the end of the ones to do (or becomes the first done one). done checks an item off or back on; undone only puts a done item back, and does nothing for one still to do. rm and clear-done print an undo number (--json prints the removed items with it); restore puts that removal back where it was. The server keeps the last 16 removals until it stops, and clear-done with nothing done removes nothing and prints no number. Over the socket these are the todo request’s edit, undone, move, clear-done and restore, listed as the todo-panel feature.

notify shows a toast in the window and sends a desktop notification through the terminal of every open window, like the one for an agent that needs you.

todo send types an item’s text into an agent and presses Enter, exactly as send --enter does: the same target (--pane or --tab, else the pane the command runs in, else the one the window shows), the same refusals (an agent waiting for an answer, resuming, showing a dialog, a panel, its shell mode or its feedback survey, a program that stopped reading; --force for the ones send --force lifts) and the same confirmation that the agent recorded the prompt. Once it did, the item is checked off and the command prints sent todo 3 to pane 5. A refused or unconfirmed send leaves the item pending, prints the reason and exits 1. When cornercase cannot confirm that kind of send at all (a / command, an agent it does not read), the text is sent and the command exits 0 with sent todo 3 to pane 5, but cornercase cannot tell whether the agent took it, so it stays to do; the item stays pending, and --json has "unconfirmed": true. An item edited while it is being sent is not checked off: the command prints sent todo 3 to pane 5, but it changed while it was sent, so it stays to do and --json has "checked": false. Only a pending item can be sent: a done one is refused until it is unchecked. The TODO panel’s ▸ button does the same from the window.

Terminal window
cornercase away # agents message you on Telegram or Discord
cornercase away --off # they stop

away turns on the away mode of every chat app paired, Telegram and Discord, as Remote › continue on Telegram (or on Discord) does: while it is on, an agent that needs you or finishes its turn sends you its question or its last message, and what you reply goes to it, until you turn it off. It succeeds when at least one of them went away, and fails when none could, saying where to set it up. An agent can run it for you when you tell it you are leaving.

cornercase update checks and installs from its own process. A program that drives the server’s own update flow, such as a GUI’s update button, uses the requests update-check and update-install, the dialog’s own check and install, whose state is the update of status --app:

{"command": "update-check", "args": {}}
{"command": "update-install", "args": {}}
  • update-check asks GitHub whatever check_updates says, and answers the update object when it ends. A check already running is joined. A failure is failed with the reason, and the release found before is kept.
  • update-install installs the release found, and answers the update object when it ends, with installed true: restart to use it (restart or restart --when-idle). It is conflict when there is no release (ask for update-check first) or when cornercase can’t replace itself here (the message names the command to run). An install already running is joined, one done answers at once, and a failure is failed and can be asked again.
  • A development build, which never looks for updates, lists both as unavailable in contract and refuses them with unavailable.

There is no command for them: cornercase update does the same from a shell, and works without a server.

An agent that coordinates others may need a newer cornercase, for a command that was just released. A plain restart would stop every agent in the middle of its turn. --when-idle restarts the server only once no agent is working, in the same step it sees that, so none starts a turn in between:

Terminal window
cornercase update --when-idle --yes --timeout 3600
cornercase restart --when-idle --yes

The pane the command runs in doesn’t hold the restart, so an agent can run it as its last step: the restart stops it too, and its conversation resumes. Until then every window says a restart is pending, with a button that cancels it. cornercase restart has the details.

Terminal window
cornercase usage
cornercase usage --json
cornercase usage --cached

usage prints what the Limits tab of the Usage dialog shows, so a script or another app can read how much of your plan is left:

Claude Code · max plan updated just now
session (5h) 34% · resets in 2h 14m
week 81% · resets in 3d 4h
week · Opus 12% · resets in 3d 4h
extra usage: 12.34 USD of 50.00 USD
Codex
usage unavailable: codex did not answer in time

It asks the installed agents (Claude Code and Codex; both when neither is installed, each saying why it could not run) by running claude and codex on the server’s machine, the ones agent_commands names. That sends no prompt and costs no tokens, and each agent gets at most ten seconds. A probe already running, started by the dialog or by another usage, is waited for rather than started again. Nothing is asked between requests: a program that polls should use --cached, which answers at once with the last answer and its age and asks nothing. The command needs a running server, since the probes and their last answers live there. It exits 1, after printing every agent, when one could not be asked.

With --json it prints {"agents": [...]}, one entry per agent with every key present (null when it has no value): agent and title, loading (a probe is running now), read_at (UTC) and age (seconds) of the last answer with numbers, plan, limited (false means the account has no plan limits, null that nothing was read yet), limits, extra (Claude Code’s extra usage or Codex’s credits, as text), notice (what the dialog shows instead of bars, such as not signed in · run codex login) and error. After a failed probe, plan, limits and the rest are from the answer before it, as old as age says. A limit has label (the dialog’s text), kind (the agent’s own name for it: Claude Code’s session, weekly_all or weekly_scoped, or Codex’s primary or secondary), scope (the model or bucket it applies to, null for the whole plan), percent, severity (normal, warning or critical; read it leniently) and resets_at (UTC, or null). kind and scope together name a limit from one answer to the next. With accounts, an agent has one entry for its usual login and one per account, which adds account (its folder name) and label (the name you gave it, or System for the usual login); the entry new tabs use (the usual login’s when that agent uses no account) has active: true. These keys are left out when there is nothing to say, so an answer without accounts is the same as before.

It is the request {"command": "usage", "args": {"cached": false}}. An older server answers that it has no usage command.

Terminal window
cornercase stats
cornercase stats --json
cornercase stats --share

stats prints the parallel-work headline, the 24 h / 7 days / total table and a text heatmap (·░▒▓█). With a server it includes numbers still in memory; without one it reads stats.json. An older server that has no stats command says to update cornercase and restart it.

--json prints an object with since (UTC), windows (24h, 7d, total) and blocks (newest first). Each window has agents, prompts, turns, agent_seconds, busy_seconds, most_at_once, waited_seconds, tokens (in, out, cache_read, cache_write) and by_agent (claude, codex, gemini, opencode). Durations preserve milliseconds as fractional seconds. Busy time and most-at-once span all kinds; the per-agent counters contain starts, prompts, turns, working/waiting durations and token kinds. Every key is present, even when zero. Each block gives agent_seconds. merged_pull_requests is null unless the server has read your merged pull requests (see Usage and stats); otherwise it has 24h, 7d, total, complete (false when a count is a lower bound) and accounts (provider, host, login, error). The text form adds a merged PRs row, with a + after an incomplete number; a server is needed for it.

--share prints the same share link as Usage → Stats → share. Copy and post it yourself; anyone with the link sees the numbers on the card. See Usage and stats for the counting rules and privacy.

Terminal window
cornercase config
cornercase config --json
cornercase config set sidebar tree
cornercase config set fetch_minutes 10 counts false
cornercase config set issue_tabs github,linear
printf '%s' "$LINEAR_API_KEY" | cornercase config secret linear
cornercase config remove linear
cornercase config project ~/code/web-shop --setup 'npm ci' --teardown 'rm -rf node_modules'

config lists every setting of the Settings dialog, the connections (accounts and bots) and each project’s setup and teardown. A setting that the dialog shows as a row has a key, the name of its config.json key (an agent’s mode and extra arguments are agents.claude.mode and agents.claude.extra, and the issue tabs are one list, issue_tabs), so a program can draw a form from the answer instead of knowing each row.

set takes KEY VALUE pairs and changes them as the dialog does: the same checks, the same live effects (browser access starts or stops, keep awake takes effect, the layout changes) and the same notices. Either every value is accepted and saved, or nothing is. On the command line the value is converted by the setting’s type: true or false (also on, off, yes, no) for a switch, a whole number, a comma-separated list for list, otherwise text.

A token is never an argument. secret KEY reads it from standard input (hidden on a terminal), checks it with its service exactly as the dialog does (a tracker’s token check, a bot’s getMe), saves it and answers with the account, never the token. remove KEY forgets a token, a bot, a paired chat (telegram.chat, discord.chat) or the paired browsers (browsers). A token that comes from an environment variable cannot be replaced or removed from here, as in the dialog. Pairing a phone stays in the window: it needs its QR code.

project sets what a project’s folder runs when a worktree is made and before one goes; --project ID or the folder names it, and an empty text removes the commands. Only a repository’s root has worktrees, so a folder that isn’t one is refused.

Over the socket, every answer has the same shape and the request is {"command": "config", "args": {"action": "get"}} (or "set", "project", "secret", "remove", each with its entry):

{"command": "config", "args": {"action": "set", "entry": {"fetch_minutes": 10, "sidebar": "tree"}}}
{"command": "config", "args": {"action": "project", "entry": {"path": "~/code/web-shop", "setup": "npm ci"}}}
{"command": "config", "args": {"action": "secret", "entry": {"key": "linear", "value": "lin_api_…"}}}
{"command": "config", "args": {"action": "remove", "entry": "linear"}}
  • settings: for each one, key, page and section (where the dialog shows it), label, type (switch, number, text, choice, list or key), value and default (as JSON), shown (what the dialog shows as its value), note, choices (for a choice and the list: value, note, dangerous), field (the label of its edit field), question (the title of its pick), dangerous and restart. restart says the change only applies after a restart; none of today’s settings does. set takes strict JSON types: true, a number, a string, a list of strings.
  • connections: key (shortcut, linear, jira, plane, gitlab, asana, browsers, telegram, telegram.chat, discord, discord.chat), kind, label, note, state (missing, connected or refused), account (who it is connected as), from (saved or env) and env for a token from the environment, count for the paired browsers, field (what to paste) and removable. A token or a bot is set with secret, never through set.
  • projects: folder, key (as written in config.json), project (the id of the open project in that folder), repository, setup and teardown.
  • revision, path (the config.json file), problem (why it cannot be read, when it cannot) and notices: what the dialog would have said for each change made.

revision goes up when a setting, a project’s commands or a connection changes, however it changed (the dialog, a request, or an edit of the file), and status --json, status --follow and view lines carry it as config.revision: a program that mirrors the session reads the settings again when it grows. Notes that change by themselves, such as the keep-awake state, don’t move it.

A refusal has a code: not-found for a key that names no setting or connection, malformed for a value of the wrong type or one the dialog’s check refuses (the message is the dialog’s), conflict for what the current state blocks (a token set by an environment variable, Jira’s site and email missing, a folder that isn’t a repository, a config.json cornercase cannot read), and failed when the service refused a token. Changes made through set never write over a config.json that cannot be read.

Terminal window
cornercase plugin actions
cornercase plugin actions --context pane --json
cornercase plugin run acme.notes note --pane 5
cornercase plugin run acme.notes note --worktree feat/x

plugin actions lists what the plugins you turned on add to the menus, in the order the menus show them (your plugins in the order of plugins.json, then each manifest’s actions), for one kind of row (--context project, workspace, tab or pane) or all of them. The text is a tab-separated table under a header, plugin, action, contexts, title, so cut and awk can read it.

plugin run runs one on what you name (--pane, --tab, --workspace, --worktree or --project) the way its menu entry does: in the workspace’s folder (the project’s for a project), with the ids of what you named in the environment, stopped after 5 minutes, its output in the plugin’s log. It waits for the action, prints how it ended and exits with 1 when it failed. Nothing changes in the window: no toast, no focus. A workspace being removed or closing, and a project closing, are refused, since the action would run in a folder being deleted.

Actions are named by plugin id and action id, and listed with a revision that goes up each time the plugins change (counted from the server’s start, so send boot too). plugin run refuses a revision the plugins moved past, conflict, as a menu opened before the change does nothing, so an action whose command or contexts changed since you listed it never runs by surprise. Left out, --revision is the current one, which plugin actions --json also prints.

The requests behind them are plugin-list, plugin-actions and plugin-run. {"command": "plugin-actions", "args": {"context": "pane"}} answers revision and actions (plugin, name, action, title, contexts). {"command": "plugin-run", "args": {"plugin": "acme.notes", "action": "note", "revision": 3, "item": {"pane": 5}}} takes every field, the item as close and focus do, and answers when the action ends:

{"plugin": "acme.notes", "action": "note", "context": "pane", "project": 1, "workspace": 2, "tab": 3, "pane": 5,
"ended": "exited", "code": 0, "signal": null, "error": null, "ms": 41, "log": "/…/plugins/state/acme.notes/plugin.log"}

ended is exited (with code), signal (with signal), timeout or error (it could not start); error is null exactly when it exited with 0. An action that ran and failed is an answer, not a refusal. A group, or an action run on a kind of row it isn’t offered for, is malformed; a plugin that is unknown, off or cannot run, an action it lacks, or an id that names nothing is not-found. A client that leaves before the action ends gets no answer, and the action runs to its end.

plugin-list answers what Settings › Plugins shows: revision, problem (why plugins.json cannot be read, or null) and plugins, one row per plugin with id, enabled, state (on, off or cannot-run), error, Settings’ label and note, source, log, manifest and services. A service is running (with its pid), stopped (with restart_in, the seconds until it starts again) or starting. Processes a plugin keeps running in each local window belong to those windows, so the server can’t tell. Turning plugins on and off, installing and updating stay with cornercase plugin, which works on the files. An older server refuses each of the three by name.

--json prints a command’s answer as JSON. These fields are kept stable; new ones may be added.

Command Fields
status version, boot (which start of the server this is, see Contract and compatibility), caller (your pane’s id, or null), shown (the ids of what the window shows), groups (id, name, collapsed, icon, colour), projects (id, name, custom_name, path, group, active, collapsed, workspaces), each workspace (id, name, custom_name, path, branch, worktree, behind, pull_request, run_dir, run, active, collapsed, tabs), each tab (id, name, custom_name, status, active, layout, panes), each pane (id, path, program, agent, status, background_shell, dialog, survey, at_prompt, model, context, active, caller, resumes, resuming, title, parent, subagents, role, held_sends, draft, and collapsed, there only while it is true). held_sends is how many messages send is holding until the person in that pane is done typing, and draft is whether that pane has text a person typed and has not sent yet (see Talk to a pane). subagents is how many subagents the agent started that still run (a number, 0 for shells and agents without a reader); role is the role the pane plays in a flow run (orchestrator, coder…), or null outside one; title is what the program last set as its title (OSC 0 or 2), as a terminal tab shows it, or null; it can change several times a second while an agent works, and status --follow sends it at most every half second. pull_request is null, or the pull request of the workspace’s branch: number, title, url, state (open, draft, merged or closed), checks (failing, pending, passing, or null without checks) and failed, the names of the checks that failed, conflicts (true while GitHub says it has merge conflicts with its base; its pull_request workflows don’t run until they are resolved, so checks covers only what ran) and base, the branch it merges into, or null. run is there while the workspace runs a flow: end (pull_request or commits, what the flow delivers) and reached (for pull_request, true once the branch has a pull request that is open, a draft or merged, or once the orchestrator marked its own pane for review; for commits, only that mark counts. The mark counts until the worktree moves to another branch). Groups, projects, workspaces and tabs come in the order of the sidebar. colour is a group’s palette index (0-255) and icon its symbol; custom_name is the name given in cornercase, or null when the item takes its folder’s, branch’s or program’s; collapsed is whether a project or workspace is folded. layout is how the tab’s panes are split: {"leaf": PANE}, or {"split": {"dir": D, "ratio": R, "first": …, "second": …}}, where dir is right (side by side) or down (one above the other) and ratio is the share of the first side, from 0 to 1. active is whether it is the active one of its parent; status is working, waiting, pending, done, idle or null; background_shell is true while the agent’s turn is over but a shell it started in the background still runs (its status stays working); dialog is true while a Claude Code agent shows a dialog, a panel or its shell mode instead of its input box; survey is true while it shows its feedback survey above its input box; resumes is true when the pane’s agent conversation comes back after a restart; resuming is true while it is coming back, and the pane takes no input; parent is the id of the pane it was started from (by start, new-tab or split run inside that pane, or a flow’s orchestrator), or null when it has none or that pane closed; at_prompt is whether the pane’s shell sits at its prompt, with nothing else running in it, so it is false whenever an agent runs, whatever the agent shows. While keep awake is on, keep_awake has held (whether the computer is being kept awake now), by (the tool holding it) and unavailable (why it can’t be, on this machine); see Keep the computer awake. theme says where the colours of the terminals’ programs come from: from and scheme (see Colours for programs). config is {"revision": N}, which goes up whenever a setting changes.
status --follow One object per line: what status prints, plus revision.
status --app revision (with --follow), version, boot, todos, queue, away, update and keep_awake, see Follow the rest of the app.
status --panes panes: one object per pane, with the columns of --panes as keys, in their order: pane, tab, workspace, project, program, agent, status, background_shell, dialog, at_prompt, model, context, caller, shown, active, tab_name, workspace_name, branch, worktree, project_name, path, survey, resuming, parent, subagents, role, held_sends, draft. Every object has every key, null where there is no value.
open, new-workspace, new-tab, split, start project, workspace, tab, pane: the ids of what was made (for open, the project’s shown workspace, tab and pane), and projection when --focus --view moved a view.
focus --view project, workspace, tab, pane the view shows now, and its projection.
send, wait pane, and, once a wait ended, ended (idle, done, pending, waiting, working, shell, text or quiet) and line for a --text match. start --wait adds ended and line too.
wait --any, wait --all panes: one entry per line of text, each with pane, tab when it was given as a --tab, ended (as above, or closed) and line for a --text match.
flow list flows (each with name, origin, path, description, roles, problems, check, flow and edited), broken, hidden (the deleted built-in flows, like flows) and builtins. See Manage flows.
flow run run (the run’s id), project, workspace, branch, members and run_dir.
flow delete, flow hide, flow restore name, origin, deleted, hidden, changed.
flow runs version, boot, runs (with --follow, each line also has revision), see Follow a flow run.
flow messages One object per line: run, workspace, seq, time, from, to, kind, text, bytes, edge.
flow files workspace, run, dir, files (name, path, bytes, modified) and more.
flow-save (a request) name, origin, path, saved, problems, check.
usage agents: agent, title, loading, read_at, age, plan, limited, limits (label, kind, scope, percent, severity, resets_at), extra, notice, error. See Plan usage.
plugin-list (a socket request) revision, problem and plugins: id, enabled, state, error, label, note, source, log, manifest and services (id, state, pid, restart_in). See Plugins.
plugin actions revision and actions: plugin, name, action, title, contexts.
plugin run plugin, action, context, the ids it got (project, workspace, tab, pane), ended, code, signal, error, ms and log.
search matches: pane, tab, workspace, project, place, who, source (prompt, reply or screen), line, text, written; and capped. See Find where something was said.
read pane and text. With --last-message, also agent (claude, codex, gemini or opencode), written (when the agent wrote it, in UTC, such as 2026-10-08T19:56:37.241Z) and turn_over (true once the agent stopped working, as --until turn-over sees it; missing while cornercase doesn’t know yet).
read --last-message with several targets panes: the individual read --last-message answers above, or entries with error and the requested pane or tab id.
events One object per line, see Follow events: time, event, project, workspace, tab, pane, and agent, from, to, program, kind, parent (for an opened pane started from another pane), token, status, place, message, panes (for notice), text (for clipboard) or count (for bell and dropped) depending on the event.
changes One object per line, see Follow a workspace’s changes: boot, stream, revision, scope, mode, state, error and workspaces.
changes-mode stream, scope, workspace and mode.
changes-file workspace, mode, the file (path, old_path, status, added, removed, fold, viewed, digest), styles and hunks.
changes-gap workspace, path, hunk, styles and lines.
changes-base workspace, base, default and branches.
changes-viewed workspace, path and viewed.
surface One object per line, see Stream a tab’s terminals: boot, session, surface, revision, tab, cols, rows, layout, dividers, styles, hyperlinks and panes; for --view, also view and projection. With --patches, later lines carry base and only the changed panes.
view One object per line, see A view of your own: what status --follow prints, plus view (id, projection, active, cols, rows, shown, controls, visible) and each tab’s controller.
view-set The view: id, projection, active, cols, rows, shown, controls, visible.
go The ids it went to (project, workspace, tab, pane), and projection with --view.
input pane, bytes (what was written) and skipped: event (its position from 0) and why (mouse, release, or key for a press cornercase has no encoding for) of each event that was not sent.
scroll pane and scroll: total, offset and rows, as in a frame.
selection pane and text (absent when nothing is selected).
resize surface, tab, cols and rows.
redraw surface and tab.
link pane and link: null when the cell holds no file, else path (canonical), inside (in the pane’s workspace), workspace, relative (inside only), lines (first and last, absent without lines), and row, start and end, the cells the path takes (end excluded).
ratio, move-pane tab and layout, the tab’s split tree as status --json gives it.
group-new group, the new group’s id.
group-style group, icon, colour, and icons and colours, the sets to choose from.
removal-check workspace, path, branch, changed, lock, roles and teardown, as close --check --json prints them.
todo list todos: id, text, done.
todo rm, todo clear-done todos: the items removed, and undo, the number todo restore takes (none when nothing was removed).
todo send pane, as send gives it, todo, the item checked off, and checked (whether the item was checked off) and unconfirmed (true when it was sent but stays pending).
config revision, path, problem, settings, connections, projects and notices: see Settings.
contract version, contract, boot, operations (name, available, and reason when it is not), features and streams (name, version). See Contract and compatibility.
Status Meaning
0 Done.
1 An error or a timeout; the message says which.
2 Wrong usage, such as an unknown option.

Some messages you may meet:

Message Meaning
no cornercase server is running Start cornercase first, or cornercase start-server without a window; the other commands never start one.
the running cornercase server is too old for cornercase … The server comes from a release without these commands, or without this one. Restart it: cornercase kill-server (it closes all its terminals), then cornercase.
the running cornercase server (…) has no … command The command is newer than the server. Restart the server so it runs the version you installed.
this cornercase server has not started its session yet … A server started without a window has no size to give a new terminal, and hasn’t restored your session yet. Run cornercase start-server, or open a window with cornercase.
the agent in pane … is waiting for an answer … send refused to type into an agent’s question or permission prompt.
the agent in pane … shows a dialog, a panel or its shell mode … send refused to type into a Claude Code dialog, panel or shell mode. Read the pane, close it and send again, or pass --force.
the agent in pane … shows Claude Code's feedback survey … send refused to type while Claude Code’s survey would take a digit as its answer. Dismiss it with cornercase keys --pane … 0 and send again, or pass --force.
pane … is resuming its agent's conversation … send refused to type while the pane’s agent comes back in its conversation after a restart. Wait until status no longer shows it resuming, or pass --force.
not confirmed: the prompt may not have been submitted … send --enter pressed Enter, but the agent recorded no new prompt. Read the pane before sending it again.
the … agent in pane … has not written a message yet read --last-message found the agent’s record, with no message in it.
the program in pane … is not reading its input … Over 16 MiB sent to the pane are still waiting for its program to read them, so send and keys stop adding more.
the program in pane … is not reading its input … so nothing was sent input found over 16 MiB waiting for the program. With a code it is busy.
pane … is … now; read its latest frame and try again and other conflict messages The terminal’s size changed since your frame, a cell is outside it, the pane’s program scrolls itself, or nothing is selected to copy. Nothing was done.
timed out after … The --timeout passed; the rest of the message says what the pane was doing.
the restart was cancelled in the window … Someone pressed cancel on the window’s notice while restart --when-idle waited; the server keeps running.
cornercase hit a bug, see server.log cornercase hit a bug while it handled the command or waited for it. Your shells keep running; see this message in troubleshooting.

The commands and the server don’t have to come from the same release: after cornercase update, a command of the new version talks to the server still running the old one, and the other way round. A program of your own, such as another UI for cornercase, can rely on the same rules, through the commands or straight through the socket.

Terminal window
cornercase contract
cornercase contract --json
cornercase 0.12.137
contract 1
boot 4242-1700000000
commands 78 (78 available)
stream events 1
stream watch 1
stream surface 1
stream view 1
stream app 1
stream changes 1
stream flow-runs 1
stream flow-messages 1
features boot, structured-errors, strict-fields, views, host-events, theme, notices, surface-patches, config, close-unlock, sound, issues, todo-panel, changes, agent-folds, accounts, account-login, account-switch

--json prints an object with:

  • version: the server’s release.
  • contract: the version of the rules on this page, an integer starting at 1. It goes up only when something that was there changes meaning or shape, never when something is added.
  • boot: which start of the server this is. It changes on every start, cornercase restart included, since ids start again from 1. It is the same value as CORNERCASE_SERVER in the panes and boot in status --json.
  • operations: every request the server takes, by name, with available. A server with start-server among them can be started without a window; one without it needs a window before it opens terminals. One that is not available on this server has available: false and a reason; the others keep working.
  • features: what a request can ask for (below): boot, structured-errors, strict-fields, views (the view field and --view; a client checks it before sending a view to a server), host-events (the host field of events, which an older server would ignore; a client checks it before sending it) theme (the theme request and start-server’s theme; check it before sending colours), notices (a view’s visible, the notices field of events and the go request; check it before sending them), surface-patches (the patches field of surface; an older server ignores it and sends complete lines, so check it before asking) config (the config request and config.revision) close-unlock (the unlock field of close, which an older server ignores and then refuses with the lock’s message; check it before sending it), sound (a notice’s sound field: an older server never sends it), issues (the issue and source fields of start and flow-run, which an older server ignores and then starts the agent without the issue; check it before sending them), todo-panel (the todo request’s edit, undone, move, clear-done and restore; an older server refuses them as malformed, so check it before sending them), changes (the changes requests and the changes count of status), agent-folds (the pane item of collapse and expand; an older server refuses it as malformed, so check it before sending it), accounts (the account-* and profile-* requests and the profile field of start, start-with, new-tab and flow-run, which an older server ignores and then starts the agent with its own login; check it before sending it), account-login (the check field of account-list and each account’s login; an older server ignores check and answers without asking, so check it before sending it), and account-switch (the account-rename and account-use requests and the accounts field of start, start-with, new-tab, flow-run and the queue’s add and edit, which an older server ignores and then starts with the accounts in use; check it before sending it).
  • streams: the formats of what the server streams, with their versions: {"name": "events", "version": 1} for events {"name": "watch", "version": 1} for status --follow {"name": "surface", "version": 1} for surface and {"name": "view", "version": 1} for view, whose lines are watch format 1 plus view, and {"name": "app", "version": 1} for status --app, {"name": "changes", "version": 1} for changes, and {"name": "flow-runs", "version": 1} and {"name": "flow-messages", "version": 1} for flow runs and flow messages.

A server from before contract says the running cornercase server (…) has no contract command, or that it is too old: it knows none of the rest of this section, so don’t send it ids you kept from before.

  • Fields are only added, never renamed or removed. Read answers leniently: ignore keys you don’t know, and don’t depend on their order (events lines come out with their keys sorted).
  • A new behaviour that must not be ignored gets a new request name, which an older server refuses with has no … command rather than doing something else.
  • contract goes up when something that was there changes. Additions show in operations, features and streams instead.
  • The format of each stream has its own version. Format 1 of events is the one described in Follow events, format 1 of watch the one in Follow the whole state, format 1 of surface the one in Stream a tab’s terminals, format 1 of app the one in Follow the rest of the app, format 1 of changes the one in Follow a workspace’s changes.

A request through the socket is JSON: {"command": "status", "args": {}}, its args named after the command’s options; the repository’s tests/fixtures/control/ holds examples of what goes over the socket. It is answered with {"ok": …} or {"error": "…"}. A request that also carries "contract": 1, or "boot": "…", gets errors with a code instead:

{"error": {"code": "not-found", "message": "there is no pane 9; `cornercase status` lists them"}}
Code Meaning
unsupported The server has no such request, or the request’s contract is newer than the server’s. Nothing was done.
malformed The request is not one: a plugin action run on a group or on a kind of row it isn’t offered for, its arguments don’t have the expected types, a setting’s value is of the wrong type or one the Settings dialog refuses, or it names a view and takes none (`stats` takes no view).
stale-boot The request’s boot is not this server’s: it restarted since, and the ids you hold may name other panes now. Nothing was done.
not-found A plugin that isn’t on or an action it doesn’t have, no group, project, workspace, tab, pane, view, surface or changes stream has that id, a file has no change or a hunk no gap, changes-mode names a stream another connection opened, a branch doesn’t exist, no setting or connection has that key, or the tab has no divider at that path. A view is gone once the connection that opened it closes.
unknown-field A field this server doesn’t know, named by its path, such as args.bogus. Nothing was done.
unavailable The request is listed in operations with available: false, with the reason as its message. A development build turns off update-check and update-install; a release turns off none.
busy The pane’s program isn’t reading its input: 16 MiB are waiting for it. Only input answers this; send and keys keep answering failed. Nothing was sent.
conflict The request doesn’t fit the current state: a plugin action listed at another revision of the plugins, the pane’s terminal has another size, a cell is outside it, the program scrolls itself, nothing is selected to copy, or, for update-install, no release found or a cornercase that can’t replace itself, a workspace for changes isn’t in a git repository, a connection already follows changes, or, for config, a token set by an environment variable, a project folder that isn’t a repository or a config.json that cannot be read; or a row moved before one of another workspace or project, a pane moved to another tab, or where its tab has no room; or an issue of a tracker that is not connected, a place to choose for an issue that isn’t tied to a repository, or tracker settings that changed during the lookup. Read the latest state and try again. Nothing was done.
failed Anything else.

The message is the text the command would print. Text that is not JSON at all gets the plain form, {"error": "…"}, since the server can’t tell what the request asked for. Without contract or boot, a field the server doesn’t know is ignored, as it always was; with them it is refused, so a newer option never passes for done on an older server. Either one set to null counts as not sent. A contract newer than the server’s is refused with unsupported, so a client written for later rules learns it at once instead of being read under older ones. The cornercase commands themselves send neither, so what they print doesn’t change.

With boot set to the value from contract or status, a request kept across a restart is refused before it acts on an id the new server gave to something else.

The socket takes one request per connection. Each message is framed as its length, 4 bytes little-endian, then its postcard encoding: a request is variant 4 followed by the JSON text (its length as a varint, then its UTF-8 bytes), an answer variant 5 in the same form. events sends one answer per event and ends with variant 2, 3 or 4 (its panes closed, the server stopped, or it restarted); watch, the request behind status --follow, answers with the state and then sends it again in the same form after each change, until variant 3 or 4, and app, the request behind status --app --follow, does the same with the rest of the app’s state; surface does the same with a tab’s frame, and ends with variant 2 when the tab closes (or its view does); view sends the state, with the view in it, and holds the view open until the connection closes. A request for a view carries its id as view next to command, once contract lists the feature views. Only your own user can connect. Nothing needs to be sent before the request.

An agent only knows what is in its context, so it doesn’t know about these commands until you tell it. cornercase ships a short set of instructions as an agent skill (a SKILL.md in the repository); install it for Claude Code, Codex and other agents with:

Terminal window
npx skills add usecornercase/cornercase-terminal --skill cornercase -g

Only the skill’s name and description stay in the agent’s context; the rest loads when a task needs it. cornercase skill prints the copy that matches your version; for an agent without skills, put that text in your AGENTS.md or CLAUDE.md. cornercase never installs it by itself, for the same reason it installs no hooks: it doesn’t write into your agents’ setup.

The instructions tell agents to use the commands only when you ask them to open tabs, run something in another pane, or start and coordinate agents; to learn the syntax from cornercase --help; to find panes with status --panes rather than walking the tree; never to steal focus, run kill-server or close what they didn’t open; not to answer another agent’s permission prompt without asking you; to read a pane before sending again after a timeout or a prompt that was not confirmed; to keep their --timeout under their own command timeout; to read several agents’ answers in one read --last-message call; and to restart or update cornercase only when you ask, with --when-idle.

Variable Use
CORNERCASE 1 in every pane. The commands work there; only opening the window inside cornercase is refused.
CORNERCASE_PANE The id of the pane a shell runs in. The commands use it to know where they run.
CORNERCASE_RUN_DIR In the panes of a flow run only: the folder where the team keeps its files.
CORNERCASE_SERVER Which server started the shell, the boot of contract. A server only takes CORNERCASE_PANE as the pane a command runs in when this is its own, since every server numbers its panes from 1.
CORNERCASE_SOCKET Which server to talk to, as for every cornercase command. Panes don’t get it: a shell inherits the server’s environment, so cornercase in a pane finds its own server anyway, and setting it would also move config.json and the session (see Files and environment).

As before, only your own user can reach the server’s socket. But whoever reaches it can type into every shell, and change your settings and tokens, so a coding agent that can run cornercase can run anything in any pane, and run commands outside its own sandbox through another pane. Keep the socket out of reach of agents you sandbox: run them in a container or under another user without access to it, or don’t give them a shell inside cornercase.

cornercase queue add --at 'in 2h' --agent claude --prompt 'Fix the login form' schedules work and prints its persistent id. Use --issue URL_OR_KEY for an issue, --workspace ID for a tab, or --worktree BRANCH for a worktree. --at 'claude reset' and --at 'codex reset' use the next five-hour plan reset. queue list --json, queue edit ID --at TIME, queue remove ID and queue run ID inspect and manage it. See Queue agents for formats and flags. Queued launches accept folder trust prompts automatically.