Quick start
These four steps take you from a project folder to work you can check and undo.
-
Open a workspace. Run
cherrypiin a project folder. The launch screen shows where the data folder will be before anything is made (Open a workspace). - Ask Picache. Picache is the agent you talk to in a Chat. Type in the prompt band above the status line and press Enter. Alt+Enter steers a turn that is already running, and Esc stops it (Send, steer or stop).
- Launch a vendor agent. Pick one in the rail, such as Claude Code or Codex. It starts in a terminal pane, inside a box that limits what it can reach (Launch a vendor agent).
- Check and undo. Every Picache turn takes a snapshot of the folder before it starts and after it ends, so you can see what it changed and put the folder back (Undo a turn's changes).
picache snapshots changes turn:<id> # what a turn changed
picache layer restore turn:<id>:start # the folder as that turn found it
When the status row shows N NEEDS YOU, something is
waiting for your answer (When something needs
you).
The workspace
A workspace is a folder that cherry π manages, with its work split into layers.
cd ~/src/api
cherrypi # open the workspace here, or make one
The program has two halves. The harness,
cherrypi.ape, is a Lua runtime built on redbean. It
decides what is allowed and keeps every record. The
desktop is a Go terminal program that draws
panes.
The harness keeps running when you quit the desktop, and the next launch reconnects to it.
Each project layer is bound to a subfolder, with its own Picache conversation and its own panes. Layer Zero is a scratchpad. A layer can also be a git worktree, and removing it never deletes its branch.
Panes hold shells, vendor agents, files, editors, runs, models and more. You can split, stack, zoom or move them.
What runs where
Four kinds of process make up cherry π.
- The desktop is the Go program you launch. It draws panes and makes no decisions.
-
There is one harness per workspace. It decides what is
allowed, keeps the records and runs the work. Desktops attach
to it on
run/harness.sock. - Each terminal has a pump. The pump holds the terminal and its recent output, so a shell or an agent keeps running if the desktop or the harness goes away.
- Sidecars run beside the harness. The signer holds the keys, the P2P sidecar talks to the network, and model servers run as jobs.
A program in a pane talks to the harness with the
picache command (section 04),
over picache.sock. Only you can read that socket.
Every call carries its pane's token, which names who is calling
(section 03).
Anything confined, such as an app's handler, a script or a clean room, starts as a capsule through one door in the harness (section 09).
One file
cherrypi.ape is one file that carries everything it
needs for macOS on Apple silicon and Linux on x64 and arm64,
including the Raspberry Pi. When you run it at a terminal, it
starts this machine's desktop.
The desktop is inside the file too. It is built natively for macOS and statically linked for Linux, and it draws with the Ghostty terminal library.
On Linux, nothing the file carries needs a system loader, so it runs on NixOS. The file is in the Actually Portable Executable (APE) format, and on a Linux machine with no APE loader it writes its own. The same commit builds the same bytes from any checkout.
Open a workspace
Run cherrypi in a folder. It looks upward for a
.cherrypi/, the way git looks for .git,
and stops below your home folder, so the home workspace,
~/.cherrypi, never swallows every project under it.
The launch screen offers the workspace in this folder or above it, a new workspace here, another folder, the home workspace and recent workspaces. Each choice shows where its data folder will be before anything is created. Then you pick the latest conversation, another one, or a new one.
Flags let you skip these questions:
cherrypi -state ~/src/api/.cherrypi # this data folder
cherrypi -session <id> # this conversation
cherrypi -private # a harness of its own, ended with the desktop
cherrypi -remote studio:web # a paired machine's workspace
cherrypi -remote studio:web:L1 # ... one layer of it
cherrypi -remote studio:web:terminal:<id> # ... one terminal
cherrypi -stop-harness # stop this workspace's harness
Inside cherry π, Open does what -remote does, and
shows the result in a pane (section 08).
Each data folder carries a marker that says it belongs to
cherry π. When it picks a data folder, cherry π only takes one
that is empty, already marked, named .cherrypi or
already holding its database. It refuses any other folder that
holds files, and a data folder or marker that is a symbolic link.
-
In a git work tree, it offers to add the folder to
.git/info/exclude, which is never committed. - Layer folders are stored relative to the workspace, so you can move the whole tree.
- You're warned about synced folders such as iCloud Drive and Dropbox, because SQLite is fragile there.
Deleting a project folder deletes its history too, because the data folder is inside it. Settings warns you about this, and cherry π itself never deletes a data folder.
The data folder
A workspace keeps everything it knows in its data folder. What
belongs to you or to the machine lives in the home data folder,
~/.cherrypi.
Your project's own files hold only what you put there, such as a saved layout or an agent you declared for a folder.
project/.cherrypi/
.cherrypi-workspace-v1 the ownership marker
cherrypi.db the workspace: desktop, grants, settings,
tasks, offers, its evidence chain
layers/<id>.db one store per layer: its conversations,
runs, receipts and its own chain
scratchpad/ Layer Zero's folder
trash/ what Files deleted
run/harness.sock where desktops attach
run/tty/ one socket per terminal pump
picache.sock where picache calls arrive
harness.log what the harness said
apps/ exports/ logs/ backup-…/
~/.cherrypi/ the home data folder, and the home workspace
identity.db who this machine is; the keys stay in the signer
p2p/p2p.db pairs, contracts, the fleet
p2p/rooms.db rooms and their messages
ai/models.json models, referenced where they are
preferences.json theme, glyphs, keys: yours, in every workspace
workspaces.json recent workspaces, for the launch screen
floats.json where your floats were
Layers
Each layer has an id that survives renames and moves, and a SQLite store of its own. The store holds the layer's conversations, runs and receipts, and its own evidence chain, which is anchored in the workspace's chain (section 10).
Because a layer's history is in its own file, a layer can leave the workspace whole. It can be exported, shared, forked or kept in step with another machine.
Export and import a layer
picache layer export L1 --out api-layer.db # history and folder
picache layer export L1 --history-only # history alone
picache layer export L1 --git # include .git
picache layer import api-layer.db # as a working layer
picache layer import api-layer.db --as reference
An export is one SQLite file, signed by this machine. It holds the layer's history, its evidence chain and its folder: files of any size, empty folders, symbolic links that stay inside it, modes and modified times.
Files are split into chunks of up to 4 MiB and stored by SHA-256 hash, so a chunk that many files share is stored once. The signature covers the hashes, and every chunk is checked as it's read, so an import refuses a chunk that has been changed.
On a Mac, exporting 2 GiB plus 2,000 small files took 25 seconds. Verifying the export took 9 seconds and importing it took 23, and memory use peaked at about 106 MB.
An export leaves out .git unless you pass
--git. It also leaves out folders that can be
rebuilt, such as node_modules, .venv
and target. You can set your own list with
layer.export_excludes. The archive lists everything
it left out.
An import as a working layer writes the files to a new folder and opens the conversation it had in front. An import as a reference is read-only. Importing an archive signed by a machine you haven't paired asks you first.
Grants (section 03) never travel. They stay in the workspace that gave them.
Share or fork a layer
picache layer share L1 studio # offer it to another workspace or machine
picache layer fork L1 # a copy beside it, under new ids
When you share with a paired machine, up to 10 MiB travels with the offer. Anything larger goes through your own Drive, which needs no cloud and no account.
Each chunk goes to the Drive once, and the receiver fetches only the chunks it doesn't already hold. The share command says how many chunks were new. If an accept stops part way, it picks up where it stopped.
A fork copies the folder, history and conversations. Nothing the fork does reaches the original, and both evidence chains record the fork.
You can also attach any layer or reference to the layer in front as a brain. Picache and agents can search a brain, and nothing writes to it.
Undo a turn's changes
Every Picache turn takes a snapshot of the layer's folder before it starts and after it ends.
picache snapshots # list them
picache snapshots changes turn:<id> # what a turn added, removed, changed
picache layer restore turn:<id>:start # the folder as that turn found it
Snapshots are stored as hashed chunks inside the layer's store, so a turn that touched one file costs the space of one file. A restore takes a snapshot first, so you can undo the restore. It also tells you whether the result is exact.
Snapshots never touch .git or rebuildable folders.
Files over 32 MiB are listed by name, but their contents are not
kept. When you export or fork the layer, its snapshots go too.
On cherry π's own source repository (1,296 files, 421 MB), the first snapshot took 2.65 seconds and the next one 0.02 seconds.
| Setting | What it does |
|---|---|
layer.snapshots | Turns snapshots on or off |
layer.snapshot_max_file | The largest file kept |
layer.snapshot_keep | How many to keep (1000 by default) |
Share a layer live
A live layer stays the same on several machines. Each member's edits and conversations reach the others within a few seconds.
picache layer live L1 studio # offer L1, live
picache layer live L1 # list the members
picache layer live L1 --remove laptop # owner only
picache layer live L1 --leave # keep your copy, alone
Only the owner can offer a live layer. Whoever accepts it as a working layer becomes a member. The owner's machine adds you when your accept reaches it, and from then on you can write.
Every two seconds, each member runs a round:
- It snapshots its folder and sends the files that changed.
- It writes the other members' changes into its folder, even in the middle of a turn.
- A file you changed since the snapshot is left alone and goes out in the next round.
Several members can edit one text file at once, and everyone keeps every change (Edit a file together).
Files travel as hashed chunks, and a chunk you already hold isn't fetched again. The rest come from the machine that wrote them, or from its Drive when that machine is offline. Copying to the Drive never holds up a round.
Sometimes a file can't be written, because its chunks can't be fetched or because it's a folder on one machine and a file on another. The file then waits, and you're told once. Every round tries it again.
When a member's turn ends, the next round sends it, and it shows up in your copy as a read-only conversation. Message bodies over 1 MiB are cut.
These never travel:
- grants, and the layer's panes
-
.gitand the folders inlayer.export_excludes, such asnode_modules -
files over 32 MiB (
layer.snapshot_max_file)
Live layers pick up where they left off when P2P starts. The owner can remove members but can't leave. A member who leaves keeps the folder as a layer of their own.
Edit a file together
When members of a live layer edit one text file at once, every member ends up with every change. That holds for changes to different lines or different words, and even for one word changed two ways, where both new words stay, one after the other.
Each save goes out as a set of edits, not as a new copy of the file. The file is kept as a CRDT, a structure built so that every copy agrees whatever order the edits arrive in. Nothing is merged after the fact, and no change is lost.
If your editor saves a copy from before others' changes arrived, their changes are kept. If you delete exactly the text another member just added, it can come back once. If it does, delete it again.
Edits from a save go out in the next round, every two seconds, not as you type. If two members create the same new text file at once, both texts are kept in it, and one of the two members is told.
Other files are replaced whole. Every member ends up with the version that comes last in the layer's shared log. This applies to:
- files that aren't text: not UTF-8, containing a NUL byte, or over 1 MiB
-
paths that match the layer's
/.lwwfile, which takes gitignore patterns. List logs and generated files there.
If your change to such a file is replaced, you're told, and the
notice gives the command that shows your version, such as
picache snapshots show 14 logo.png. A change made by
someone who had already seen yours loses nothing, so it arrives
without a notice.
For any file, you're also told when a delete removes your change or a change undoes your delete.
Arrange the desk
The layer in front is a tree of tiles. Any pane can also float over it, like a window.
picache split --right --label tests -- make test # a new tile
picache float --cascade # pile up the floats
picache new-layer ~/src/site --label site # a new layer
Actions → Float this pane lifts a pane out, and the tiles close up. Drag a float's title to move it and its edges to resize it. Programs such as lazygit, btop or htop open as floats, with a splash screen until they draw.
Floats stack. If you drop one on another's title, or use Actions → Cascade the floats, they pile up with every title showing.
A float buried under others still shows its agent's badge. A buried terminal's title ends with its last line of output, so you can read a cascade of monitors without raising any of them.
- ◌ in a title makes a float see-through.
- To dock a float, drag its title and hold Space. Drop it on a pane to join it as a tab, or on an edge to make a new tile.
- After a restart, floats come back where you left them and run their programs again.
Scripts can drive layers with picache new-layer,
close-layer, focus-layer,
label-layer and move-layer, and they
receive layer events.
The Actions list can stay open as a pane (Open Actions as a pane). Each action then applies to the pane you were in before, and the header names that pane.
Save a layout
picache save-layout dev # or Actions → Save this layer as a layout
picache apply-layout dev --right # beside the current pane
picache apply-layout dev --new-layer docs # as a new layer
A layout goes into the layer's folder as
.cherrypi/layouts/dev.json, and you can undo the
save. It keeps every kind of pane, with each terminal's command
and folder.
When you apply a layout, a terminal that's still running is
reattached instead of started twice, unless
layouts.live_panes is fresh. An agent's
pane resumes its own conversation, using the conversation id the
agent's hooks reported (Claude Code, Codex, Pi).
Terminals keep running
Each terminal runs in its own small process, its pump. A shell or an agent keeps running when you quit the desktop, when a new build replaces the harness, or when you reload the desktop (Actions → Reload cherry-pi). It comes back in its pane with its recent output.
Closing a pane ends its program, and Actions → Stop all terminals ends them all. A reboot ends them too.
Use sudo
Your own shell can sudo, and sudo asks for your password in the pane. An agent's pane, and any pane an agent opens, can't use sudo.
| Setting | Default | What it does |
|---|---|---|
terminal.sudo.shells | on | Your shells may sudo |
terminal.sudo.agents | off | Agents' panes may sudo |
terminal.sudo.forget | on | sudo forgets your password before an agent or Picache types |
terminal.sudo.unboxed_agents | password | macOS: an agent outside its box needs your password at sudo, or (refuse) is refused |
A change to these settings applies to panes opened after it.
On Linux, cherry π can never gain privilege. redbean sets no-new-privileges on itself, and everything it starts inherits the flag. So a pane that may sudo is started by your systemd user session instead, on a terminal that cherry π opened for it. The pane's environment is passed in a file only you can read, never on a command line.
Even then, cherry π holds that pane, its record and its output. Without a systemd user session, no pane can sudo.
macOS has no such flag, so there an agent's box itself refuses sudo, su, login and the administrator prompt's tools.
| Pane | macOS | Linux |
|---|---|---|
| Your own shell | sudo, asking for your password | sudo, through your user session |
| An agent in its box | refused by Seatbelt | never: bubblewrap sets no-new-privileges itself |
| An agent outside a box |
terminal.sudo.unboxed_agents:
password (default) or refuse
|
refused: the pane keeps no-new-privileges |
On macOS, an agent's box is a Seatbelt profile. Only a Seatbelt
profile of its own could refuse sudo to an agent outside a box,
and Seatbelt boxes don't nest. An agent that brings its own
sandbox, as Claude Code does, could then not start that sandbox.
So the setting for an agent outside a box,
terminal.sudo.unboxed_agents, is your choice.
sudo remembers your password for a few minutes. So before an agent or Picache sends text or keys to any pane, cherry π tells sudo to forget it, and sudo asks you again next time. If sudo can't be told, the typing is refused.
Typing from another of your machines needs the As me grant (section 08), so that it counts as yours.
None of this covers an agent CLI you start by hand in your own shell. It can do what that shell can, including use a password you typed a minute ago.
Open several desktops
If you open the same workspace from a second terminal window,
both desktops attach to one harness. The status row shows
CONNECTED · 2 desktops.
| Shared | Each desktop's own |
|---|---|
| Terminals: output shows on both, and either can type | The layer in front |
| Each layer's layout | Focus |
| The Picache conversation, and any turn so far | Zoom |
A question Picache asks appears on every desktop, and the first answer counts. The desktop you used last plays notifications. If two desktops change a layout at the same moment, the later save wins.
A paired machine's desktop can attach too, over P2P: to the whole workspace, to one layer or to one terminal. It acts as that machine, not as you, and holds only what you grant it (section 08).
Another machine's desktop opens here in a pane of its own, framed in the remote colour and marked ◎.
The desktop
The desktop draws your work as a tree. Each part of the tree is a single pane, a stack of panes shown as tabs, or a split.
Panes tile exactly, and a terminal resizes with its pane. A pane's kind decides how it draws and which keys it takes. Floats are panes that sit over the tree like windows (section 01).
Kinds of pane
| Pane | What it is for |
|---|---|
| Terminal | A shell, a vendor agent or any other program, on libghostty and held by its pump (section 01) |
| Chat with Picache | A conversation with Picache, with its input at the foot. Several can be open at once. |
| Editor, Viewer | A file, read through the harness. Markdown is rendered, code is coloured, and a save is checked against the bytes it read. |
| Files | A folder as a tree beside your work. You can open files beside it, copy, move, trash or archive them, and see each file's git change. |
| Runs | What ran in this workspace, when it waited on you, and how it ended |
| Inbox | Issues that you and the agents write in, and the offers, asks and tasks between layers, for you to accept or reject |
| Goals | Goals given to vendor agents, with each one's steps, Picache's review and the whole diff, for you to accept or reject |
| Models | What this machine can run, as measured, and each seat's model and whether it's loaded |
| Keys | Your API keys, sealed on this machine and shown by their last four characters |
| Downloads, Tools, Media | Durable jobs: downloads with exact bytes and checksums, the managed media tools, and imports and conversions |
| Art | Clips, pictures, and video played by ptv |
| Machines, Capsules | Your other machines, with pairing and shares, and every capsule, here or on another machine, each with Stop |
| Rooms, The Bakery | People on paired machines, and public rooms you join by name |
| Extensions |
Installed .pi apps, what they ask for, and
the panes and programs they add
|
| Actions, Errors, Settings | Actions lists every action and can stay open. Errors lists what failed or was refused. Settings holds every setting, on seven pages. |
When a new pane asks what it should be, you can pick any of these kinds. You can also save it in a layout and find it by name with Go to (Find anything).
Mouse and keys
Everything you can do has something to click, and the keys are shortcuts.
A pane's header is a small toolbar with split, zoom, resize, move and close. When space runs short, the least important controls are removed from the header, so nothing you can't see is still clickable.
- Drag a border to resize.
- Drag a title onto another pane to swap or dock.
- Drag across any text to copy it.
- Click outside a dialog to close it.
- Press F6 to hand the pointer back to your terminal, so it can make its own selection.
| Key | Does |
|---|---|
| F1, Ctrl+P | Actions: every action, filterable |
| F3 | Hide or show the left rail |
| F5 | Move the keyboard between a terminal and the prompt |
| F8 | Zoom the focused pane or float, and back |
| F10 | Show, hide or focus the floats |
| Ctrl+Alt+P | Go to: layers, panes, files, conversations, receipts, runs |
| Ctrl+Alt+A | The next agent that needs you |
| Ctrl+Alt+T, Ctrl+Alt+E | A Chat on this conversation, or a new conversation |
| Ctrl+Alt+R | The Inbox of the layer in front |
| F4, Ctrl+Alt+F | The layer's Files pane, or a new one beside |
| Ctrl+Alt+X | Pick up the focused pane and move it with the arrows |
| Ctrl+Alt+S | Send to: choose where the prompt goes |
| Alt+Enter, Esc | Steer Picache's running turn, or stop it |
The rail
The rail is the column on the left (F3 hides it). At its top is the layer's banner, then a tree of where you are, then AGENTS, the agents you can launch, and NEEDS YOU, everything that waits on you.
Each layer has its own banner. Under the picture are four
buttons. The first shows how the picture is drawn (▣
pixels, ▀▄ half blocks, # ASCII,
⣿ braille) and steps to the next. ☰
chooses the banner: the theme's mark, one of cherry π's logos, or
a picture of yours (png, jpg, gif or webp, up to 8 MiB).
‹ and › step through the logos. The
choice is kept in the layer's own store, so it goes with an export
or a fork. While Settings is open, the rail shows the banner of
the layer you opened it from.
The tree has three sections:
-
Layers: each layer holds its panes as its tiles
hold them (a stack's tabs, its floats, and a pane's own tabs),
an Agents branch with the agents launched in it and the goals
given in it, and the surfaces over it. Open Settings is a row of
its own,
⚙ Settings, because it belongs to no layer. - Sessions: the conversation you are in and the recent ones, each saying where it is open.
- Machines: this machine with its workspaces, each with its harness, then your paired machines and what each shares with you.
Click a row to go there. Tab from the prompt gives the tree the keys: the arrows move and open, Enter does what a click does, / filters it, and Esc gives the keys back. What you open and close is kept for the next start.
Each row says what is going on in it, in the shapes the agent badges use (Agent status):
| Mark | Means |
|---|---|
! |
Waits for you. An agent's or a terminal's own row says on
what, such as ! Bash: rm -rf build.
|
⋈ |
A machine asks you for a share, or asks to pair. A machine asking to pair is listed until you answer. |
✗ |
The program reported a failure. |
● |
It finished while you were looking elsewhere. |
◔ |
Working, with the program's own progress
(▰▰▱▱▱ 40%) when it reports one. A
conversation works while one of its turns is under way.
|
A plain terminal shows these too when its program reports its
state over OSC 7501. A closed row carries the most urgent mark of
everything under it, with how many wait when there is more than
one: !2, counted as NEEDS YOU counts them. So a
closed layer, workspace or section never hides what needs you.
rail.motion (Settings, Desktop, The tree's
activity) sets how the marks move. With full,
the default, a working row's ◔ turns, and a row
lights up for a moment when news arrives while you look elsewhere.
calm holds the marks still, and off
draws none. The rail ticks only while something on it moves.
Manage files
The Files pane shows a folder as a tree and opens files in a Viewer pane beside it. Its menu row and its File list hold the same actions. Actions that don't apply are dimmed and show the reason.
Every change (copy, move, rename, trash, archive, unpack, chmod) goes to the harness, which checks it, keeps it inside the folder and writes a receipt (section 03). The desktop can't write your files itself. Anything you trash can be restored.
- A paste of many files is saved as a plan before the first one moves. If the paste is interrupted, Operations… shows it, and Finish completes the rest.
- In a git checkout, the header names the branch, and each changed row shows its state. Show change opens the diff beside it.
- If a file changed after you opened it, the save is refused and your edit is kept.
Find anything
Press Ctrl+Alt+P for Go to. It searches as you type, and every word you type must match.
It finds layers and panes by name, agent, kind, file or title. Through the harness it also finds file names and text in every layer's folder, saved conversations, messages, receipts and recorded runs.
Text indexing has published limits, and the workspace's own data
is never indexed. The harness refuses to search the whole disk
(/). You set search excludes and the cache budget on
the Search page of Settings.
When something needs you
The status row shows N NEEDS YOU while the harness
waits on you. It may be waiting for you to grant a capability that
something asked for (Grants), or for you to
deal with work a crash left behind (section
10).
Failures and refusals are counted in red on the status line until you see them. While cherry π runs, the Errors pane lists the last 500. The harness also keeps its own refusals as receipts, so Go to finds them.
A turn stops if Picache asks you something and nobody answers within 10 minutes, if no desktop is open, or if cherry π restarts. The question then waits in Recovery, marked asks you. When you Approve or Decline it there, the turn goes on from that call. The call is never made twice.
The setting picache.approval.unanswered chooses what
happens to an unanswered question. It can wait (the default, stored as
park), or refuse the call and go on without it.
For agents that need you, see section 05.
The prompt and the Chat
You write a governed turn (How a turn runs) in the prompt band above the status line. The band is always one row high. For a long prompt, a chip opens a bigger window (Ctrl+Alt+L).
The band names where the turn goes, and Send to changes that without moving you. Each layer has its own Picache conversation, so switching layers switches the Chat. A Chat pane follows the newest reply until you scroll up.
A reply can include a form (A2UI) that you fill in with the pointer or the keys. Its button sends your choices as your next message and does nothing else.
Pictures, video and icons
Terminals that draw pictures with the Kitty graphics protocol, such as Ghostty, kitty and WezTerm, show an image as a real picture, fitted to its pane. In other terminals, printimage draws it as text.
Video plays in an Art pane through ptv, as text with sound.
Converting a video for ptv runs as a job. Both programs ship
inside the APE, cherrypi.ape.
Icons need no patched font. Where the terminal supports the Glyph Protocol, cherry π registers its own shapes with it. Elsewhere it uses plain shapes.
There are seven themes, each a palette of named roles. Terminal Mono uses no colour at all, and nothing relies on colour alone.
picache and Picache
Picache is the agent you talk to in a Chat, and
picache is the command it uses as
its hands.
Picache is pronounced pee-KAH-cheh. The
picache command is available in every pane cherry π
opens, and it offers the harness's own tools as 95 verbs.
Every tool Picache calls is one of those verbs. When Picache calls one, it is run by the same handler, decided by the same policy and recorded in the same receipt as when you type it.
Who can call a verb
The same verb, from you, from Picache and from a vendor agent:
# you, in any pane
picache split --right --label tests -- make test
# Picache, in a Chat: the same verb, as a tool call
split {"side": "right", "label": "tests", "command": "make test"}
# → you're asked in the client; on yes, the same handler runs,
# and the receipt names picache as the caller
# a vendor agent, in its pane: the same verb, its own token
picache split --right -- make test
# → you're asked in the client, naming the agent and its pane
| Who calls | How | Who the harness sees | Where it is asked |
|---|---|---|---|
| You, or a program in your shell | picache <verb> … |
the operator |
At your terminal, for a destructive command or a write the
workspace has not allowed. --yes answers in
advance.
|
| Picache | A tool call: one verb, named parameters |
picache, with a token made for the turn and
revoked when it ends, in the folder of the layer in front
|
In your client: Allow this once, or No. A write the workspace has not allowed also offers Until cherry π closes and Always in this workspace. |
| A vendor agent in a pane | picache <verb> … in its own pane |
agent:claude, from its pane's token |
In your client, naming the agent and its pane, and never in its own terminal, which the agent controls |
| A vendor agent over MCP |
An MCP tool, through picache mcp in its pane
|
agent:claude, from its pane's token |
As for its pane: in your client, never in its terminal |
picache itself is a few lines of shell. It passes its
arguments to cherrypi.ape cli, which talks to the
harness with the pane's token from PICACHE_TOKEN.
Output is text for people, or --json for programs.
The policy decides each call from who made it, what the workspace has granted and the token's budget. The grid in section 03 shows the outcome for each caller. A refused call exits non-zero, says why, and leaves a receipt like any other call.
Find a verb
picache help # every verb
picache help split # one verb
picache help --json # every verb, for a program
picache status # who you are to the harness, and your folder
picache panes # every layer's panes and their state
A few you'll use often:
picache wait claude --state blocked
picache send claude --wait "run the tests"
picache float --cascade
picache save-layout dev
picache apply-layout dev --new-layer docs
picache events --json terminal
Give an agent the verbs over MCP
picache mcp is an MCP server over standard input and
output. Add it as an MCP server command in the agent, from the
pane the agent runs in.
# for example, in Claude Code
claude mcp add cherrypi -- picache mcp
Its calls carry that pane's token, so the harness sees the agent
as the same caller whether it types a picache
command in the pane or calls a tool over MCP.
picache mcp: the verbs as MCP tools, under the
pane's token.
It follows the MCP specification of 28 July 2026 and accepts clients of earlier versions. It offers tools only, and each caller sees only the tools it could ever be allowed to call.
An agent sees the reading tools, ask, the Inbox's
tools, Cherrytop proposals and the desk.
The desk is the set of tools that drive panes, layouts and layers
(Picache's tools lists them).
Arranging its own panes needs no approval, but you are asked
before it opens a pane or types into one. It sees the writes only
while it works on a goal
(section 05). It never sees
run (which needs a terminal an MCP client doesn't
have), history (which reads Picache's conversation),
search and jobs (which read past its
folder) or cohort.
A result is checked against the tool's output schema where the tool declares one, and a mismatch is an error.
picache mcp is also the only MCP server a cohort
gets. A cohort is a vendor agent you bring into a Chat
(section 05).
Picache's tools
Picache has 46 of the verbs as tools. With them it can:
- read the workspace and its panes, and its own conversation exactly (below);
- change files with an undo;
- run a command as a job in its box (section 09), or run a clean room or a script;
- ask another layer or a vendor agent (section 05);
- drive the desk: open, type into, arrange and close panes, save and apply layouts, and open and arrange layers;
- propose opening, showing or arranging things on Cherrytop, for you to accept (When an agent proposes);
- read and answer issues, see who an issue could go to, and write to agents (Agents write to each other);
- read and stop the jobs it started.
A conversation starts with the core tools (status,
layers, ls, cat,
find, grep, write,
patch, undo, run) and one
more, tools, which lists the rest in groups.
| Group | For |
|---|---|
panes | Reading panes, typing in terminals |
arrange | Where panes sit, and layouts |
layers | Opening, closing and moving layers |
memory | History past the window, search, cherry π's own docs |
work | Scripts, a clean room, asking a layer or an agent, its jobs |
forms | Showing you a form for a choice or an input |
inbox | Issues and replies, who an issue could go to, what an agent's hooks were seen doing, and agents' messages |
web | Cherrytop: what is open, and proposals for you to accept |
app.<id> | One per installed app |
A group loads when Picache asks for it or calls one of its tools, and stays for the conversation. Loading a group changes what the model is told and leaves what it may do as it was. With groups loaded this way, the start of each request is about 6,500 characters instead of about 20,200.
picache help lists the full set.
An installed .pi app adds its own tools while you
grant it. In a thread that only reads, such as another layer's
question, Picache gets only the reading tools.
The other 46 verbs are yours to type. With them you:
- pair machines and manage the fleet, your identity and profile, and the machines that witness your ledgers;
- answer offers, link workspaces and set what each layer takes from them;
- give agents goals (section 05), set agents' boxes, run evaluations and install apps;
- export, import, share, fork and restore layers, and list their snapshots;
- run a Bitcoin node, check cRED and mine it (section 12);
- watch events, run your own scripts, and start and stop loops (below).
Two of them are not for typing. connect is what a
confined agent's proxy asks about a connection, and
mcp is the MCP server you give an agent.
Each turn, Picache is told the names of these 46 verbs. When you need one, it names the command for you to type instead of trying it.
How Picache learns cherry π
Picache learns from an atlas built from the same registry: every verb and tool, its effect, when it asks and how to use it. The atlas is rebuilt whenever the registry changes, so it can't describe a verb that isn't there.
Next to it are an operating shelf and these docs, packed into the
runtime. Picache searches them with its knowledge
tool before answering how to do something. You and your other
agents can search them too.
picache kb search "share a layer" # the docs and the atlas
picache kb index on # also search this layer's files
picache kb semantic on # ... by meaning, not just words
Your other agents learn the command from
picache skill setup.
- It gives Claude Code, Codex, OpenCode and Pi a skill that explains cherry π.
- It gives Claude Code, Codex, Cursor Agent, Devin and Kimi CLI hooks, so each one's pane shows its state. The hooks of Claude Code and Codex also tell a new session which pane it's in.
Each change is marked as cherry π's, and
picache skill remove takes out only those.
What the model sees
A conversation is never compacted. Every message, receipt and checkpoint stays in the layer's store as written, and the model gets a window over that record.
Each turn leaves a checkpoint. It records how the turn came out (never rounded up to completed), which tools it had, any effect in doubt, the files, jobs and calls it touched, and the start of its reply. The next turn starts from it.
The model's one system message holds the latest checkpoint and the conversation's first request. Then come the newest messages that fit:
| Setting | Default | What it limits |
|---|---|---|
context.recent_limit | 8 | Newest messages in the window |
context.message_chars | 1600 | Bytes of each message |
context.window_budget | 8192 | Bytes in all |
Your current request is always kept whole. The checkpoint counts what the window left out, and a checkpoint written when Picache had different tools is marked stale.
Beyond the window, Picache can look things up exactly. The harness fills in which conversation a lookup reads, so a call can't reach another conversation.
-
history_searchsearches its conversation's messages, receipts or checkpoints. It returns two previews with ids and a cursor. -
history_readreturns one record exactly, a chunk at a time, with a digest that shows if the record changed between chunks.
Everything the model is handed is versioned: the instructions, the tool declarations, the window's defaults and the strategy. Each turn records its version in a receipt, so you can tell which context a reply was made under.
picache context # exactly what the model was given on its last call
Run a loop
A loop works toward one goal over several turns in the folder your pane opened in, checking each step with the layer's tests.
picache loop start --goal "make the API tests pass" --steps 10
picache loop # list runs
picache loop status <run>
picache loop continue <run> --steps 4
picache loop stop <run>
A run works in that folder. Each step:
- is a whole Picache turn in the run's own conversation, listed as Loop: and the goal, and governed and receipted like any turn;
-
gets the goal, a short memory of earlier steps and the run's
PROGRESS.md, and is told not to redo finished work; -
ends by running the layer's test command as a job
(
loop.test_command.<layer id>, elseloop.test_command).
If the tests pass, the run ends. If they fail, the next step works on their output.
When the steps run out (six by default), the run stops as
blocked, and loop continue gives it more. With no
test command, a run takes its steps and stops for you to review.
A run stops as blocked before its next step while anything in its conversation waits on recovery (section 10). After a restart, a running loop stays blocked until you continue it, unless recovery mode is automatic.
Anyone can list and read runs. Only you can start, stop or
continue one, and a script cannot. An app you granted
ai.redloop.manage
(section 09) can start, stop or continue the
runs it started, and you are asked each time it starts or
continues one.
Agents
There are two kinds of agent in cherry π, and they differ in who governs them.
-
A folder's agent is Picache, or an agent you
define for a folder. The harness governs it: it decides what
the agent may do and records what it does. All of its tools
are governed
picachecommands, so it can act only within the permissions the workspace has granted. - A vendor agent, such as Claude Code, Codex or Pi, runs under its own harness in a terminal pane, confined in a box (section 09). Its run is observed and recorded, but cherry π does not govern what the agent does.
Declare a folder's agent
picache agent new docs --role "Keeps the API docs in step with the code"
picache agent new docs --library # keep it in the workspace, not the repo
picache agent list
picache ask L1/docs/api "which endpoints have no examples?"
Every folder is answered by an agent: its own, if one is defined for it, otherwise the nearest parent folder's, then the layer's, then the workspace's, and Picache when none of those exist.
picache agent new writes the agent's definition to
.cherrypi/agent/agent.json inside the folder. It
names a persona, a preferred model, and a folder of private
Markdown notes, kb/, that only this agent reads. The
definition lives in the folder, so it goes with the folder
wherever the folder is copied or shared.
Use --library to store the definition in the
workspace instead of in the folder. The workspace records which
folder paths it belongs to, so the repository contains no agent
files.
The persona applies only within cherry π's own rules and cannot override them. The agent's permissions always come from the workspace's grants, never from its definition file. Its actions are recorded in receipts under its own name.
When you send a question or a task to a layer, the layer's agent answers it. You can also address a question directly to a folder inside a layer, whether that folder is in this workspace, in a linked workspace, or on another machine.
Test and share an agent
picache eval # run the cases
picache eval schedule on # run them while the model is idle
picache agent pack --out docs.pi
picache agent install docs.pi
picache eval runs test cases against an agent: a
standard set of core cases, plus any cases the agent defines
itself. An evaluation cannot change anything: read-only
operations run for real, and every other operation the agent
attempts is recorded but not carried out. The core cases check
that the agent:
- reads a file before answering a question about it;
- searches the folder to find a file before trying to read it;
- looks at the pane when it is asked what a pane shows;
- asks before running a command, instead of running it;
- when a job belongs to the operator, names the command the
operator should run (for example,
picache pair) instead of attempting the job itself; - answers how-to questions about cherry π, such as how to unpair a machine or give a folder its own agent.
picache agent pack bundles an agent into a single
signed .pi file, and
picache agent install checks that signature
wherever the file is installed. It refuses an agent that is
unsigned or was changed after signing. An agent signed by someone you do not know needs
--untrusted.
Launch a vendor agent
The rail lists the vendor agents cherry π can launch: Claude Code, Codex, Gemini CLI, Cursor Agent, Copilot, Pi, and others. Next to each one, it shows whether the agent is installed and whether an update is available.
When you launch an agent, cherry π starts it in a new terminal pane opened at the layer's folder. By default it runs confined in its box (section 09), and the run is recorded (section 10). The agent keeps its own conversation transcript, and cherry π does not take it over.
Ask a vendor agent in a Chat
If you start a Chat message with @claude, Claude
Code answers that message instead of Picache. A vendor agent
brought into a Chat this way is a cohort.
@claude what does the build script do? # in a Chat
picache cohort # who can be asked, and who is running
picache cohort claude "what is in here?" # ask once, from a shell
picache cohort stop all # stop a conversation's cohort
In a Chat, cherry π talks to the agent over ACP (Agent Client Protocol), and the agent works in the layer's folder. It is confined in the same box it would use in a terminal pane, with the same launch setting, network access, and login, but it has no terminal.
The agent's own tools for writing files, editing files, and running commands are turned off, which leaves it Read, Glob and Grep. Its only MCP server is cherry π's own (section 04), reached with a token made for it alone, so anything it tries beyond reading is checked against the same policy as any other agent's work.
If the agent asks for permission to do something, cherry π asks you, and your answer covers that one call only. If you do not answer, the call is refused.
@claude in a Chat: a vendor agent over ACP, in its
box, with cherry π's tools.
The reply holds the agent's answer, marked as external and unverified, with the tools it says it ran and any questions it asked. The turn still belongs to Picache and is recorded like any other.
Your next message goes to Picache unless it also starts with
@claude. Picache can read Claude Code's answer as
part of the conversation and knows that Claude Code wrote it.
Claude Code stays running for that conversation, keeping its context, until it has been idle for 15 minutes, the conversation moves to a different folder, or you stop it. At most four of these agents can stay running at one time. When a fifth is needed, the oldest one is stopped.
Bringing in a vendor agent cannot be undone: your question, and anything the agent reads, is sent from this machine to the agent's vendor. You can bring one in directly at any time. Picache can do so only with your approval for that call, and an agent brought in this way cannot bring in another one.
A vendor agent can be asked in a Chat only through the Agent
Client Protocol (ACP). Two agents have it: Claude Code, through
the @agentclientprotocol/claude-agent-acp adapter,
and OpenCode, whose own opencode acp speaks it.
Ask OpenCode with @opencode.
Give an agent a goal
A goal gives a vendor agent a piece of work to complete on its own. The agent works in a copy of your layer, and none of its changes are applied to the real layer until you accept them.
picache goal @claude "make the login tests pass" --steps 8
picache goal @claude,@opencode "make the login tests pass" # one copy each
picache goal # the goals, newest first
picache goal status <run>
picache goal comment <run> src/login.ts:42 "check the token's expiry"
picache goal revise <run> # send the open comments back
picache goal todo <run> "add a test for the expired case"
picache goal accept <run> # merge its work into the layer
picache goal reject <run> # leave it unmerged
picache goal push <run> # its branch to a git remote, with your git
picache goal offer <run> # its branch to a paired machine's layer
picache goal stop <run>
picache goal continue <run> --steps 4
Claude Code and OpenCode can take goals. Each is given the goal
through ACP, with its own tools narrowed to reading, so every
change it makes goes through cherry π. An agent starts in the
agent box only once it has been measured there, and one that
hasn't been is refused with confine_agent_unmeasured.
OpenCode is measured under Seatbelt on macOS, not yet under
bubblewrap on Linux.
A goal works like a loop (section 04),
except that the vendor agent, not Picache, takes each step. A
step is one turn for the agent, like a message you address to it
with @claude in a Chat, and ends by running the
layer's test command. A goal has six steps unless you set
another number.
The agent works in the .worktrees/goal-<id>
folder inside the layer, on a branch named
cherrypi/goal-<id>. The copy includes the
layer's dependencies and existing build output, and is
copy-on-write where the file system supports it.
-
The agent acts with a token created only for this goal. Using
cherry π's
writeandpatchcommands, it may change files only inside the goal's working folder. Every change can be undone and is recorded with a receipt. - It cannot write to the copy's
.gitfolder. - At the end of every step, the agent's changes are committed to its branch.
The run stops when the tests pass, when its steps run out or a time limit you gave it passes, or when a step's turn fails or needs your review. Failing tests do not stop it: their output is the next step's work. Picache then reviews the branch. The review is read-only: Picache compares the agent's work with the goal, points out anything that looks wrong, and recommends whether you should accept the work.
Accepting the work merges the branch into the layer. The merge is refused if either folder contains uncommitted work or if the merge would conflict, and a refused merge leaves both folders unchanged. Rejecting the work leaves the branch unmerged, so the agent's work stays on record. Either way, the working copy is removed and the branch is kept.
If a layer is not in git, the agent still works in a copy of the whole folder, but each step ends with a snapshot instead of a commit. Accepting the work performs a three-way merge against the snapshot taken at the start, and the merge is refused entirely if there is any conflict. The layer is snapshotted before the merge, so you can undo the merge afterwards.
If you set goal.workspace to worktree,
or the layer's folder is below the top of its repository, the
agent works in a git worktree of the layer instead of a copy.
The Goals pane lists every goal and shows its steps, Picache's review, and the complete set of changes.
Only you, the operator, can create, stop, continue, comment on, accept, reject, push or offer a goal. Agents and scripts cannot.
Several at once
New goal…, in the Goals pane's menu (n) and in Actions, asks for the goal, the agent and the number of steps. With Another after this on, the dialog opens again when you start one, so five goals are five presses of Enter.
Naming several agents, as in @claude,@opencode,
gives the same goal to each, in a copy of its own. Every agent is
checked before any starts. The list marks the goals of one group
with ⇄, and each card names the others. When you
accept one, cherry π asks whether to Reject them
or Keep them; a rejection says which approach
was accepted instead.
Commands, each asked of you
An agent on a goal can call run and
script in its copy, and nowhere else. It never gets
a shell of its own. You're asked about each command in the
client: Allow this once, Allow it for
this goal (that exact command line, kept on the goal and
receipted), or No. The command runs as a job in
the agent's box, writes only in the copy, never in its
.git, and stops when the goal does. The card lists
what you allowed and every command a step ran.
Setup, test and run
A layer can have three commands, each a setting with an
optional layer-specific form, as the test command has
(section 04):
loop.setup_command, run once in a new copy before
the first step; loop.test_command, run after each
step; and loop.run_command, the app itself.
Layer commands…, in the Goals pane's menu, sets
all three for the layer in front.
Each goal gets a block of ten ports, starting at 42000, which it
keeps until you decide it. Its first port reaches the setup,
test and agent's commands as CHERRYPI_PORT, and the
agent is told to listen there, so two goals' servers don't
collide. A setup that fails stops the run, its output shown on
the card, and Continue runs it again. The
card's Run opens a terminal pane,
run · goal <id>, in the copy, running the
run command on that port.
Comment, and request changes
Click a line in the goal's diff to mark it, then Comment on <file>:<line>…. Comment… without a line comments on the change as a whole. A comment is drawn under its line and listed on the card, which keeps the line's text as it was. A comment is never deleted: Withdraw marks it withdrawn, and each one is receipted.
With comments open, the card's main button is Request
changes (goal revise). The run goes on for
two more steps unless you say otherwise, and the next step's
prompt carries each comment with its file, line and text. A goal
whose tests passed can be revised, as one that ran out of steps
can, but a decided one cannot. Picache's earlier review moves to
the goal's history, and a new one follows the revision, reading
which comments were sent and what has changed since.
Todos, and Ready?
A goal keeps a list of todos. You add yours with
Todo… or goal todo, and the agent
can add its own and close them, but not yours. Each step's prompt
lists the open ones. Accept is refused while any is open
(goal_todos_open).
The card's ready? line sums up what Accept
depends on: the test, Picache's review, open comments, todos,
conflicts, and uncommitted work. A ✗ marks what
Accept would refuse, or what you'd want to read first.
Push, and ask for a pull
Push… pushes a goal's branch to one of the
layer's git remotes, under the same name. It works with any git
host, or none, and cherry π holds no token for it: the push runs
with your own git in a terminal pane, push · goal
<id>, in the layer's folder, so your ssh agent,
credential helper or password does the signing in. With several
remotes, cherry π asks which. Once the branch is there, the pane
prints the text of git request-pull, which asks for
it to be pulled; send it to whoever merges, by any route.
The push names the commit cherry π read when you asked, so a
branch that has moved since isn't pushed in its place, and it
runs no git hooks. A goal outside git, or one still running,
can't be pushed, and a layer with no remote is refused with
goal_no_remote, naming git remote add.
The card's pushed line names each remote that
has the branch, as git last heard, and says when the branch has
moved on since. Merging stays yours, here with Accept or where
you pushed it.
Offer a branch to a paired machine
Offer… sends a goal's branch to a layer on a paired machine or of a linked workspace, peer to peer, with no git host between them. It lists each one, addressed to the layer of the goal's name there, and lets you change the address before it goes. The branch travels as a git bundle of the goal's commits, with its goal, agent, log and the files it changes, up to 10 MiB; past that, push it instead.
The offer waits in that layer's Inbox, and accepting it is that
operator's. Accepting checks the bundle, then fetches the branch
into that layer's repository under its own name, with nothing
checked out and no hook run. It's refused if the repository
doesn't hold the commit the goal started from
(conduit_branch_unrelated), and it never moves a
branch of that name that's at another commit
(conduit_branch_exists). Merging it is theirs.
Agent status
Each agent shows both its state and how that state was determined, because a badge that said working while the agent waited for your answer would mislead you. The state comes from several signals, ordered from the most reliable to the least:
- The process itself: whether it has exited, whether it has started a child process, and whether it printed anything in the last moment. These checks know nothing about the particular agent, so they work for an agent cherry π has never seen.
- The agent's own reports: its terminal title, and reports sent through the Program Status Protocol (OSC 7501), where the agent states whether it is working, idle, finished, failed, or blocked waiting for a permission, an answer, or a sign-in.
-
Hooks installed by
picache skill setup, which make Claude Code, Codex, Cursor Agent, Devin and Kimi CLI report their state in the same way. With these hooks, every one of them but Cursor Agent reports a permission dialog as blocked, together with the tool and the command it concerns.
The status badge is one symbol for what you should do next. An inferred state is drawn dim, and the agent's detail pane says how the state was decided. The more reliable signal wins: an observed exit overrides an agent's report, and an agent's report overrides an inference.
Only an agent's report can mark it blocked. The badge never guesses that state, because silence cannot distinguish a model that is still thinking from a question that is waiting for your answer. If two panes run the same agent and a report could belong to either pane, the report is withheld so that it never appears on the wrong pane.
The rail's tree shows the same marks on each pane's row, and a closed row carries the most urgent of what it holds (The rail).
A script or shell can wait until an agent needs you:
picache wait claude --state blocked # until it needs you
Notifications
When an agent needs your attention or finishes its work while you are not looking at it, cherry π notifies you once and plays a sound. When several agents need you at the same time, one notification covers them all.
Press Ctrl+Alt+A to jump to the agent that has been waiting for you the longest. Questions are shown first, then failures, then results you have not yet seen. A result remains marked as unseen until you focus its pane.
Each agent can have an expression: a picture, a sound, and a line of text for each state. The picture appears next to the agent's name in the rail and on its profile card.
You choose how notifications are delivered: through the terminal cherry π runs in, through the operating system's notification system, or not at all. The setting applies in every workspace.
The software factory
The factory makes .pi apps
(section 09). You describe what to build
in a sentence or two, and a vendor agent builds it in a layer of
its own. Checks that the agent never sees decide whether the app
passes, and nothing is accepted until you accept it.
- A schematic describes one kind of app: a working skeleton to start from, what every app of its kind must keep, the capabilities it may ask for, which agent builds it, and how much that agent may spend.
- A product is one app a schematic made. It is a git repository and a layer of its own, beside the layer it was made from.
- The builder is the vendor agent working on it, as a goal (section 05).
- The hidden checks are the schematic's scenarios (Hidden checks). The harness runs them against the packed product, and the builder hears only what failed.
- A Try runs a product before it is installed, on data of its own.
Make a product
Add a Product pane in any layer. In a layer that is not a product, the pane is where one is made:
-
Schematic. The layer's schematics, and an
Add button for each starter cherry π ships:
data-app(a collection in the app's own tables, with verbs, a pane and pages),pane-program(a program in a terminal pane, drawn from keys) andweb-app(pages that are the product, with a Cherrytop widget). Adding one copies it into.cherrypi/schematics/, where you can edit it. - Builder. The vendor agents that can work over ACP, as a cohort does (section 05). The schematic's own is chosen unless you pick another. One that is not installed on this machine is named but not offered.
- What it spends. Before anything starts, the pane says how many steps and minutes the builder may use. The steps are the builder's own turns, from its vendor, not Picache's.
Make a product… asks for the product's name (its layer and its app's id), then what it should be. Nothing starts until you have answered both, and then the pane shows the new product. From a shell:
picache factory # this layer's schematics, the starters, the products made
picache factory add data-app # copy a starter into .cherrypi/schematics/
picache factory run data-app books "a reading list with ratings"
picache factory run data-app books "…" --agent @claude --steps 8
While it is built
The builder works as a goal does, in a copy of the product, one step at a time. Each step ends with the schematic's public test. After a step, the result of the last hidden check (in the schematic's words) is part of what the builder is told next.
The Product pane says where the build stands:
| The pane says | What it means |
|---|---|
| building · step n of m | The builder is working. Stop ends its run, and you then accept or reject what it did. |
| Picache is reviewing it | The builder stopped. The hidden checks run on its copy, and Picache reads the result and the changes. |
| waiting on you | Picache's review is shown. Accept merges the build into the product. Reject… leaves it out, with your reason if you give one. Open its chat shows its steps and the review. |
| its build was accepted / rejected | Decided. A rejected build's branch is kept for the record. |
A build is accepted only on a current pass of the hidden checks, on exactly what is in its copy now. A pass on an earlier version, or under scenarios that have changed since, does not count.
A run stops for review when its steps or its minutes run out.
picache goal continue <run> --steps n --minutes n
gives it more.
Each step's commit says what made it, as git trailers:
Cherry-pi-Goal, Cherry-pi-Agent (with
the name and version the agent gave),
Cherry-pi-Launch (confined or
standard) and Cherry-pi-Factory-Run.
The merge that accepts it says why: the goal, Picache's review,
how many hidden checks passed on which candidate, and who
accepted it.
Hidden checks
A scenario is a small Lua data file in the schematic's
scenarios/ folder. The harness packs the product
once and runs each scenario on its own copy, in a capsule. When
the product asks for its own tables, the request is carried out
for real. Any other request is answered from the scenario's
answers (or the schematic's
finish.answers) or refused.
| Kind | Checks |
|---|---|
call | A verb or tool answers with this text, pattern or data |
asks | What it asked the host for: these capabilities, never those |
rows | After these calls, its own tables hold these rows |
page | A page answers with this status, type and content |
program | These keys give a screen that contains this, and fits the pane |
static | Its manifest is valid, it asks only for what the schematic allows, it packs and its signature verifies, and it is small enough |
The builder never sees a scenario. With
feedback = "summary" (the default) it hears which
scenarios failed, what was expected and what was observed. With
"verdicts" it hears only which failed. A builder
launched confined cannot read them. One launched
standard could, and every verdict records which it
was.
To run the hidden checks now, use Run hidden
checks in the pane, or
picache factory finish <product>.
Try a product
Try it runs what is in the product now, before it is installed, on data of its own and only with what the schematic allows. The pane then offers what the product has:
- each of its panes, and each program in a terminal pane;
-
its pages: Open in Cherrytop, or
https://try-<id>.pi; -
Open its console: type a verb and its
arguments,
tool <name> <json>,tablesorrows <table>, and see the answer, what it asked for, and what changed in its tables.keep <name>saves the last call as a new hidden check that expects what you just saw.
Close Try ends it, and its data goes with it. A
Try never installs anything, and <id>.pi
stays the installed app's.
picache factory try books
picache factory call books add "Dune"
picache factory close books
Pack for review
Pack for review, or
picache factory pack <product>, runs the
hidden checks. If they pass, and only then, it packs the product
as a signed .pi onto the review shelf,
<state>/factory/shelf/. The product's
evidence is inside the signed file: the order, the schematic,
every check and scenario result, and whether the builder was
confined.
picache factory says how each schematic is doing:
its runs, how many were packed, accepted and rejected, and how
often its hidden checks passed.
Turn it off
The factory is a first-party addon, on by default.
picache addons disable factory turns it off, and
picache addons enable factory turns it back on.
While it is off, there is no picache factory and no
Try, and the Product pane says the addon is off and how to turn
it back on. A build the factory started cannot be accepted while
it is off, because its hidden checks are not there to pass it.
Models and scripts
Picache runs on a model you choose, and you can script cherry π in Lua.
Choose a model
Open Actions → Open Models. Its two tabs are This machine and Catalog.
| Row | Shows |
|---|---|
| Machine | The chip, its memory and cores |
| GPU | How much of its memory is in use |
| Runtime | cherry π's own llama.cpp (b11819), with Install when it's missing |
| Seats | Picache, Small, Search, and Cloud in hybrid mode: each one's model, with Load and Empty |
| Thinking | Whether a reasoning model thinks first (off by default) |
Each model gets a verdict: runs, tight, too large or won't load. llama.cpp measures it from the model's header, so a catalog model is measured before you download it. Try loading confirms it with one real load.
A model loads only into free GPU memory, with 1 GB to spare. If there isn't room, cherry π stops models it started to make room: those in no seat first, then the oldest, and never the one serving Search. If even that would not make room, the load is refused before anything stops, and the reason names what holds the memory. Model servers cherry π didn't start are listed, with Stop.
Seats' models stay loaded side by side while they fit. Empty clears a seat, and unloads its model if no other seat uses it. Search takes only an embedding model, and no other seat takes one.
The pane also finds or imports models you already have, and uses
them where they are. Downloads are checksummed, resumable jobs,
and llama-server runs as a job too.
Picache can also use any OpenAI-compatible service. Without a model, it answers with a fixed template (section 03).
Vendor agents (section 05) keep their own models, subscriptions and settings.
Your API keys
Open Actions → Open Keys. The signer seals your API keys on this machine (Where your secrets go), and the pane shows each one only by its last four.
| Tab | Holds |
|---|---|
| Providers | Hugging Face, OpenRouter, Qwen Cloud, DigitalOcean inference |
| Agents | Each vendor agent's own keys, such as a login token you handed in |
A row reads one of these:
- stored (
••••abcd); -
an environment variable (
$HF_TOKEN ••••9876), which is read but never copied in; - not set;
- locked, when the sealing key has changed.
A stored key wins over the variable. Listing your keys opens none of them anywhere else, because the pane gets only the last four from the signer (Where your secrets go).
Press Enter to set a key, masked as you type, and x to clear one, which asks first. Only this machine's own desktops can see or change keys, and no verb reaches them. Each change is a receipt naming the key, never its value.
An agent's keys go into its environment when it starts, or stay with the signer when its Key is set to inject (Keep the key out of the box). Provider keys belong to the Cloud seat (Which model answers).
Which model answers
Without a router, every step goes to the Picache seat with the
workspace's thinking setting. A retry or a continued turn stays on
the same model, so the prompt cache holds
(route.sticky).
A router is an installed app's route hook. It
proposes a seat, thinking and limits, or handing the turn to a
vendor agent. Each proposal is checked before it is used:
- the app is granted that seat (
route.seat.small, …); - the seat's model is loaded;
- the model has qualified for the job;
- anything leaving the machine is allowed.
A proposal that fails a check is overruled, and the record says why. A router never answers a permission prompt or changes a tool call.
picache eval qualify small # a seat's model must pass before a router uses it
picache layer egress ask # may this layer's requests leave the machine?
Leaving the machine means the Cloud seat, in hybrid mode with a key
from Keys, or a handoff to a vendor agent. It's off by default
(route.egress). The Chat says when a step was sent to
a cloud model or handed to an agent.
Run a script
A Lua script runs in one of two places, depending on who wrote it.
picache do layout.lua name=dev # yours: runs as you
picache script check.lua --budget calls=20 # anyone's: runs in a capsule
| Compared | picache do |
picache script (code mode) |
|---|---|---|
| Written by | You | A model, or anyone you don’t vouch for |
| Runs |
In your terminal, as the picache command you
started
|
In a capsule: an empty folder of its own, confined by Seatbelt on macOS |
| Can use |
Lua’s whole library, including io.popen and
os.execute
|
Computation, JSON and the verbs it was given. No files, no programs, no network. |
| Governed | Each picache.* call, as the pane |
Each verb call, as its caller, within a fixed list and budget |
picache do takes its scripting shape from Rex, the
Superlogical terminal. Both run a local Lua script that can
arrange terminals, wait for events and run commands. Rex removes
io.popen and os.execute; cherry π leaves
them working. Cherry Pi also applies its own pane authority and
receipts to calls made through picache.*. With the
rex addon on (picache addons enable
rex) and Rex running on the same Mac, the same script has a
rex table that talks to that server:
rex.call, rex.session,
rex.layout and rex.block.call, as in
Rex's own Lua.
picache.* still talks only to cherry π.
Blocking them would not protect anything, because the script is
your program running in your terminal, and redbean's
unix library does the same with fork
and execve. A script reaches what you can reach at
the prompt, and no more.
When an agent runs picache do in its pane, the script
runs in that agent's box.
A script you didn't write belongs in picache script,
where the whole environment is the limit. Picache's
script tool is that same capsule, limited to the
tools its turn has, so a script there can do no more than Picache
could do one call at a time.
Each picache.* call carries the pane's token and
leaves a receipt. A command started with io.popen
never reaches the harness, so, like a command typed in the pane,
it leaves no receipt.
Still, prefer picache.exec over
io.popen:
- It takes a list of arguments instead of a shell line, so nothing needs quoting.
- It has a timeout.
- A command it starts stops when the part of the script that started it ends.
-
The script keeps reading events while it waits.
io.popenblocks the whole script until its command ends.
Machines, offers and rooms
Conduit is how cherry π talks to itself and to others: layer to layer, workspace to workspace and machine to machine.
The three differ only in how the bytes travel. The records, an offer's lifecycle, the receipts and the authorization are the same in all three.
| Address | Means |
|---|---|
L1 | A layer in this workspace |
L1/docs/api | A folder inside it |
web:L1 | A layer in a linked workspace on this machine |
studio:web:L1 | A layer on a paired machine |
Every machine keeps a durable queue, so all of this works without a server.
- An offer is written to the outbox before the network is touched.
- It counts as delivered only once the receiver has saved it.
- A repeated message gets its stored answer instead of running twice.
- An offer that doesn't arrive stays queued and is sent again. A refusal is final.
- Both ends keep the record.
Offers, asks and tasks
From a shell, the three look like this:
picache offer L2 message "the schema moved to v3"
picache offer L2 patch src/api.ts oldName newName
picache ask L2 "which tests cover login?"
picache task L2 "update the client for the v3 schema"
An offer is a message, a patch or a whole file for another layer. Nothing changes there until you accept it in that layer's Inbox (Ctrl+Alt+R).
A patch is applied through that layer's own patch
verb, jail and undo, and it applies in full or not at all. Anyone
may offer, agents and Picache included, but only you answer.
An ask puts a question to the other layer's agent. The question runs in a thread of its own, which can only read and can't see either layer's chat.
The answer comes back labelled as coming from that layer, and it is treated as information, never as instructions. The thread opens as a tab on your Chat.
A task is work. Once accepted there, it runs as a thread with the usual tools. Its report comes back marked external_unverified until you promote it.
Writes never cross layers directly. Reads can, limited to the other layer's folder:
picache cat --layer runtime README.md
Link two workspaces
picache link ~/src/web --accept message,patch,ask
The command links this workspace to another one open on this
machine and names the kinds of offer it may send. The other
workspace is asked to link back. Once linked, its layers are
addressed as <workspace>:<layer>.
An offer to a workspace that isn't open waits in the outbox until it opens.
A layer can decide for itself what it takes from one linked
workspace or paired machine, or from all.
picache accepts L1 # show the lists
picache accepts L1 web message ask # only these kinds from web
picache accepts L1 web none # nothing from web
picache accepts L1 all inherit # hand the choice back
A list can allow fewer kinds than the link allows, or more. A list
for one peer takes precedence over the list for
all.
A layer with no list takes what the link allows, and any kind from a paired machine. Only you set these lists.
Identity and the signer
An installation has one identity. It is made of the machine's keys (Ed25519, Curve25519 and secp256k1) and a key for each agent, Picache first, ready to be named on Nostr. Workspaces and layers have no keys, because they're addresses under the machine.
The private keys never reach the Lua runtime. They are held by a separate signer process on Bare, which receives what cherry π wants signed and sends back a signature.
picache identity # the node id and each agent's public key
Pair your machines
P2P is an addon, off until you turn it on. It runs the Bare and Holepunch stack in a sidecar, boxed apart from the signer (section 09).
picache addons enable p2p_holepunch
picache fleet new # on the first machine: make a secret
picache fleet join <secret> # on each other machine
picache pair studio # ask; it shows a code
picache pair approve <code> # on studio: the code, typed
Asking with picache pair studio shows a code on your
machine. The code is typed on the machine you're joining: its
operator reads it off the asking machine and approves with it.
Studio holds the request for ten minutes.
Studio's list of requests never shows the code, so a stranger who asks under your machine's name can't be approved by someone copying a code from it.
A code typed before its request has arrived is kept for ten minutes. The asking machine asks again while it waits, and it is approved when the request comes in.
P2P belongs to the machine, so a pairing made in one workspace holds in all of them.
A fleet is the set of machines that can find this one. Fleet machines announce themselves on rednet, the public peer-to-peer network, under a topic hashed from the fleet's secret. Only machines with the secret can work out the topic.
- Without a fleet, and with the open directory off, a machine is listed nowhere.
-
The open directory stays off until you turn it on
(
picache fleet open on). -
On one local network, fleet machines also find each other over
mDNS, and they keep working with no internet. The mDNS beacon
shows the machine's id and key to everyone on that network. Only
its fleet is hashed.
picache fleet lan offstops the beacon. - A beacon is only a hint. Seeing one never makes a machine trusted.
Pairing pins each side's key and sets up a contract in each direction. Each contract has exact scopes and can be revoked. It lasts 30 days and is renewed for as long as you stay paired. Unpairing revokes what this machine issued.
A paired machine may ask this one to run a capsule
(section 09). The request is refused
unless you allow it for that machine
(picache machines host studio on).
Open another machine's desktop
picache machines open studio # its workspace
picache machines open studio:web:L1 # one layer
picache machines open studio:web:terminal:<id> # one terminal
You can also use ◎ Open on that machine's card in the Machines pane. Either way, the workspace opens in a pane here.
The pane has the remote colour (theme roles remote
and remote-focus), and its title starts with ◎, so
you won't mistake it for a local pane. Close the pane to leave.
Only you can open one. An agent can't, and neither can a desktop
that is itself remote. While desktops from other machines are attached
here, the status line says so, as in
1 from studio (view).
The other machine lets the desktop in when its workspace accepts remote desktops and its grant to you allows it. Its operator sets this:
picache desktops edit # on the other machine: off, view, edit or full
picache desktops off
A desktop given the whole workspace reaches the files, git and trash of its layers' folders, by root, never by path.
Folders outside the layers stay out of reach, such as folders you searched or pinned, or a job's scratch folder. A grant therefore never grows with what you do after giving it.
This machine's own data folders (.cherrypi and
~/.cherrypi) are never reachable, even with the As me
grant (below).
Share one layer or one terminal
picache machines desktop studio edit --layer L1
picache machines desktop studio view --terminal <id>
You can let a machine's desktop into one layer and nothing else, with the first command above or with Desktop per layer… on the machine's card.
A layer's grant works even when the workspace takes no desktops. When several grants cover what was asked, the narrowest one decides.
That machine can also ask for a layer as a share, which you
approve (Actions → Share this layer with paired
machines). It then opens the layer with
◎ Open or picache machines open.
The other machine sees that layer's view, conversations, terminals, files, evidence, offers, goals and git, and nothing else. Its terminals open in the layer's folder, and it can't add, close or move layers.
Changing a grant closes at once any desktop it no longer covers.
Presence names the layer, as in
1 from studio (full, L1 alone).
Narrower still, you can share one open terminal: use
Actions → Share this terminal with paired
machines and approve it, or grant it with
--terminal as in the second command above.
A terminal without a grant of its own uses its layer's grant. The other machine sees only that terminal, in one pane. The grant and the desktop end when the terminal closes.
What a remote desktop may do
A desktop from another machine acts as that machine, not as you,
and it's recorded as that machine (node:<id>,
desktop on studio).
Its grant is a preset over seven capabilities:
read, files.write, chat,
layout, git.write,
terminal.type and goals.decide.
| Grant | Holds |
|---|---|
| Watch | Reading |
| Edit files (where a new grant starts) | Reading, changing files, talking to Picache, saving layouts |
| Full | Everything except typing in terminals |
| As me (your own machines only) | Everything, acting as you. The record still names the machine |
picache machines desktop studio view # a preset
picache machines desktop studio read,git.write # a custom grant
picache machines desktop studio inherit # back to the workspace's
You can also use Desktop… on the machine's card.
Custom grant… takes a list such as
read,git.write.
The workspace's own setting caps the grant for the whole workspace: No desktops, Let them watch, Let them edit files or Let them act.
A desktop closes at once if its grant loses a capability it came in with, or moves to or from As me.
Only As me can type in a terminal, because a shell runs as you, with all of your home, and can commit to git. Other desktops, custom ones included, can watch terminals but can't open any. A watched terminal's header says watching.
Messages to Picache from a remote desktop get the same limits. A turn it starts or steers can read, and it writes files only if the grant allows. It can't run commands, type, arrange the desk, or start an agent or a goal.
When another machine's words reach a chat
Once another machine's words reach a conversation, its later turns stay held until you acknowledge them. A held turn reads only what that machine could read. Anything it would change is asked of you as usual.
If two machines' words reach one turn, it gets only what both grants allow.
The chat says whose words they were, with an Acknowledge button. Click it once you've read what those turns did. It works only on this machine's own desktop, and not while a turn is running.
picache history acknowledge <conversation> # the same, from a shell
The command works only in a shell you opened by hand on this machine, and the desktop asks you to confirm it. Panes an agent asked for, As me panes and other machines' panes can't use it. An agent that you start yourself, in a shell you opened by hand, can acknowledge.
What stays with this machine
Every call names the capability it needs. A call from another machine is answered only when its grant holds it, and anything undecided is refused.
Some things stay with this machine's own desktops, even under As me:
- settings, pairing and grants;
- approvals and recovery;
- installing onto this machine;
- running jobs, capsules and app panes;
- speaking as this machine in rooms and the lobby;
- stopping the harness.
On a trusted network, a machine with no grant may watch but can
never change anything. An explicit off still holds
there. You mark a network as trusted like this:
picache desktops trust 192.168.1.0/24
On the remote desktop, what its grant leaves out stays visible but dimmed, with the reason underneath, and can't be clicked.
Profiles and shares
A machine shows its paired machines a profile: a name, a handle, a status, a bio, a time zone, an avatar and a banner. Its avatar and banner are kept in the machine's Drive and checked by hash.
picache profile set name "Studio"
picache profile avatar ~/me.png
picache shares add layer L1 request # paired machines may ask for L1
A machine also shows whatever you share: a workspace's desktop, a layer, a terminal, a room, an agent or an app. A share is asked for, approved and revoked like a pairing.
A shared workspace, layer or terminal then has ◎ Open on its card. The Machines pane does all of this too, and runs the same commands underneath.
Issues
An issue is a thread in the Inbox that you and your agents write in, like mail.
| List | Holds |
|---|---|
| Waiting on you | Every decision waiting on you, as the rail's NEEDS YOU lists them |
| Issues | Open issues |
| Conversations | What agents wrote to each other, and to you or Picache (below) |
| Offers | Offers between layers |
| Done | Issues done or declined |
Start an issue with New issue in Actions (F1) or the Inbox menu, or press n in the Inbox. Then type in the deck at the bottom of the screen. The first line is the title, and the rest is the body, in Markdown.
Each issue is numbered (#12) and tagged with your
current layer. While an issue is on screen, what you type in the
deck is a reply to it.
Done, Decline and Reopen move it between lists, and the thread notes each move. ● marks an issue an agent answered since you last read it.
Agents write from a shell:
picache issue # the open issues
echo "Login fails on Safari." | picache issue new "Safari login"
picache reply 12 "Fixed in the session cookie."
An agent sees only the issues tagged with its own layer and the
ones assigned to it. What it
writes shows its name and the end of its pane's run id
(@claude·3f2a), and is marked unverified. Only you
can move or retag an issue.
Send an issue to another machine
To send an issue to a linked workspace or paired machine, use Send to… on an open issue, or:
picache issue send 12 studio
The issue arrives in the other Inbox like mail, open and unread, and marked with where it came from (from laptop).
The receiving side has no step to take it in, because pairing or
linking already gave consent. A layer's own list
(picache accepts) can still refuse issues, and a
linked workspace takes none unless its link allows
issue.
Each side keeps its own thread with its own number, and every
reply reaches the other side. An agent's reply is still marked
unverified and names its machine
(@codex·91c0 (studio)).
The receiving side moves the issue, and the sender's copy follows. If a machine can't be reached, the issue stays queued and is retried, and the issue reader shows that it is waiting.
Only this machine's own desktops and As me desktops write in a shared issue. Another machine's desktop attached here with any other grant can only read it.
Agents write to each other
An agent in one pane can write to an agent in another, the way Claude Code's SendMessage writes to another session. Writing takes one command, and the message reaches that one pane, not every agent of that kind:
picache message who # the agent panes you can write to
picache message codex·91be "The API is ready on main."
picache message read # what waits for you, now
An agent is named by its vendor and the end of its pane's run id
(codex·91be), or by the vendor alone
(codex) when only one is open. You can write the same
way from your own shell, to any agent.
Each message carries a type. An answer names the question it answers, and a handoff names what to take up.
| Type | For |
|---|---|
message | Anything (the default) |
question | Something the other agent should answer |
answer | An answer, with --re <id> naming the question |
handoff | A result for the other to take up, with --ref a branch, commit, path or goal |
blocked | The sender can't go on without something |
done | The sender finished what it was asked |
picache message codex "Is the build green?" --type question
picache message claude "Green on main." --type answer --re ev_81c2
picache message codex - --type handoff --ref fix/parser < notes.md
picache message on its own lists your
conversations, and picache message show codex shows
one.
An agent that opened another agent's pane also hears from cherry π about that pane, the way herdr tells a parent. It hears done when the other agent finishes a turn (with what it last said), needs you when it waits on your permission, and exited when it exits.
Each message spends the sender's budget for offers, so two agents can't keep writing to each other forever. Reading spends nothing.
Waking an idle agent
The agent's own hooks hand a message over: when its next turn
starts, when its turn ends, or when it runs
picache message read. An agent sitting at its prompt
fires no hook, so on its own it would get a message only when you
next type to it. To avoid that wait, cherry π types one short line
into its pane, which starts its turn:
[cherry-pi] message from @claude·3f2a (in your context)
Like herdr's hc send, the wake waits for you. If
you're typing in that pane, it waits until you stop.
If that agent's hooks have never been seen handing a message over,
the line asks it to run picache message read
instead.
Tell Picache, and hear back
You can also leave the command to Picache. Tell it in plain words,
such as ask codex to check the build, and it writes the
message itself, from your chat. The agent writes back with
picache message picache …, and Picache's next turn
in that chat has the answer.
An agent can write to a chat only after Picache has written to it
from that chat, so no agent can start talking into your
conversations. As an address, picache means the
newest such chat, and picache·a1b2 names a particular
one.
What you see
| Where | What |
|---|---|
| An agent pane's header | ✉ 2 while two messages wait for its next turn, and ✉ after pause while a wake waits for you to stop typing there. It clears when the agent takes them. |
| Inbox → Conversations | Every conversation between agents, and with you or Picache, each message with its type (question, answer to ev_81c2, handoff · fix/parser) |
| The deck, with a conversation selected | It says MESSAGE, and what you send goes to every agent in it whose pane is open |
| A Picache chat | What an agent wrote back, dimmed, at the turn that took it. It stays in the chat's history and isn't fed into later turns |
| The agent's own pane | Whatever that vendor's app draws for context its hooks add, and the wake line |
Who may write to whom
By default an agent writes only to agents in its own layer. Each layer can let other layers in, or keep them out, for itself:
picache layer messages from web # this layer takes web's agents' messages too
picache layer messages none # its own agents only
picache layer messages any # any layer's
picache layer messages default # as the workspace says
picache layer messages # what it says now
| Setting | Default | What it does |
|---|---|---|
inbox.messages | layer | Whom an agent may write to: its own layer, any agent pane here (workspace), or no one (off) |
inbox.message_notes | on | An agent hears when an agent pane it opened finishes a turn, waits on you, or exits |
inbox.message_wake | idle | An idle agent is woken with one line once you pause in its pane. off leaves the message for its next prompt |
Picache has the same commands as tools (message and
messages, in the inbox group), and so
does an agent given the verbs over MCP.
The web desktop doesn't wake an idle agent, so a message to one there waits for its next prompt. How a vendor's app draws what its hooks add is up to that vendor.
Messages stay on one machine. To reach another machine, send an issue (above).
Rooms and the Bakery
Rooms are for people on paired machines, across the internet.
- Each message is signed by the machine that wrote it.
- It's accepted only from a member.
- It waits in an outbox while a machine is away.
Beside each room is a side room with the same people, where agents
may be asked in. An agent named there with @ answers
once. It reads the room's last few messages and its own folder,
and it changes nothing.
The Bakery is public: a set of rooms on rednet, joined by name like IRC, and off until you join. Each person goes by a name generated from their key. Nothing is kept unless you choose, and agents are never allowed.
Rooms carry messages between people. A single layer can be kept in step with another machine (section 01), but a whole workspace can't.
Confinement and .pi apps
A capsule is a confined process that can talk only to the harness.
Every capsule starts through a single entry point, which this manual calls the door. The door picks the strongest confinement the machine has. A machine that can't confine runs no capsules, and says why.
| Platform | Confinement |
|---|---|
| macOS | Seatbelt |
| Linux | bubblewrap where it starts, otherwise Landlock |
Capability checks govern what a capsule asks of the harness. The confinement limits what it can do on its own.
The signer and the P2P sidecar
Two processes start outside the door: the signer, which holds the keys, and the P2P sidecar, which faces the network.
The sidecar has a box of its own, apart from the signer's keys. It can write only in its own folder. The only local sockets it can reach are those in its own socket folder (its own, each workspace's remote-desktop socket and the ones desktops dial from) and the system's name lookup.
- On macOS, the box is Seatbelt.
-
On Linux, it's bubblewrap. On kernel 6.12 and later, Landlock
stops the sidecar from connecting to the host's abstract Unix
sockets, though it can still bind a name there. Otherwise it
gets a network of its own behind
pasta, which keeps it from them entirely. With neither, it's boxed but can reach those sockets, and its log says so. - Where only Landlock is available, the sidecar runs unboxed and logs that.
Hosting capsules for other machines is opt-in, and a host that can't confine refuses them.
Capsule profiles
A capsule is defined by a named profile with five parts. Both the operating system's boundary and the harness's seal (the verbs the capsule may ask the harness for, fixed when it starts) are built from that one profile, so the two can't disagree.
| Part | Decides | For example |
|---|---|---|
| image | What runs inside | cherry π's own APE running Lua, an agent's CLI, or a command |
| ground | The files it sees | Sealed (copies only), one folder, or the pane's folder and named read-only roots |
| network | Where it can connect | Nowhere, a proxy with a list, or open (labelled as such) |
| broker | What it may ask the harness for | Verbs sealed at start: a script's are its caller's tools, a clean room's none |
| limits | How much | Seconds, CPU time and file size. A script's budget also caps its calls, writes and runs. |
The seal can only narrow what a capsule may do. A request outside it is refused, and a request inside it still goes through the policy for its verb.
A capsule can never do more than whoever started it could do directly, and it ends when whoever started it ends.
If the harness itself dies first, a clean room (below) on macOS, or under Landlock on Linux, runs on, still confined, until cherry π next opens. Then it is stopped. Its out/ folder is kept and its copies of the inputs are removed.
Each capsule's record names the confinement that held it, which is not always the one that was asked for.
Run a clean room
A clean room runs a command, or a Lua script with
--lua, on copies of files. Use it to open an archive
you were sent, run a repository's tests offline, or convert a
document.
picache cleanroom invoice.zip -- unzip invoice.zip -d out/
picache cleanroom notes.docx --for 15m --tools -- pandoc notes.docx -o out/notes.md
picache cleanroom data.csv --lua summarize.lua
- It has no network, and no view of your folder, your home or cherry π's state.
-
What it writes to
out/is kept in cherry π's state, and the reply lists it with its path. Everything else is thrown away, and the originals are never touched. -
It runs for five minutes by default. You can set another time
with
--for, and anything longer than an hour is cut to an hour. -
It can use the system's own tools. With
--toolsit can also use what Homebrew installed, read-only.
Picache also offers the clean room as a tool.
In the Capsules pane, Run one starts a capsule for a purpose: one prompt and its answer, a room for inspecting files, or a fifteen-minute agent-code session. The capsule runs on this machine, or on a paired machine when this one can't confine. An interactive capsule runs only on this machine.
Agent boxes
A vendor agent such as Claude Code, Codex or Pi starts confined in a box by default. The whole CLI runs inside it, including its hooks, its file tools and the MCP servers it starts.
picache box claude # its box settings
picache box claude launch standard # or confined, the default
| The agent can | The agent can't |
|---|---|
| Write its pane's folder and its own configuration | Read the rest of your home |
| Read its own install | Read cherry π's state |
Use its terminal and picache | Become root (Use sudo) |
Launch is a per-agent setting with two values,
confined and standard, and only you can
change it. A standard launch starts the agent without the box,
and its record says so.
When an agent starts, cherry π looks through its folder for
cherry π state and repositories. It goes at most 8 folders deep,
through at most 100,000 entries, for at most five seconds. In a
node_modules folder it checks each package but does
not walk every file inside it, so a .git or cherry π
state deeper inside a package isn't found.
If cherry π can't look through the whole folder, or can't open a folder inside it, the agent won't start in a box there. Start it in a smaller folder, or launch it standard.
On Linux, the box hides the cherry π state it knows of, and any it finds in the agent's folder at the start. State made there later isn't hidden.
On macOS, an agent can commit, merge and stash in the
repositories its folder holds at the start. It can't change their
config or hooks, or make a .git anywhere in its
folder. So git init, cloning into its folder and
updating submodules fail there. Do those yourself, outside the
box.
It also can't rebase. Both kinds of rebase (including
--apply), git am and a cherry-pick of
several commits fail in the box. Each would leave state behind
that your next git rebase --continue or
git am --continue would pick up and carry on with as
you. Rebase outside the box.
On Linux, those repositories are read-only, so the agent can't
commit. It can make a new repository in its folder, though. When
you run git in that repository, git runs its hooks and settings,
such as core.fsmonitor. Check a repository an agent
made before you run git in it.
On either system, an agent can leave a bare repository in its
folder. Git accepts one in any folder, with no .git
in its path, and the box doesn't catch it. Run
git config --global safe.bareRepository explicit so
your git won't use one unless you name it.
On macOS, when a boxed agent ends, cherry π waits up to three seconds for what it started to finish, and stops the rest. Then it looks again through the places in the agent's repositories where git writes, for a link or a file with a second name (a hard link). Your git would write through such a file as you. If it finds any, the agent's terminal shows each path:
cherry-pi: the agent's box left in its repository what your git would write through, as you, and git does not make. Look at each and remove it before you run git there:
/path/to/project/.git/logs/refs/heads/x/main
The same text goes into git-sweep.txt in the agent's
run folder, under confine/agents/ in cherry π's
state. Remove each path before you run git there. The agent's
next box in that folder won't start until you do.
Before it makes its own git writes in a repository that a box
guarded, cherry π looks there too. Its own writes are staging or
committing in the Changes pane, and accepting a goal. If it finds
anything, the write is refused with
confine_git_sweep, naming each path.
Git you run yourself isn't checked, so don't run git in an agent's repositories while the agent runs.
When the agent ends, cherry π doesn't wait for a process the agent started in a process group of its own.
The first look at a repository goes through at most 200,000 entries, for at most five seconds. After a look that finds nothing, cherry π looks again only in folders that have changed since. A look that is cut short counts as finding something.
When an agent won't start in its box, it says why. These codes concern its repositories:
-
confine_agent_gitdir: a.gitfile or link, or a repository'scommondir, leads to a place in its folder the agent could make or swap. Such a place is a folder that doesn't exist yet, a file, a path through a link, or the agent's folder itself. Your git would use whatever the agent put there. Point the.gitat the repository itself, or remove it. -
confine_agent_git_link: on Linux, a.gitin its folder is a link. The box keeps what it leads to read-only, but not the link, which the agent could swap. Replace it with a.gitfile that saysgitdir: <path>. -
confine_agent_filesystem: on macOS, its folder isn't on APFS, or its file system can't be identified. The box matches names the way APFS finds them. On HFS+, for example, git finds.gitunder names the box doesn't match. Use a folder on APFS. -
confine_agent_git_sweep: a repository in its folder still holds what an earlier box left, as described above. The message names each path. Remove them.
For any of these codes, you can also launch the agent standard.
Network access
When an agent's network setting is ask or
allowlist, the only thing it can reach from the box
is a proxy that cherry π runs outside it. The proxy carries
connections to what the agent's list names.
picache box claude allow registry.npmjs.org # add a rule
picache box claude allow '*.githubusercontent.com:443'
picache box claude unallow registry.npmjs.org
picache box claude network ask # for anything else
network | For a host not on the list |
|---|---|
ask | You're asked (the default) |
allowlist | Refused |
open | Allowed, with no list (below) |
Each list starts with the vendor's own endpoint, measured under the box. Names that resolve to this machine or a private network are refused unless a rule names that address.
The proxy decides by host name. It carries HTTPS connections, which it never reads inside, and plain HTTP requests. When you're asked, your answers are kept as receipts, and the proxy's log keeps the rest.
With open there is no list. A proxy runs only when the
agent's key is injected, to carry its vendor's hosts to the
injector. The agent can reach this
machine's loopback and your local network, and it can listen on
TCP ports.
It can also reach other agents' proxies on 127.0.0.1, which don't
check who is calling. If another agent has its key injected
(Keep the key out of the box), an
open agent can send requests through that agent's
proxy, and they go out with the other agent's key.
On Linux,
open needs kernel 6.12 or later, where Landlock
stops the agent from connecting to this machine's abstract Unix
sockets. An X server's socket, for example, takes keystrokes. On
an older kernel, an open box is refused.
Sign-in in the box
Claude Code keeps its macOS login in the Keychain, which the box
can't reach, so in the box it keeps its login in a file in its
own config. Sign in… on the agent's Profile menu
runs its own sign-in command in its box, with the network open
for that command alone. Token… keeps a token from
claude setup-token sealed instead.
On Linux the box is bubblewrap. With ask or
allowlist, the proxy is bridged into the agent's own
network namespace. An agent that hasn't been measured under
bubblewrap won't start in a box there. The pane says why, and you
can launch it standard.
The box has two limits:
- A confined agent can read and write a config file that a dotfile manager links from elsewhere.
- The box shares the kernel with the rest of the machine. It isn't a virtual machine.
Keep the key out of the box
A key handed into the box sits in the box's environment. Anything the agent runs can read it, such as a test runner, a package's install script or an MCP server.
picache box claude credentials inject # a stand-in goes in the box
picache box claude credentials hand-in # the real key goes in the box
When an agent's Key setting is
inject, it gets a stand-in key of the same shape
(sk-ant-oat01-cherrypi-…) and starts as usual.
- When it calls its vendor's API, the proxy hands that connection to cherry π's signer, outside the box.
- The signer, which already holds the key sealed, opens it and swaps it in for the stand-in.
- It sends the request on to the vendor over verified TLS. The answer streams back unchanged.
The key is never in the box, and outside it only the signer opens it.
credentials inject set. Until an agent has run
injected on this machine, the default is hand-in (below).
The agent trusts a certificate made for this run alone, which can vouch only for its vendor's hosts. That certificate's private key is deleted as soon as the run's certificates exist. The signer's injector ends when the agent does.
Which sign-ins can be injected
Claude Code can be injected when its token or API key is kept in cherry π. With inject set, the login file in its config is hidden from the box, so the real key isn't beside the stand-in.
Codex can be injected when it uses an API key from Keys. Its ChatGPT sign-in can't be, because Codex renews that sign-in with a token in the request body and the injector changes only headers. With inject set, that sign-in's file is hidden from the box and Codex uses the API key. To use the ChatGPT sign-in, choose hand-in.
You give the agent its key the same way for both, with Token… on its Profile menu or a key in Keys. Choose between inject and hand-in with the Key row in the agent's Profile, or with the commands above.
An agent defaults to hand-in until it has run
injected on this machine, and to inject after that.
If the setting is inject and a launch can't inject, the launch is
refused with the reason. It does not fall back to handing the key
in without saying so.
Where your secrets go
A key, a token or a password is opened only where it's used. Everything else gets its name, its last four characters, or nothing.
A token or an app's secret typed on the command line is refused, because it's then in your shell's history and in the list of running processes. Pipe it in instead:
pbpaste | picache box claude token
pbpaste | picache app set my-app api_key
picache app set my-app api_key --clear # forget it
Every picache call is recorded as a receipt, so what
a receipt keeps passes through two filters. First, a verb that
knows an argument is secret hides it. Then anything shaped like a
key, wherever it appears, is cut down until only what kind of key
it was remains.
Each pane's PICACHE_TOKEN is that pane's authority.
The pane's record keeps only a digest of the token, so a copy of
cherrypi.db gives nobody a pane's rights.
Files that hold a secret, or might hold one, are open to your user alone:
| File | Mode | Holds |
|---|---|---|
cherrypi.db, its -wal and -shm, and its backups | 0600 | Receipts, pane records |
secrets/credentials.db | 0600, folder 0700 | Your keys, sealed |
identity/ | 0700 | The signer's root key and identity keys |
| A job's log | 0600 | What a job printed |
The key log (CHEWY_KEYLOG) | 0600 | Keys pressed, with typed text kept only as <text> |
Other processes get no more than they need. A capsule inherits no environment. The P2P sidecar gets none of the signer's variables and no variable named as a key, and neither does the program that opens a browser.
On Linux, a pane that may use sudo starts through your systemd user session. The session reads the pane's variables from a file only your user can open, which is removed once it has been read, so no value appears on a command line.
An agent's key is handed into its box by default until that agent
has run injected on this machine
(Keep the key out of the box). Text an
agent types into a pane with picache send is
filtered only by its shape, and a password has no shape the
filter can recognise.
How it compares
Agents that ship a sandbox put the commands they run inside it and leave the agent itself outside. The box in cherry π holds the whole agent, for every vendor alike.
Each row was checked against that tool's documentation on 18 October 2026:
| Tool | What is boxed | On by default | Network |
|---|---|---|---|
| cherry π | The whole agent, any vendor, and every capsule | Yes | Per agent: ask, list only or open |
| Claude Code | Its shell commands | No | A proxy whose domain list starts empty |
| Codex CLI | The commands it spawns | Yes | On or off |
| Gemini CLI | Tool calls, or the whole CLI in a container | No | Open, or a proxy you supply |
| Cursor | Terminal commands | Not stated | A domain list, plus its defaults, or all |
| Pi | Nothing in core. A community extension boxes its tools. | No | Rules, plus a reviewer per connection |
| Docker Sandboxes | The whole agent, in a microVM | When used | A host proxy with policy modes |
Compared with these tools, cherry π has no virtual-machine tier for hostile binaries. Like Docker Sandboxes, it can keep the key out of the agent's reach (above) when the agent's Key setting is inject.
Sources: Claude Code, Codex, Gemini CLI, Cursor, pi-sandbox, Docker Sandboxes.
Addons and .pi apps
There are two tiers of extension: addons and .pi apps. The Extensions page covers both in full, along with Cellar Door's worlds.
| Compared | Addons | .pi apps |
|---|---|---|
| Made by | cherry π (first-party) | Anyone |
| Ship | Inside cherrypi.ape, with no folder to drop one into | As one .pi file |
| Plug into | Registries the core owns: P2P backends, peer discovery, signers, model backends, knowledge, worlds, terminal carriers, verbs, goal gates, app providers | Verbs, tools, panes, terminal programs, hooks |
| Run | In the harness, and may run a sidecar | In a capsule, holding only what they were granted |
picache addons # list them
picache addons enable p2p_holepunch
The core itself is never a plugin. It is made up of authority and policy, receipts and the ledger, confinement, the addon loader and the verb registry.
A .pi file is one SQLite file that holds the whole
app: its manifest (which holds its signature), its handlers and
its assets.
Outside the signature, it holds tables the app declares for its own data. Those tables are written through the harness's gate (section 10), one statement at a time, and journaled. When a new version is installed over the old, they are carried over and the old file is kept as a backup. A version that changes a table's shape without a migration is refused before anything is written.
An app signed by this machine or a paired one installs at once. Anything else, including an unsigned development folder, asks first and is marked as such wherever it shows. Each capability it asks for is granted separately.
What an app can add
An app can add:
picache <app> <verb>commands- tools for Picache
- panes drawn from A2UI
-
its own program in a terminal pane, which needs a grant of its
own,
app.terminal, because a program there hears every key - hooks, which need
app.hook
A hook hears each call before it runs and its result afterwards. It may refuse a call, warn, add a note or make a suggestion, and whatever it does is labelled as the app's.
A hook can never approve a call, change a call or its result, or answer a question cherry π asks you. A hook that is slow or fails counts as silent.
Meta, the first app, inspects and packages .pi files,
including itself, and asks you each time it signs.
After a crash, an app's work is recovered the same way as a tool's. Work that takes longer than one call runs as a loop the harness steps, and never as app code left running (section 10).
An app's own data, on every machine
An app with appdata:own keeps rows in the tables it
declares. Its pages in Cherrytop, the browser desktop, read and
write those rows by revision.
A write names the revision it read. If another window wrote first, the write is refused and the reply gives the revision that is there now, so nothing is overwritten unseen. Every other page of the app is told to read again.
To keep an app's rows the same on a paired machine that has the same app installed:
- Share it here:
picache shares add app <id>. - Ask for it there:
picache shares ask <machine> <share>. - Approve the request here:
picache shares approve <request>.
From then on each side sends the other its changes. To see what
syncs, run picache app sync. To see the writes that
lost a conflict, run picache app conflicts <id>.
To stop syncing, run
picache shares revoke <request>.
Only the two apps linked by the share, signed by the same publisher, take each other's rows.
Recovery and evidence
After a crash, cherry π can say what happened and what didn't, because it keeps two records apart: how far a task got, and whether its effect changed anything.
| Record | Values | Says |
|---|---|---|
| A task's status | queued, running, blocked, completed, failed, cancelled | How far the work got |
| An effect's outcome | applied, not_applied, uncertain | Whether the world changed |
A step that ran hasn't necessarily had its effect.
Settle an effect in doubt
The harness writes down the intended effect before the call runs. If the call is cut off midway, its effect is left uncertain, along with any output it had saved. An uncertain effect blocks a retry until someone settles it.
Actions → Review recovery lists the waiting work of every conversation, including the ones you don't have open. It shows what's in doubt and the output saved before the stop, then offers the harness's choices: Mark Applied, Mark Not Applied, Retry Safely (offered only when repeating is harmless) or Stop.
Your choice is checked against a fingerprint of what you were shown. If the view changed under you, nothing is done and you're told so.
Resume interrupted work
When a harness opens, any work it finds still marked as running goes back in the queue, whatever the recovery mode (below). This restores the record and reruns nothing.
Running the work again is another matter, because its effect may already have happened. Who resumes it is a setting, Actions → Recovery mode, and the recovery list's title shows the current mode.
| Mode | Resuming |
|---|---|
| Manual | A person, task by task |
| Supervised | A person: the default |
| Automatic | Unattended, within a budget, gated |
A resume is a new turn. The old ids and receipts stay as the record of the interruption. A task holding an effect in doubt is refused. It is neither resumed nor skipped.
- The new turn picks up where the old one stopped, in the same folder and under the same limits, and never beside a running turn.
- It's told what the interrupted turn already changed and how that came out, so it doesn't repeat it.
- The old task is marked as resumed by the new turn, so it isn't offered again.
Actions → Resume all ready runs the ready queue up to a budget and
reports each outcome, as in
2 resumed · 1 deferred · 1 in doubt.
A command that Picache runs is a job (see Jobs below). The job is tied to the call that started it before that call begins waiting on it. If the harness dies during the wait, the job keeps running and writes its own exit status.
When the harness opens again, an effect whose job has exited is settled from that record and the job's output, the same way you would settle it. If the job is still running, the effect stays in doubt until the job ends. If it was cancelled or is gone, the effect is left for you to settle.
Apps after a crash
An app's handler runs in its capsule (section 09) and can reach nothing from there. It asks the harness for every change, one at a time, even changes to its own tables.
Inside a turn, each ask is saved on the turn's task before it runs, like Picache's own calls (section 03). That holds whether the ask comes from a tool, a command or a hook. A crash midway leaves the ask uncertain, so it is never done twice without anyone noticing.
Review recovery names the app and what it asked. The resumed turn is told whose write it was and goes on as a new turn. The app's call is not revived.
Until you settle it, the app can't be upgraded or removed, so the version you are deciding about is always still there.
Work that takes longer than one call never runs inside the app. The app starts a loop run instead, and the harness steps it, saves between steps and recovers it like any other run (section 04). Two grants control this.
| Grant | Lets an app |
|---|---|
ai.redloop.manage | Write and delete its own loop definitions, and start, continue or stop its runs |
ai.redloop.read | Read loop definitions and its own runs |
The app names its definitions under its own id and names a layer by id, never by path. The answers it gets contain no folders or file paths.
You're asked each time the app writes or deletes a definition, or starts or continues a run, and it sees only its own runs. The approvals inside a run stay yours, and so does the scheduler.
Jobs
Long work is a job: downloads, tool installs, conversions, captures, indexing a layer's files, and the commands Picache runs.
A job is a durable record of its kind, state, phase, exact bytes, rate and log. The record is updated as the job changes, and each change is announced as an event.
- Many run at once.
- Each can be cancelled, and a download can also be paused and resumed.
- Each survives a restart.
- A finished download is checked against its pinned SHA-256 before it is moved into place.
Panes only show jobs. That is why the Downloads and Tools panes never disagree about how far a job has got.
Runs
Every terminal is a run, and every run has a record. The Runs pane keeps these records across restarts. Each one says:
- which agent or program, in which layer and folder;
- when it started;
- each time it waited on you, and why;
- how it ended: closed by you, exited with its code, or ended when cherry π closed.
The evidence chain
Receipts are stored as rows. The ledger is separate, and it holds commitments.
Each governed turn commits one ledger entry,
turn_receipt, in the same transaction as its last
receipt. The entry's digest binds every receipt the turn wrote. A
file operation commits an entry of its own, and so does settling
an effect in doubt.
The entries form a hash chain. Each entry's id is a BLAKE2b-256 hash of its body, and each body names the entry before it. The newest entry therefore commits everything before it, and changing a covered receipt breaks verification.
Each layer keeps its own chain in its own store, and the workspace's chain anchors it. The layer's first entry names the workspace's newest entry, and the workspace records an anchor that names the layer's newest entry.
A layer that leaves the workspace takes evidence with it that verifies on its own. Actions → Verify evidence chain checks every chain and anchor, and says where a chain broke.
Actions → Seal this session records a receipt in the chain that is bound to a conversation's messages and receipts. Seals are unsigned.
A seal proves only that the covered rows haven't changed since it was made. It isn't third-party attestation, and it proves nothing against an owner who rewrites the whole store.
In a browser: the web desktop and .pi names
cherrypi -browser opens Cherrytop, this machine's web
desktop, in your browser, signed in, starting its web server first
if needed. cherrypi.ape --browser opens the lander
instead.
cherrypi -browser # Cherrytop; -web and -gui do the same
cherrypi.ape --browser # the lander
cherrypi.ape names install # serve it at https://cherry.pi
cherrypi.ape web status
The lander is the cherry mark on black. Click it and you hear a short boot sound, then Cherrytop opens.
-
/site/is this website, which the APE serves itself with no network involved. /guide/is the build's own docs.-
/machinelists the workspaces and the apps served here.
There's one web server per machine, and it belongs to no workspace. It listens only on this machine's loopback and answers only the names it serves. A request for any other name is refused, so a page elsewhere can't point its own name at the server and read what it serves.
Its pages are open to read. The web desktop and its APIs need a browser that you have signed in (see Sign a browser in).
Before you install the names, the lander is at
http://127.0.0.1:3141.
.pi name reaches this machine's web server.
Every step stays on this machine.
Install the names
cherrypi.ape names script # read what will run as root
cherrypi.ape names install # asks for your password (twice on macOS)
cherrypi.ape names status # check it as a browser would
names install puts the lander at
https://cherry.pi, with no port in the address. On
Linux it asks for your password once. On macOS it asks twice:
once for administrator rights, and once more to trust the
certificate authority in your login keychain.
On macOS it asks in a dialog, then puts four things in place:
-
The address
127.0.0.2on the loopback, so cherry π's names leave127.0.0.1:443to whatever else on this machine already uses it. -
Two rules for pf, the macOS packet filter, that send that
address's ports 443 and 80 to the web server. They load under an
anchor macOS already provides, so
/etc/pf.confis never edited. -
/etc/resolver/pi, which sends lookups for.pinames, and only those, to cherry π's DNS. -
A LaunchDaemon,
pi.cherry.names, that restores the address and the rules at boot.
Then it asks to trust this machine's certificate authority in your login keychain.
Install the names on Linux
On Linux, sudo asks for your password once, in the same terminal.
Lookups for .pi names, and nothing else, go to cherry
π's DNS through the system's own resolver. The files that do this
belong to cherry π, and which ones it adds depends on the system:
- Where systemd-resolved runs (Ubuntu, Fedora), a drop-in for it.
-
Where NetworkManager writes
/etc/resolv.confinstead (Debian's desktop), drop-ins that turn on its dnsmasq and forward.pinames.
A systemd socket passes ports 443 and 80 on 127.0.0.2
to the web server, and it does so at boot too. This machine's
certificate authority (see A certificate
authority that can only sign .pi) goes into the system's
certificate store (update-ca-certificates or
update-ca-trust).
Your own NetworkManager settings are never edited. If you set
dns=none or rc-manager=unmanaged
yourself, install leaves it alone and tells you.
On macOS and Linux alike, browsers with their own certificate store (Firefox
everywhere, Chrome and Chromium on Linux) get the authority too,
through NSS's certutil. If certutil is
missing, install names the package that has it.
names status explains any failure. What was installed
is recorded in ~/.cherrypi/web/names.json.
A certificate authority that can only sign .pi
.pi.
Each machine makes its own certificate authority the first time a
name needs a certificate, and that authority is constrained to
.pi.
It can sign cherry.pi and my-game.pi,
and browsers refuse any certificate it signs for another name. Its
key, which only you can read, therefore can't vouch for a site
outside .pi, such as your bank's.
names install prints its SHA-256, so you can check it
before you trust it.
The web server picks each name's certificate as the connection
opens, and sends a plain http:// request on to
https://. Each certificate lasts 825 days, and the
server issues a new one 30 days before it ends, so you never need
to renew one yourself.
Serve an app's pages
A .pi app can have pages. In its manifest,
web = { root = "site" } names the folder of assets
that is its site. It may name an index other than
index.html.
picache app serve my-game # https://my-game.pi
picache app unserve my-game
The app must ask for the capability domain.serve, and
nothing is served until you run app serve. An agent
can't run it for you.
Its files are then served read-only at
https://<id>.pi, straight from its
.pi, or from its assets/ folder while
you are developing it.
Each app has its own origin, so no app's page can read another's or the lander's. Its pages may run their own scripts, but they can't load anything from elsewhere.
An app without pages gets a card at that address instead, showing its name, description, version and signer.
Names belong to the machine, and these rules apply:
-
cherry.piis reserved, and so ispicache, the id of Picache's built-in chat. - An app's id must be a single DNS label to get a name.
- An id served from one workspace can't be served from another until it's unserved there.
app unserve stops serving an app. Denying it
domain.serve or removing the app also stops it.
The web desktop
Click the cherry on the lander and you're in Cherrytop, this
machine's desktop in a browser tab. cherrypi --gui
opens it directly. Cherrytop is a separate desktop from the TUI.
What you do in one doesn't change the other, except where both
read the same harness.
-
Windows hold a shell (
/terminal), the desktop TUI itself (/tui), a running terminal attached from a workspace, this website, a served app, or Picache's chat (Picache in a window). - The canvas under the windows is an infinite surface that you pan and zoom, with icons, widgets, frames and named spaces. Ctrl+Alt+D shows it. Press m to change its mode (ember, glass, graph, mono, silk), or g followed by a number to fly to a space.
The widgets read every workspace this machine has opened. One whose harness isn't running is read from its store, with nothing live. They show:
| Widget | Shows, and what you can do from it |
|---|---|
| waiting for you | Each question a turn stopped to ask (When something needs you): the call, why, and whether it's destructive. Approve once or decline answers exactly that question and resumes the turn, as Recovery does. |
| goals | Each goal's progress. You can stop a running one, accept or reject a finished one's work, or continue one that stopped short (Give an agent a goal). |
| running | Each open terminal run. View puts it on the canvas, live, and window opens it in a window. Both attach to the run itself (section 13). |
| agents, inbox, workspaces | Agents' states, active Issues, and which harnesses are running. |
| apps | Served apps. An app can offer widgets of its own: pages of its site, in the same sandbox as its windows. A widget can show live data by running one verb the app names for it, but only to read: it can't make the app change anything. Meta's Installed apps is one (right-click the canvas, Add app widget). |
| clock, machine, notes | The time, a count of what's here, and notes kept on this machine. |
An answer, a goal verb or an attachment from the canvas goes through the same harness calls the desktop uses, so each is receipted the same way. A question is asked in the page, never in the browser's own pop-up.
The windows and canvases are kept in
~/.cherrypi/web/web.db. Every change is recorded as
an event, so another open tab shows it at once.
Choosing Open in browser on a terminal pane in the desktop shows that same running terminal in a browser tab, already signed in for you. Typing in either shows in both, and closing the tab leaves the program running (From a browser).
Canvases, pinning and zoom
-
You can have several canvases. Make, rename and remove them from
the canvas button in the bar or from the palette
(Ctrl+K, then
canvas). Each canvas has its own windows. Ctrl+Alt+] and [ go to the next canvas and the one before. Canvas Zero is always there, and you can have up to 24 in all. - You set placement from the canvas's right-click menu: free, snap to grid, or locked. You can also turn on push others aside when dragging. A single item can be locked on its own with Lock in place.
-
Pinning a window (with
⇣in its header, or Pin focused window to the canvas in the palette) turns it into a canvas item that keeps running. Popping it out again turns it back into a window. The frame is never reloaded, so a shell keeps its session and an app keeps what it holds. - When you zoom out, items turn into cards and then icons, depending on the width each has on screen. A run's card shows the agent's own report, or that the run ended. Otherwise it says no report and does not guess. A shell's card shows its last lines, and the shell stops drawing until it's live again.
Picache in a window
Picache is the one app built into Cherrytop. Open it from the apps and it talks to the harness of a running workspace. You pick the workspace and the conversation, and the conversations are the same ones the TUI lists. A new conversation you start here shows up in the TUI too.
The chat is sandboxed because it shows a model's words, and a model's words are untrusted text. Even if something in them got past the page, it would reach nothing, because the frame has no sign-in and can only ask Cherrytop.
- A turn streams into the chat as it runs: your words, each tool call, a question, and the reply. stop ends the turn at its next step. Something you send while it runs is queued for when it ends.
- A question the turn asks shows in the chat. Pressing decline answers it at once. Pressing approve once… opens Cherrytop's dialog, which shows the call as the harness states it, not as the chat page shows it. Only that dialog can approve.
- The chat's card on a zoomed-out canvas shows the last exchange.
The chat's tools work in the workspace's own folder. Every model
call, the generic openai_compatible backend's
included, runs in the inference worker, so the harness keeps
answering while a model thinks.
When an agent proposes
Cherrytop belongs to you. An agent can see what's open on it, but it can't open, focus or move anything there. Instead it makes a proposal, with a note, and you answer it in the browser.
| Command | Does |
|---|---|
picache web status | Whether this machine's web server is serving, how many canvases, and how many proposals wait (yours among them) |
picache web canvases | The canvases, by id and name |
picache web instances [--canvas <id>] | What's open on one canvas: its windows, and each item by the id focus takes |
picache web open <surface> [--note …] | Proposes opening terminal, tui, site, activity, app:<id> or an attached run |
picache web focus <item> [--note …] | Proposes showing one item on the canvas |
picache web arrange <plan | -> [--note …] | Proposes where items go: [{ item, x, y, w?, h? }], as the argument or on stdin |
picache web proposals [--all] | An agent's own proposals still waiting. --all adds the answered ones, with your answer. You see everyone's. |
picache web withdraw <id> | Takes back one of your own proposals that is still waiting |
An arrangement moves canvas items and leaves windows where they are. A paired machine's turn can't propose.
Picache has the same commands as tools, in the web group
(Picache's tools).
Sign a browser in
The web server listens only on loopback, but every program on this machine can reach loopback, including an agent. So the desktop's pages and their APIs need a browser that you have signed in.
cherrypi --browser # opens Cherrytop, signed in
cherrypi.ape web login # sign a browser in to the desktop
cherrypi.ape web login --print # print the link instead of opening it
cherrypi.ape web sessions # the browsers signed in, each with an id
cherrypi.ape web logout <id> # sign that one browser out
cherrypi.ape web logout # sign every browser out
web logout ends them all.
-
Opening
/cherrytop,/terminal,/tuior/machinewithout signing in takes you to/login, which explains how to sign in and then brings you back. -
http://127.0.0.1:3141andhttps://cherry.piare different sites to a browser, so each is signed in on its own.web login --loopbacksigns in the first of the two. - A browser's session ends after 30 days without use, or after 400 days in all. Sign this browser out in Cherrytop's palette ends only that one.
- Apps shown in Cherrytop run sandboxed in a separate origin: an app's script can't call the desktop's APIs or read its page.
Commands
| Command | What it does |
|---|---|
cherrypi -browser, -web |
The same as cherrypi --gui: starts the web
server if needed and opens Cherrytop, signed in
|
cherrypi --gui |
Opens Cherrytop, the web desktop, signed in |
cherrypi.ape web login |
Signs a browser in with a one-time link;
--print prints it instead of opening it
|
cherrypi.ape web sessions |
Lists the browsers signed in: an id, which browser, where, and when each was last used |
cherrypi.ape web logout <id> |
Signs that one browser out |
cherrypi.ape web logout |
Signs every browser out |
cherrypi.ape names install |
Sets up the address, the ports, the resolver and the boot job, and trust for the certificate authority in the system and in browsers' own stores (one password on Linux, two on macOS) |
cherrypi.ape names script |
Prints what the install runs as root |
cherrypi.ape names status |
Tries https://cherry.pi as a browser would,
ticks off each piece and store, and lists when each
certificate ends
|
cherrypi.ape names remove |
Takes every piece away and stops trusting the authority |
cherrypi.ape web start, stop,
restart, status
|
The web server's lifecycle. stop waits up to
five seconds before it forces the server to stop
|
picache app serve <id> |
Serves an app's pages at https://<id>.pi
|
picache app unserve <id> |
Stops serving them |
Serving pages this way opens no port to the network.
Bitcoin and cRED
cRED is a credit mined against Bitcoin. A claim is an ordinary
Bitcoin transaction that pays up to three addresses beginning
1red, ground for the block before it. Anyone checks
a claim from public Bitcoin data and the published rules alone:
there is no server to ask and no witness to trust.
Four first-party addons carry it, each off until you turn it on:
| Addon | What it does | Needs |
|---|---|---|
bitcoin | Runs Bitcoin Core on this machine, and gives the others the chain | |
cred_core | Reads the chain for claims and counts them by the rules | bitcoin |
cred_miner | Mines here: a wallet of coins, the grinder, claims | cred_core, bitcoin |
cred_frontends | The hand loop, a full-screen program for a terminal pane, and the miner's page in Cherrytop | cred_core, and cred_miner to play |
picache addons enable bitcoin
picache addons enable cred_core
picache addons enable cred_miner
picache addons enable cred_frontends
Each network counts claims from a pinned block. Regtest, a private chain this machine mines by itself, counts from block 0. Signet, Bitcoin's public test network, counts from block 329,000, and every machine that follows it counts the same markers the same way. Mainnet is not open yet: its block is not pinned, so a mainnet node counts nothing.
Run a Bitcoin node
picache bitcoin install # download and verify the pinned Bitcoin Core, as a job
picache bitcoin start regtest # starts detached: it runs until you stop it
picache bitcoin # the install, and each network's node
picache bitcoin mine 101 # regtest only: mine blocks to this node's own wallet
picache bitcoin readers regtest # who follows the chain, and how far each has read
picache bitcoin log regtest 50
picache bitcoin stop regtest
A node outlives the harness and the desktop; only
picache bitcoin stop ends it. Each network keeps
its own data folder, its own ports and its own RPC credentials. Only
you install, start, stop or mine; anyone may read the status.
The build pins Bitcoin Core 30.2 for macOS (ARM, x64) and Linux
(ARM, x64). On Linux it is Bitcoin Core's own release, which
needs glibc's loader at its usual path; NixOS has none, and there
the install is refused with bitcoin_no_glibc.
Every command in this section takes signet, or
main, where it says regtest; only
mining blocks is regtest's alone. A signet or mainnet node is
pruned: it keeps only the blocks its readers have still to read.
A keeper job, shown in the Jobs pane, asks Bitcoin Core to drop
the rest every minute, from the first sync on. A synced signet
node takes about 4.5 GB, nearly all of it the chain state;
its block files stay under 150 MB. Leave about 6 GB
free for the first sync: Bitcoin Core stops, saying the disk
space is too low, when it can't write its cache.
How a claim counts
A claim spends up to three of your coins, each P2TR (key path) or P2WPKH. Input i carries a nonce in its sequence number, and the transaction's lock time is the block it was ground against, the anchor L. Output i pays the marker that input derives: a P2PKH address made from the coin's key, the coin's own outpoint, the anchor block's hash and height, and the nonce. Nothing else can produce it, so a marker can't be ground ahead of time or claimed with someone else's coin.
A marker counts when:
- the claim is mined in block L + 1, the very next one;
- its address begins
1red, in any case; - its address meets the difficulty: a hash of the address, the policy and the anchor falls under a fixed target (difficulty 12 on every network);
- it is one of the first three outputs; any after are ignored;
- its epoch still has room under the schedule.
The address is the amount. Its digit groups are
added up into naks (10,000 naks make one CRED), and its tier
multiplies them. In
1ReDfLGrbgawLxKaA71zYBWXwC76629Aa the groups 1, 71
and 76629 make 76,701; the 66 makes it a Pair, ×1.5,
so it pays 115,051 naks: 11.5051 CRED.
| Tier | After the brand, the address has | Pays |
|---|---|---|
| Mythic | a run of five of one character, or 1red again | ×150 |
| Jackpot | 777, or a run of four | ×30 |
| Double Red | red again, in any case | ×12 |
| Straight | three digits in a row, up or down | ×8 |
| Trips | a run of three | ×4 |
| Two Pair | runs of two of two different characters | ×2 |
| Pair | one run of two | ×1.5 |
| Plain | none of these | ×1 |
No marker pays more than 2,097,151 naks. Moving a marker's sats later does not move its cRED, which stays with the address.
Check cRED yourself
picache cred scan regtest # read the chain to its tip for claims
picache cred # the policy, how far the scan has read, markers by status
picache cred markers regtest 20 # the latest markers, and what each came to
picache cred balances regtest # naks per counted marker
picache cred miners regtest # naks per claimant key
picache cred card 1ReDfLGrbgawLxKaA71zYBWXwC76629Aa # what an address alone says
picache cred policy regtest # the rules and their hash
The scan reads each block with its inputs' previous outputs, so every claim is checked from the chain alone. A reorganisation takes back what it removes. A block whose previous outputs this node no longer has stops the scan instead of being guessed at. All of it is public data, so anyone may run these.
Mine
The miner keeps a wallet of its own: dedicated coins, one key each, every key in the signer. It never shares a key with another wallet, because a marker's key is the coin's key plus public numbers. The grinder needs no key at all.
picache cred-miner address regtest # a new key to fund (p2tr, or add p2wpkh)
picache cred-miner settle regtest # read the chain: funding arrives as ready coins
picache cred-miner split regtest 3 2000000 # three coins of 0.02 BTC: a hand needs three
picache cred-miner grind regtest # grind every ready coin against the tip
picache cred-miner hand regtest # the best hit per coin, and the hand they make
picache cred-miner preview regtest # what shipping now would send, and its postage
picache cred-miner ship regtest # sign and send the claim: it must make the next block
picache cred-miner settle regtest # after the next block: what counted goes in the ledger
picache cred-miner sweep regtest 1 # spend markers back into a coin when it pays
A hand can be one, two or three coins. A claim pays each marker 546 sats and its fee, and must leave at least 546 sats of change, so the smallest a signet faucet gives, about 1,400 sats a coin, makes a two-coin hand at 2 sat/vB.
The grinder is a pinned sidecar, one job per coin, shown in the
Jobs pane with its hits, rate and tries, such as
2 hits · 412,113/s · 9,301,544 tried. Its log is a stream of JSON lines, the same events
Rednode's miner wrote: progress about every fifty thousand tries
(attempts, rate, matches) and each match the moment it is found,
so a frontend can follow a grind live. Every hit it reports is derived again before it is
used, so a faulty grinder can waste time but never send a bad
claim. A hand whose block has been followed by another is
refused: it would be late. Settling records each counted marker
in the ledger once, as cred.claim.miner.settled.
status, coins, hand and
preview are anyone's to read. The rest make keys,
spend your coins, use this machine's processors or record in the
ledger, so they are yours alone. An agent can say which to type;
it can't type them.
Play a hand
Run the hand loop in any terminal pane:
cherrypi.ape cred-play regtest
Every block deals a hand. The loop grinds your ready coins against the tip, and the reel shows each qualifying address as it is found. Each is real, and each would count if shipped in time. Your hand is the best hit on each coin, up to three, with what it pays. Under it are the odds that the next minute improves it. They come from this machine's rate and the hits this session has seen, and say so. When the next block comes, the loop settles what you shipped and deals again.
| Key | What it does |
|---|---|
| s | Shows the claim first: inputs, markers, change, size and postage. y sends it; any other key sends nothing |
| f | Folds: stops grinding this block. The next block deals a new hand |
| g | Grinds the current block again |
| w | Sweeps markers worth spending back into a coin |
| m | Regtest only: mines a block |
| q | Quits, and stops grinding |
Postage is the ante. A hand worth less than its fee is yours to
fold. A block can come at any second, and the time already
waited doesn't make it any sooner, so shipping is always a call
you make. A claim that misses block L + 1 spends its
postage and counts nothing. Status is always written as a word,
never shown only as a colour, and NO_COLOR turns
colour off.
Watch the miner in Cherrytop
With cred_frontends on, Cherrytop serves the
miner's page as an app, cRED miner
(/app/cred/). Open it in a window like any served
app (section 11). Once a second it
shows the grind as it happens: the tip, and whether the node is
still syncing; the rules; the rate on a dial; hits found and
tries made; each coin's best hit and the hand they make; your
claims and how each was judged; and the latest markers in the
ledger. A new hit is celebrated.
The page only reads. Grinding, shipping and sweeping stay the verbs above, and yours. With no node running, or the miner off, it says so instead.
The Sakura protocol
Sakura is how a client reaches something that's already running in a workspace without starting a copy of it. That something is a terminal: a shell, a vendor agent, the desktop TUI in a pane. A client attaches to that run, gets its current screen, follows its output, types into it and leaves, and the program never notices.
The browser speaks it, and so does the Go client's library. The desktop's own panes don't use it: they reach their terminals directly. A browser tab showing a terminal, Cherrytop's terminal widgets and the desktop's Open in browser are Sakura attachments to the same run your pane shows (section 11).
The rest of this section is the wire reference: the frame, the handshake, the terminal stream, services, recovery, the browser carrier, and limits and errors.
Three planes over one run
A terminal has exactly one owner, its pump. The pump is the small process that holds the terminal's PTY and a ring of its recent output (section 01). Sakura adds no second owner. Over each run it carries three planes:
- The control plane decides who may attach to which run, and for how long. It covers tickets, the handshake, detach, resume and revoke.
- The stream plane carries the screen: a snapshot first, then live output in order, sent within the credit the client grants.
- The service plane carries typed calls with receipts. Typing, resizing and reading go here, so the client doesn't have to guess an input's outcome.
Carriers
Sakura frames are bytes. A carrier moves them and says who the caller is, but adds no authority of its own: the frames still authenticate, and the host still checks the run.
| Carrier | Who uses it | How a frame travels |
|---|---|---|
local-control/1 |
The Go client on this machine |
The harness's private control socket, which only a holder
of its 0600 client key can use. sakura_open
scopes a ticket to a run, sakura_exchange
carries one frame each way as base64, and
sakura_next takes the stream's next frame.
|
browser-http/1 |
A browser on this machine |
POST /api/sakura on the machine's web server,
which forwards only exchange,
next and revoke into the
harness's sakura_browser entry. Every request
carries the attachment's bearer
(below).
|
Both carriers work only on this machine. A desktop on a paired machine can't call any Sakura entry point. It attaches over the remote-desktop path instead (section 08).
The frame
Every frame is a 40-byte header and a payload of at most 64 KiB.
Integers are big-endian. The Go codec
(internal/sakura/frame.go), the Lua host
(runtime/addons/sakura/frame.lua) and the browser
(sakura-client.js) check the same golden vectors.
| Field | Meaning |
|---|---|
| magic, version | SKRA, then 1. Anything else is refused. |
| type | One of the 31 types below. An unknown type is refused. |
| flags | Bit 0: a response (a service result). Bit 1: an error, after which the context is gone unless the error says it is recoverable. Other bits are refused. |
| channel | 0 for control and services, 1 for the terminal stream. |
| seq, ack | Per channel and per direction. Control frames carry 0 and 0. Service calls count from 1. Stream frames count from 1 after the stream's generation begins. |
| length | The payload's size. Trailing bytes after it are refused. |
| epoch | The host's generation. A client's first HELLO carries 0. Every later control and service frame carries the host's. A host restart changes it, and an old attachment is then refused. Stream frames carry the stream's generation instead, which a fresh snapshot changes. |
Frame types
Thirty-one types are defined. The terminal attachment uses the ones marked used. The host refuses the ones marked refused.
| Code | Type | Direction | Handling |
|---|---|---|---|
| 1 | HELLO | client → host | used: version, nonce, features |
| 2 | CHALLENGE | host → client | used: host nonce, principal, carrier, limits |
| 3 | AUTH | client → host | used: ticket and proof |
| 4 | ATTACH | client → host | used: the run, channels, window |
| 5 | ATTACHED | host → client | used: channels, input acknowledgement, resume token |
| 6 to 8 | SNAPSHOT_BEGIN, _DATA, _END | host → client | used: the current screen, in chunks |
| 9 | READY | host → client | used: the stream is live |
| 10 | DATA | host → client | used: output bytes |
| 11 and 12 | INPUT, RESIZE | client → host | taken on channel 1 after READY, refused on channel 0. The Go client and the browser type and resize through services instead |
| 13 | EVENT | host → client | used: a resize of the run |
| 14 | WINDOW | client → host | used: more credit |
| 15 | ACK | both | used: a frame taken, or credit granted |
| 16 and 17 | PING, PONG | client → host, host → client | used: liveness, and renews the lease |
| 18 | CLOSE | host → client | used: the program ended |
| 19 | ERROR | host → client | used: a refusal, fatal or recoverable |
| 20 and 21 | DETACH, RESUME | client → host | used: leave without closing, or come back |
| 22 to 29 | OBJECT_BEGIN … OBJECT_ABORT | both | refused: reserved for object transfer |
| 30 | SERVICE_CALL | client → host | used: a typed call |
| 31 | SERVICE_RESULT | host → client | used: its result or refusal |
The handshake
An attachment begins with a ticket: 32 random bytes written as 64 hex digits, scoped to one connection context (each client gets its own) and one existing run. A ticket is good for 60 seconds and one authentication attempt. Four exchanges follow, all on channel 0 with seq and ack set to 0:
The proof is HMAC-SHA256, keyed with the ticket's 32 bytes, over:
"sakura.local/1\0" ‖ epoch (u64)
‖ len‖connection ‖ len‖principal ‖ len‖carrier
‖ len‖run ‖ len‖client nonce ‖ len‖host nonce # each len a u32
-
The principal is the host's real
local-uid:<uid>, never a field the client sends. - A lost answer is safe to retry: the identical frame gets the identical answer, and nothing runs twice. A retry with different bytes is a new request and is judged as one.
-
ATTACH must name the run the ticket was scoped to. Asking for
another is refused with
sakura_session_scopeand ends the context.
The terminal stream
Channel 1 carries the run's screen, using the codec
terminal.ghostty-snapshot/1. Nothing is live until the
client holds a whole, checked snapshot.
-
SNAPSHOT_BEGINgives the codec, the exact native ABI, the size (at most 16 MiB), its SHA-256, the grid, and the output cursor and resize revision the snapshot was taken at. -
SNAPSHOT_DATAchunks of at most 16 KiB follow, in order. -
SNAPSHOT_ENDrepeats the SHA-256 and gives the scrollback marker. The client checks size and digest, and decodes it fully, before it shows anything. -
READYcommits it. From then on,DATAcarries output bytes (at most 16 KiB each) andEVENTcarries the run's resizes, in the order they happened after the snapshot.
A snapshot holds both screens, the saved primary screen, scrollback, cursor, title, working directory and any half-parsed escape sequence, so the first live byte continues exactly where the snapshot ends. The pump's own terminal engine makes it, and answers the program's screen queries itself, so a browser never sends a reply a native client also sent.
- The initial window, the credit the client grants in ATTACH, is between 16 KiB and 1 MiB. Credit never exceeds 1 MiB.
-
If the client falls further behind than the pump's ring holds,
the host says so with a recoverable
ERROR,sakura_snapshot_required. The client asks for a fresh snapshot (see below). The screen it already shows stays until the new one is ready. -
A run that has drawn Kitty graphics can't be snapshotted:
the host refuses with
snapshot_graphics_unsupported, so it never sends a screen with its pictures missing.
Services
The Go client and the browser send typing and resizing as service
calls on channel 0, so each one gets an answer. The host also
takes INPUT and RESIZE frames on the
stream after READY, and refuses them on channel 0. A call and its
result look like this:
SERVICE_CALL seq=n ack=last result
{"service": "terminal", "op": "input", "request_id": "browser-7",
"args": {"data": "<base64>", "seq": 12}}
SERVICE_RESULT flags=response seq=m ack=n
{"request_id": "browser-7", "result": {"input_ack": 12}}
| Service | Operations | Notes |
|---|---|---|
terminal |
input, resize, read, resume |
Input is at most 16 KiB per call. Resize asks for 1 to 500
columns and 1 to 300 rows. Input and resize share one writer
sequence, seq in args, counted from the
attachment's input_ack.
|
sakura.service |
describe |
Lists the services and their limits. |
- Each context has one call outstanding at a time. Calls count from 1. Results have their own sequence and acknowledge the call they answer. A gap, a future ack or a changed duplicate ends the context.
-
input_ackmeans the pump admitted the bytes to the program's input queue, in order. It doesn't mean the program ran them. -
The host supplies the run and the attachment. Fields in
argscan't name another run or another lease. -
A refusal in a result (because the queue was full, for example)
is still an answer. The sequences advance and the attachment
stays. Only an
ERRORframe that isn't marked recoverable ends it.
Lost answers, detach and resume
Sakura's rule is that an operation is never repeated by accident, and never silently dropped.
- If an answer is lost, the client sends the exact same bytes again. The host recognises them and returns its stored answer, so nothing runs twice. Until that answer arrives, nothing later may overtake it: the browser pauses input and offers Retry.
-
If the pump can't say whether
an input was admitted (
command_outcome_unknown,command_backpressure), the client resolves that same input, with the same writer sequence, in a new service call. The pump acknowledges a duplicate it already has without typing it again. -
DETACHleaves the attachment, its counters and the run as they are.RESUMEnames the same run and the resume token, with the last frame the client took, and the host answersATTACHEDwith its counters. Withsnapshot: true, or when the client's history is gone, the stream starts a new generation with a fresh snapshot. Frames from the old generation can't touch the new one. - A harness restart makes a new host and changes the epoch. Old attachments are refused, and a client never resubmits uncertain input under a new attachment. Instead it gets a new credential and starts from a snapshot.
- An attached context holds a lease that lasts five minutes without a call, and any call, poll or ping renews it. A host keeps at most 64 contexts, counting those still waiting to authenticate. Leaving, expiry and revocation end the attachment, but the program keeps running.
From a browser
A browser attaches with a credential the harness issues for one run. You can get one in two ways without ever seeing it:
-
Open in browser, from a terminal pane's menu or
Actions, opens a one-time sign-in link to
/terminal/attach?workspace=<id>&run=<record>. That page asks the web server for its own credential, so the credential never passes through the clipboard, the terminal or a recording of it. - Cherrytop's running widget opens the same page as a widget or a window.
A script can also ask for one:
cherrypi.ape desktop -state <workspace>/.cherrypi -browser-attach <record>
That prints the credential, which is a secret, for you to paste
into /terminal/attach within a minute. It looks like
this:
{"protocol": "cherrypi.browser-attachment.v1", "carrier": "browser-http/1",
"connection": "<32 hex>", "ticket": "<64 hex>", "epoch": 71,
"session": "<the run>", "principal": "local-uid:501",
"workspace": "<sha256 of the data folder>", "expires": …, "max_payload": 65536}
Each request on the browser carrier looks like this:
POST /api/sakura
Authorization: Bearer <ticket until AUTH, then the bearer AUTH returned>
{"workspace": "<id>", "connection": "<32 hex>",
"operation": "exchange" | "next" | "revoke", "frame": "<base64, exchange only>"}
200 {"frame": "<base64>" | ""} # AUTH's answer also has "bearer"
400 a request the host will refuse again 401 no bearer
403 bearer refused, or the context expired 503 the harness can't be reached: retry
- The ticket is spent at AUTH. The answer to AUTH carries a new bearer that only the page holds. After that, the pasted credential can do one thing: repeat that exact AUTH if its answer was lost, until the attachment moves on. A copy left in a clipboard history can't read, type or revoke anything.
- The workspace is named by its id, a SHA-256 of its data folder, and must be in this machine's catalogue. A filesystem path, an actor or an RPC method in the body is never forwarded.
-
/api/sakuradoesn't need the desktop's sign-in cookie. The bearer is its own authority, and the page sends no cookies with it. Getting a credential from/api/desktop/actdoes need the cookie (section 11). -
The page polls
nextwhen it has nothing to do, backing off from 25 ms to 400 ms, and wakes at once when you type. Leaving the page revokes the attachment, with a request that outlives the tab. - Fit to window resizes the shared run for every viewer, so the page never resizes on its own. Joining keeps the grid the run already has.
Limits and errors
| Limit | Value |
|---|---|
| Frame payload | 64 KiB |
DATA, snapshot chunk, one input | 16 KiB |
| Snapshot | 16 MiB |
| Stream window | 16 KiB to 1 MiB |
| Browser input queue | 64 KiB. A larger paste is refused whole. |
| Grid a browser may ask for | 500 columns × 300 rows (a run may report up to 500 rows) |
| Ticket | 60 seconds, one attempt |
| Idle attachment | 5 minutes, renewed by any call |
| Connection contexts per host | 64 |
An ERROR frame's payload is
{"code": …, "recoverable": …}, and
recoverable appears only when it is true. Without
it, the context is gone, but the run is not.
| Code | Means |
|---|---|
sakura_auth_refused | The ticket or the proof was wrong. The ticket is spent either way. |
sakura_session_scope | ATTACH or RESUME named another run or token. |
sakura_epoch_mismatch | The host restarted. Get a new credential. |
sakura_sequence_gap | A service call skipped or acknowledged the future. |
sakura_snapshot_required | Recoverable: the client has fallen behind the ring. Resume with a snapshot. |
command_outcome_unknown | Recoverable: resolve the same input again before anything else. |
snapshot_graphics_unsupported | The run has drawn Kitty graphics, which snapshots can't carry. |
sakura_connection_limit | The host already holds 64 connection contexts. |
terminal_not_persistent | The run has no pump to attach to (when opening a credential). |
sakura_invalid_state | A frame the attachment's phase doesn't allow. |
Reference
This section lists every picache verb and every
setting in the Settings pane.
Read from cherry π's source at e67c71a8: 95 verbs, 49 of
them Picache's tools, and 25 settings. picache help
lists the verbs as they are now.
Every verb
Run picache help <verb> for a verb's usage and
what it needs. A verb's effect class says what it does to the
world, and it decides when you're asked
(section 03).
| Verb | Called by | Effect | What it does |
|---|---|---|---|
accept | you | record | Accept an offer to your layer (a patch is applied here, undoable) |
accepts | you | record | What a layer takes from a linked workspace or a paired machine, narrowed per layer |
addons | you | record | The first-party addons in this build; addons enable <id> grants what one needs and starts it |
agent | you | record | The agent for a folder: which answers here, declare one, attach or detach one |
app | you | record | .pi apps: what is installed, install and remove, pack a dev folder, grant what one asks for |
apply-layout | Picache, you | irreversible | Open a layout's panes beside a pane (yours unless --at), or as a new layer; it closes nothing |
ask | Picache, you | record | Ask another layer a question; it is answered there, reading its files, in a thread |
ask-operator | you | read | Put a form in front of the operator (A2UI JSON) and print their answer |
assign | Picache, you | record | Assign an issue to the agent in a pane run, and see or decide its delivery (the operator's) |
bitcoin | you | record | This machine's Bitcoin nodes (regtest, signet, main): install Bitcoin Core, start, stop, status, readers |
box | you | record | How each agent is started (confined or standard, its network, its list, its login) and how far it reaches on the desk |
brain | Picache, you | read | Brains attached to your layer: other layers or references, read-only |
cancel | you | record | Take back an offer from your layer that nobody has answered |
capture | you | read | What a pane shows: panes read, by Rex's name |
cat | Picache, you | read | Print a file (bounded at 256 KiB) |
cleanroom | Picache, you | read | Run a command or Lua script on copies of files, with no network and nothing else to reach; out/ is kept |
close | Picache, you | irreversible | Close a pane you opened (the operator may close any) |
close-layer | Picache, you | irreversible | Close a layer: its panes' programs end (Layer Zero stays) |
cohort | Picache, you | irreversible | Ask a vendor agent one question over ACP, in its box, with cherry-pi's tools; the answer is marked external |
connect | you | read | A confined agent's proxy asks whether a connection may go out |
context | you | read | Exactly what Picache's model was given on its last call |
cred | you | record | CRED as this machine reads it from the chain: the scan, markers, balances, and checking an address or a claim input |
cred-miner | you | record | Mine cRED on this machine: dedicated coins, the grinder against the tip, claims, settling, sweeping |
desktops | you | record | Desktops on paired machines: whether this workspace takes them, from which trusted networks, and who is attached |
do | you | read | Run a Lua script whose calls go through your token: picache.call, .split, .wait, .on … |
eval | you | record | Measure an agent: its core and own cases, non-executing, with a report kept |
events | you | read | The harness's events as they happen, one per line; --json for programs |
factory | you | record | Make .pi apps from a schematic: a product layer from its skeleton, a builder's goal, and checks that pack it for review |
find | Picache, you | read | Files whose path contains <text> (under your folder, or a subfolder) |
fleet | you | record | The fleet this machine is in: who can find it on the network |
float | Picache, you | arrange | Float a pane over the desk, out of the tree, raise or lower a float, hide and show the floats, or cascade them |
focus | Picache, you | arrange | Give a pane the keyboard: by name, or left, right, up, down, next, prev |
focus-layer | Picache, you | arrange | Bring a layer to the front: by name or place, or next, prev |
goal | you | record | Give a vendor agent a goal to work on in its own copy of this layer; Picache reviews it, you comment and send it back, and you accept or reject its branch |
grep | Picache, you | read | Lines containing <text> in files under your folder |
help | you | read | The verbs, what each may do, and how to call them; help <verb> describes one |
history | Picache, you | read | This conversation's own messages, receipts and checkpoints, searched or read exactly, past what the context holds |
identity | you | record | This node's identity and its agents' keys (identity init makes them) |
inbox | you | read | Offers to your layer that wait on an answer (--all for every one) |
issue | Picache, you | record | The Inbox's issues: list, open, show, reply; the operator moves and tags them |
jobs | Picache, you | record | Long work the runtime is doing: downloads, conversions, model servers, measurements |
kb | Picache, you | read | An agent's knowledge: search it (the atlas of every verb and tool, the operating shelf, the docs), list its sources, or refresh it |
label | Picache, you | arrange | Give a pane a title |
label-layer | Picache, you | arrange | Give a layer a name |
layer | you | record | Export a layer whole (history and files, signed), import one, fork one, share one live, list references |
layers | Picache, you | read | The workspace's layers, their folders, and the offers waiting in each |
layout | Picache, you | read | The layer in front as a tree: how it splits, each split's share, each pane; --json as a layout, --saved the saved ones |
link | you | record | Link another workspace (its folder): it may offer this one what --accept names |
links | you | read | The workspaces linked to this one, and what each may offer it |
loop | you | record | A goal pursued over several turns in this folder, each checked by the layer's test command, until it passes or the steps run out |
ls | Picache, you | read | List a folder (default: your pane's folder; --layer <name> for another layer's) |
machines | you | record | This machine's fleet: paired machines, requests to pair, the directory; machines name names one |
mcp | you | read | This pane's verbs as an MCP server on stdio, for the agent the pane runs |
message | Picache, you | record | Write to another agent's pane, as SendMessage: it gets it at its next turn or as its turn ends; read yours, see whom you can write to |
models | you | read | The models this machine has, and which one Picache uses |
move | Picache, you | arrange | Dock a pane beside another: its old place closes up |
move-layer | Picache, you | arrange | Put a layer at a place in the rail, from 2 (Layer Zero stays first) |
new-layer | Picache, you | irreversible | Open a new layer on a folder (the caller's unless named), in front unless --focus=false |
offer | you | record | Offer another layer a message or a change; it is applied only if accepted there |
outbox | you | read | Offers from your layer that nobody has answered yet (--all for every one) |
pair | you | record | Pair with a machine, or answer one that asks (pair approve [<request>] <code>, pair decline <request>, pair decline-all <node>) |
panes | Picache, you | read | The panes on the desk, or panes read <pane> for what a terminal shows |
patch | Picache, you | reversible | Replace text in a file (once, or exact with --all); whole lines match despite spacing or quotes; the old bytes are kept for undo |
profile | you | record | This machine's profile -- name, handle, bio, status, timezone, avatar, banner -- which paired machines see |
reach | you | record | Reach an address another machine serves: a loopback port here that connects to it |
reject | you | record | Reject an offer to your layer |
reply | Picache, you | record | Reply in an issue's thread (markdown; the text, or stdin) |
resize | Picache, you | arrange | Make a pane wider, narrower, taller or shorter by cells, or --to a size: it moves the divider beside it |
rex-layout | Picache, you | read | Inspect the configured local Rex session's window and pane layout |
rex-read | Picache, you | read | Read recent terminal text from a pane in the configured Rex session |
rex-split | Picache, you | irreversible | Open a Rex pane and run an approved local shell command |
run | Picache, you | irreversible | Run a shell command, after the shell policy has classified it |
runs | you | read | What ran in this workspace, newest first (an agent, or a folder's own: what ran in its folder) |
save-layout | Picache, you | reversible | Save the layer in front as a layout in the folder (.cherrypi/layouts/<name>.json) |
script | Picache, you | read | Code mode: a Lua script in a capsule whose only way out is the verbs, each one decided and receipted |
search | Picache, you | read | The workspace's search: conversations, runs, receipts and files |
send | Picache, you | irreversible | Paste text into a terminal pane and press Enter (a shell runs it); --wait answers when the turn it starts has settled |
send-key | Picache, you | irreversible | Press keys in a terminal pane: enter, tab, esc, up, ctrl+c … |
serve | you | record | Serve a port, or an installed app from its own capsule, at an address that is a key: to paired machines, approved ones, or anyone holding it |
shares | you | record | What this machine shares with its paired machines, and what they share with it |
skill | Picache, you | read | Teach Claude Code, Codex, OpenCode and Pi cherry-pi: show the guide, install the skill and hook |
snapshots | you | read | Your layer's folder as each turn found and left it: list, what changed, a file as it was |
split | Picache, you | irreversible | Open a terminal pane beside yours, or as a float over the desk, running a command or a shell |
status | Picache, you | read | Who you are to the harness, your folder, and Picache's model |
surface | you | arrange | Arrange a temporary team surface; dismissing never stops its agents |
surfaces | you | read | List lightweight pane arrangements in the caller's project |
swap | Picache, you | arrange | Two panes trade places; the tree keeps its shape |
task | you | record | Give another layer a task; it runs there once accepted, and its report comes back to you |
undo | Picache, you | reversible | Put back what a write or patch replaced, if the file is still as it left it |
unlink | you | record | Withdraw this workspace's link to another: it may no longer offer this one anything |
wait | Picache, you | read | Wait for a pane's program to end (its exit status), for text to appear, or for its state |
web | Picache, you | record | Cherrytop, the browser desktop: what is open where; propose opening or showing something |
witnesses | you | record | Paired machines that witness this machine's ledgers, one log at a time, and the logs this one witnesses |
write | Picache, you | reversible | Write a file from stdin or a text argument; the old bytes are kept for undo |
zoom | Picache, you | arrange | Fill the window with a pane, or restore it: --on, --off, or toggle |
The Settings pane
Ctrl+Alt+Q opens Settings, a surface of its own, where you left it. Its Settings pane has seven pages, drawn as cards, and each card says whether it's for this workspace, this machine or you. A choice saves at once. A choice for this workspace is also recorded as a receipt.
| Page | Holds |
|---|---|
| Web | Whether https://cherry.pi works, setting it up, and the certificate authority |
| Terminals | terminal.sudo.* |
| Agents | Recovery, goals, Picache and routing, the Inbox, and new agents |
| Sharing | clients.remote, and Machines |
| Desktop | Yours alone: theme, images, notifications, sounds, hints, layouts |
| Search | The search cache, its size, and what search leaves out |
| Workspace | The data folder, layer snapshots, and the rest |
Each setting in the next table has a fixed set of choices.
Others are described where they apply, such as
context.* and
loop.test_command.
| Setting | Choices (default in bold) | What it sets |
|---|---|---|
terminal.sudo.shells | on, off | Your shells can sudo |
terminal.sudo.agents | off, on | Agents' panes can sudo |
terminal.sudo.forget | on, off | Forget your sudo password before an agent types |
terminal.sudo.unboxed_agents | password, refuse | An agent outside its box, at sudo (macOS only) |
clients.remote | off, view, edit, full | What a paired machine's desktop may do here |
recovery.mode | supervised, manual, automatic | Work interrupted by a crash |
goal.workspace | copy, worktree | Where an agent works on a goal |
goal.commands | on, off | A goal's agent may ask to run commands |
picache.patch.match | forgiving, exact | How Picache's edits find their text |
picache.approval.unanswered | park, refuse | When nobody answers Picache's question |
picache.model.retry | once, off | When Picache's model fails to answer |
route.sticky | on, off | Keep a turn on one model |
route.egress | off, ask, on | A router may send a request off this machine |
route.qualify | required, warn | A router's model must have qualified |
picache.run.time | 10m, 1h, 4h, off | How long Picache's command may run |
picache.run.memory | 4g, 16g, off | How much memory Picache's command may use (Linux) |
picache.budget.turn | off, 50k, 200k, 1m | Tokens one turn may use |
picache.budget.cloud | off, 100k, 1m, 10m | Tokens the cloud seat may use in a day |
evidence.on_failure | closed, open | When a turn's evidence can't be recorded |
apps.dev_changes | hold, follow | When a dev folder app's files change |
inbox.delivery | next_turn, when_idle, check | When an agent gets an issue assigned to it |
inbox.messages | layer, workspace, off | Whom an agent may write to |
inbox.message_notes | on, off | Tell an agent about the panes it opened |
inbox.message_wake | idle, off | Wake an idle agent for a message |
layer.snapshots | on, off | Snapshot a layer at each turn |
When a call is refused
A refused picache call prints a code on standard
error, usually followed by a sentence, and the refusal is
recorded as a receipt.
picache <verb>: <code>: <what happened> # exit code 1
picache: cannot reach the harness at <socket> # exit code 2
With --json, the refusal goes to standard output as
{"ok":false,"error":…}. In a Chat, Picache sees
refused: <code>. Failures of the desktop's own
calls go to the Errors pane.
| Code | Means | What to do |
|---|---|---|
needs_confirmation | Your own call needs a yes: a write without the drive.write grant, or a command the shell policy calls destructive. | Answer Go ahead? in the terminal, or pass --yes. |
needs_operator | Picache or an agent needs your yes for this one call. | Answer in the client. A yes covers that call only. |
declined, denied | You said no. | Ask again if you meant yes. |
… nobody could be asked | No desktop on this machine could ask you, so the call failed closed. | Keep a desktop attached on this machine. |
waiting_approval | Nobody has answered yet, and the question is kept. | Approve or Decline it in Recovery (When something needs you). |
path_escape | The path, or where a link in it points, is outside the folder. Files refuses any symbolic link in the workspace. | Use a path inside the folder. No grant fixes it. |
bad_path | The path has a .. in it. | Name the file without ... |
unknown_verb | There's no such verb. | picache help lists them (Every verb). |
cohort_proposes | An agent tried to write without a goal. | Give it a goal, and its work comes back for review (Give an agent a goal). |
goal_todos_open | A goal still has todos open, so it can't be accepted yet. | Finish them, or mark yours done (Give an agent a goal). |
goal_no_remote | The layer's repository has no remote to push the goal's branch to. | Add one with git remote add, or offer the branch to a paired machine. |
confine_agent_unmeasured | This agent hasn't been measured under this machine's agent box, so it isn't started in one. | Use a measured agent, or start it unboxed, as you: picache box <agent> launch standard. |
conduit_branch_unrelated, conduit_branch_exists | An offered branch doesn't start from a commit this layer holds, or a branch of its name is here at another commit. | Fetch the base first, or rename the branch that's here. |
cohort_ceiling | An agent tried something irreversible. The most an agent may do is read and propose. | Do it yourself. No grant changes it. |
operator_decides | Only you can do this. The caller may propose it. | Do it yourself. |
budget_spent | A pane's caller used its share (200 writes, 50 runs, 100 offers, 500 arranges), or a script its budget. | If it is a script of yours, give it a larger --budget. |
focus_never | The agent's box says it never moves your keyboard. | Run picache box <agent> focus ask. |
hook_blocked | An installed app's hook stopped the call. | The message names the app and its reason. |
host_cannot_confine, confine_unavailable | This machine has no Seatbelt, bubblewrap or Landlock to confine a capsule. | For Run one, pair a machine that hosts capsules and send it there (section 09). |
… is not on this agent's list | A boxed agent tried to reach a host its list doesn't name. | Allow it when asked, or picache box <agent> allow <host> (Network access). |
| sudo fails in an agent's pane | Agents' panes can't sudo by default. | Change the terminal.sudo.* settings (Use sudo). |
effect_uncertain | A crash left a change in doubt, and cherry π won't repeat it blind. | Settle it in Recovery: Mark Applied, Mark Not Applied, Retry Safely or Stop (Settle an effect in doubt). |
remote_off | This workspace takes no desktops from other machines. | Change the clients.remote setting. |
remote_not_granted | That machine has no grant here. | On this machine, run picache machines desktop <machine> edit. |
remote_not_allowed | The machine's grant doesn't give what the call needs. | Give it a wider grant (What a remote desktop may do). |
layer_live_read_only | That conversation is another member's, in a live layer. | Continue it on their machine. |
agent_bundle_unsigned, agent_bundle_untrusted | An agent without a signature, or signed by a key this machine doesn't trust. | An unsigned agent never installs. With --untrusted, an agent from an unknown signer installs. |
refusing nonempty unowned state root, state root must not be a symbolic link | The data folder has other files in it, or is a symbolic link. | Pick an empty folder, or one cherry π made (The data folder). |
no harness to talk to | picache ran outside a cherry π pane. | Run it in a pane. |
Words used here
| Word | Means |
|---|---|
| Bakery | The public rooms on rednet, joined by name, like IRC. |
| box | Where a vendor agent runs by default. It sets what the agent may read, write and reach. |
| capsule | Anything confined, such as an app's handler, a script or a clean room. Each starts from a named profile, through one door in the harness. |
| clean room | A command or Lua script run on copies of files, with no network. |
| cohort | A vendor agent asked from a Chat, as with @claude. Its own write and run tools are off. |
| Conduit | How cherry π talks layer to layer, workspace to workspace and machine to machine. |
| Drive | A machine's own peer-to-peer file store, with no cloud and no account. Large shares and its profile travel from it as hashed chunks. |
| effect class | What a verb does to the world: read, record, reversible, arrange or irreversible. It decides when you're asked. |
| evidence chain | A hash chain of entries. Each turn adds one that binds every receipt it wrote. |
| fleet | The machines that can find this one. Only machines with its secret can. |
| goal | Work a vendor agent does on its own, in a copy of your layer. Nothing lands until you accept it. |
| grant | A capability you give. It stays in the workspace that gave it. |
| harness | The process, one per workspace, that decides what's allowed and keeps every record: cherrypi.ape. |
| in doubt | A change written down before it ran, whose outcome a crash hid. |
| layer | A project folder with its own Picache conversation, panes and store. Layer Zero is the scratchpad. |
| offer | A message, patch or file for another layer. Nothing changes there until you accept it. |
| Picache, picache | Picache is the agent you talk to in a Chat. picache is the command it, you and agents call the harness with. |
| pump | The small process that holds one terminal, so it outlives the desktop and the harness. |
| receipt | The record of one governed call, refusals included. A change and its receipt are saved together. |
| rednet | The public peer-to-peer network. Machines running cherry π find each other on a subnet of it. |
| seat | A place in Models for one model: Picache's, Small, Search, or Cloud. |
| signer | The sidecar that holds this machine's and its agents' keys. |