Skip to content

Agent computers: reference

For users using the Agents panel

Everything about driving a paired computer and about galopin itself, in depth. New here? Start with Agent computers, which pairs a computer in three steps. The operator's side of the same feature is The /code panel.

This page is the reference for the Agents panel and for galopin, the binary that runs on your computer. It is organised so you can jump to what you need:

You want to know… Section
what a computer will and will not allow, and how to set that Pairing options and the computer policy
what Plan, Ask and Allow do, and what the approval card offers Permissions
how the panel's parts work The panel in detail
how a session hands work to other sessions, profiles, committees Delegating work
timetables Scheduled actions
installing, updating, keeping it running, removing Installing and running galopin
what runs where How it works

Pairing options and the computer policy

The pairing dialog

The Pair a computer dialog (its heading reads Options for this computer) prints the command for you. It offers every enroll flag that sets what the computer will allow, so nobody has to edit the printed line by hand. Every control starts at enroll's own default — a control whose value equals enroll's prints no flag — with one deliberate exception: Bill to my default group starts ticked, so a computer paired through the dialog follows your console default group unless you untick it (enroll itself keeps requiring an explicit --default-group). The line above updates as you change things, with paths and globs quoted so they reach enroll as single arguments. (A control shown in an amber tone is one that opens something up.)

Common options, at the top:

Control What it adds to the printed command Default Use it when
Use a project's own opencode setup (amber) --allow-project-config off the computer opens repositories you trust, and you want their own opencode setup to load
Install opencode opencode's own installer line, pinned to the release galopin is tested against (curl -fsSL https://opencode.ai/install \| bash -s -- --version 1.19.3), before enroll off the computer is fresh and does not have opencode (the agent runs it as its coding engine); leave it off if it is installed

The computer has no limits of its own (ADR 0109). What a session may do is its Plan · Ask · Allow · Auto word in the composer (and, for Auto, the rows in Workspace → Agents), plus a fixed floor in galopin. The dialog prints no limit flag; enroll still accepts the retired --limit, --permission-max and --permission-rule for one more release, ignores them and warns.

Advanced, collapsed until opened (its summary counts what you changed, and the printed line carries every changed value whether it is open or not):

Control What it adds Default What it does
Bill to my default group --default-group ticked bills to the group the Pystino console's group picker sets, instead of pinning one group for this computer; untick to pin the group a plain enroll would pick. Change the mode later with galopin auth group
Terminals (amber) unticked: --no-terminal; a number other than 8: --max-terminals N on; at most 8 open at once the panel may open a real shell on this computer: anyone who controls your Cerea session can run commands as you, with no model and no permission rule in the way
Slash commands that run shell (amber) unticked: --no-command-shell on a slash command whose template runs a shell snippet may run it, before any permission is asked; plain slash commands work either way
Background subagents unticked: --no-background-subagents on a task can keep running after its parent turn ends
No agent tools --no-agent-tools installed installs none of the agent tools (session_* and schedule_*), so sessions cannot start or message each other, and agents cannot make schedules
Local deny (not in the dialog) --deny CLASS, repeatable (advanced, by hand) none refuses a class (edit, command, web, outside, secrets, agents, tools) on this computer whatever any session says; for an owner who does not fully trust the Cerea server
Allow free models --allow-free-models the gateway's models only models from providers other than the gateway's may be listed and used
Keep opencode's own providers --allow-opencode-provider gateway only opencode's built-in providers stay enabled next to the gateway's
Workspace folders (a list) --workspace-root PATH, one per entry anywhere workspaces may only be created under these folders
No file explorer --no-files read-only browsing keeps the explorer out of the computer's files
Allow saving files (amber) --allow-file-write editor on (unticked prints nothing) the editor can already save text files on a computer; ticking prints --allow-file-write, which only restates the default. Turn the editor off with --no-file-write (see Saving files)
Hide files (a list) --file-deny GLOB, one per entry the built-in secret list the explorer also redacts files matching these globs
Drop the built-in secret list (amber) --no-default-file-deny the secret list stays stops redacting private keys and credential files, and lets a command's @file reference read .env files, keeping only your globs

Reading secret files, work outside the project folder and shell commands are not computer settings any more: a session on Allow runs them, on Ask each is a card, and on Auto the user's per-class rows decide. Credential paths, writes into .git and CI files and a repeating agent are a card in every word but Plan (see Permission classes).

Connection plumbing (--issuer, --gateway, --cerea, --client-id, --creds, --output, --device, --device-endpoint, --loopback, --group, --shim-port, --no-discover, --yes) is not offered: the dialog fills in what it knows, and the rest you add yourself if you need it.

The command is chained with &&, so a failed step never runs the next one: the installer, then (if checked) opencode's installer, then enroll, then run, one step per line and one enroll flag per line; it pastes into a shell as is. It calls the binary by its installed path, ~/.local/bin/galopin, rather than a bare galopin, because on a fresh computer ~/.local/bin is not yet on the shell's PATH (if you set GALOPIN_INSTALL_DIR for the installer, edit the path to match). The dialog also lists the binaries and SHA256SUMS for a manual download, and the computers waiting for confirmation.

Nothing here lets a session do "anything": what a session may do is the Plan / Ask / Allow / Auto selector in its composer, inside the fixed floor and any local deny (see Permissions).

The computer policy

These are the computer's own vetoes. They are fixed at enroll time, stored in the computer's policy.json, and the chat can never loosen them over the link: whatever the panel sends, the computer refuses what its policy denies. galopin policy show prints what a computer currently allows. galopin policy set can only tighten; loosening anything needs a new enroll.

Policy Enroll flag Default What it decides Tighten later
Files --no-files, --file-deny GLOB, --no-default-file-deny read-only browsing, secrets redacted Whether the /code explorer may browse a workspace, and which files it redacts (see What the file explorer may see). Off: no explorer at all policy set --no-files, --file-deny GLOB
File writes --no-file-write turns it off allowed Whether the /code editor may save text files (see Saving files). A veto pair, like the terminal: the deployment's CODE_FILE_WRITE_ENABLED must not be false (it is on by default), and this computer must allow it. Files on the deny list are never written policy set --no-file-write
Terminals --no-terminal, --max-terminals N allowed; at most 8 open at once Whether the panel may open a real shell on the computer. A veto pair: the deployment must also set CODE_TERMINAL_ENABLED=true (see The terminal) policy set --no-terminal, a lower --max-terminals
Permission limits (retired) --limit, --permission-max, --permission-rule (accepted and ignored, with a warning, for one more release) none ADR 0109: there are no computer limits; policy.json's limits, max and rules are read, ignored and dropped at the next write; policy show says so
Local deny --deny CLASS (repeatable); policy set --deny CLASS / --no-deny CLASS none Refuse-only, advanced: a class the computer refuses whatever the session's word (see Permission classes); stored locally, never settable over the link
Safe directories --safe-dir PATH (repeatable, replaces the default), --no-safe-dirs /tmp, the temp dir, the toolchain caches (~/.cache/{go-build,pip,uv,pnpm,node-gyp}, ~/go/pkg/mod, ~/.npm, ~/.local/share/pnpm), never the whole ~/.cache, those that exist Directories outside the workspace whose contents never ask, for any session under any setting, subagents included (see Permissions). A safe directory is one the agent may write into — external_directory does not separate read from write, so a poisoned build cache is the residual risk, which is why the list stays short and never names the home folder itself, a config directory or a credential store. The ceiling key external_directory still caps every entry policy set --no-safe-dir PATH (removal only)
Agent tools --no-agent-tools installed Whether galopin installs session_list, session_read, session_spawn and session_send, and the schedule tools (schedule_list, schedule_create, schedule_update, schedule_delete), into the backend (see Sessions that talk to sessions). Denied: no tool at all policy set --no-agent-tools
Slash-command shell --no-command-shell allowed Whether a slash command's template may run its shell snippets. While denied, a command that expands shell, or whose shell behaviour is unknown (MCP prompts, ACP commands), is refused (see Slash commands) policy set --no-command-shell
A repo's own opencode config --allow-project-config ignored Whether opencode loads the config a workspace's repository carries. Ignored by default (see A repo's own opencode config) policy set --no-project-config
Background subagents --no-background-subagents allowed Whether the task tool may run a subagent in the background. While denied, background:true fails closed inside opencode; when allowed, a background child keeps running after its parent turn ends and its result returns as a synthetic message the panel shows (see Subagents) policy set --no-background-subagents
Models from elsewhere --allow-free-models denied: the gateway's models only Whether the model list may include providers other than the gateway's. By default only pystino/* models are listed and accepted, so spend always lands in the account the computer enrolled under. Cerea filters as well, and answers 403 to a disallowed model re-enroll
opencode's own providers --allow-opencode-provider denied Whether opencode's built-in providers stay enabled next to the gateway's. Off, the written opencode.json carries enabled_providers: ["pystino"] (in that file, not in policy.json) re-enroll
Workspace roots --workspace-root PATH (repeatable) unrestricted Workspaces may only be created under these paths; anything outside is refused re-enroll

Other enroll flags: --device or --loopback to force a sign-in flow, --group NAME to preselect the billing group or --default-group to bill to your console default instead of pinning one, --output PATH for where the opencode config goes (default <config-dir>/galopin/opencode.json), and --yes to overwrite without asking.

run drives opencode by default (it supervises opencode serve). run --backend acp --acp-command "<agent>" drives any ACP agent instead: opencode acp, Gemini CLI, Claude Code or Codex through their ACP adapters, Pi through pi-acp. An ACP backend reports fewer capabilities (no usage, compaction or subagents), and the chat panel hides those controls. The Changes pane works for either backend, because the agent reads it from git.

Over an ACP backend the coordination tools reach the agent as an MCP server galopin runs on loopback: see Coordination tools over MCP.

Each enrollment mints a new computer id (machine-id, in the computer's state directory). The chat therefore shows a re-enrolled computer as a new Pending computer, which you confirm again. A computer revoked in the panel is refused for good under its old id, and run reports that and exits with code 78 instead of reconnecting. A model the gateway gains later needs none of that: galopin models refresh re-discovers the gateway's models into the same config (run does the same at every start), so a re-enroll is for the computer's policy, not its model list.

A repo's own opencode config

A workspace is usually a repository you cloned, and a cloned repository is someone else's input. It can carry an opencode.json, an .opencode/ directory (commands, agents, tools, plugins, MCP servers) and an AGENTS.md. Loaded blindly, an opencode.json can point the gateway provider at another endpoint, so every prompt, and every file the prompt pulls in with @, goes to a stranger's server instead of your gateway; and its {env:...} and {file:...} settings can read your environment and files into that request. Both were reproduced against opencode 1.18.32.

So by default the computer ignores it: run starts opencode with OPENCODE_DISABLE_PROJECT_CONFIG=1. That skips the repo's opencode.json, its project commands, agents and MCP servers, and its AGENTS.md/CLAUDE.md; your own global config, your user-level commands and galopin's agent tools still load. The cost is real: a repo's own commands and agents do not show up, and the panel says so on such a workspace ("this repo's opencode config is ignored on this computer") so a missing command does not look like a bug.

Known gap: on opencode 1.18.32 neither that switch nor --pure stops a repo's .opencode/plugin/ scripts from being executed when opencode opens the directory. Do not open a repository you do not trust in an agent, whatever this setting says.

Skills installed on the computer are offered to every session. opencode discovers skills from the user's own folders as well as galopin's: ~/.config/opencode/skills/, ~/.claude/skills/ (for compatibility with Claude Code) and ~/.agents/skills/, each <name>/SKILL.md. galopin does not filter them, so whatever is installed there shows up in the session's skill list. That includes skills that Claude Code syncs from a Claude account into ~/.claude/skills/ (document skills such as pptx, docx, xlsx and pdf, for example). Some of those were written for Claude's own apps and refer to tools that opencode does not have, so a model may try to follow instructions it cannot carry out. What an agent can do on a computer therefore depends on what else is installed there. On the opencode release galopin pins there is no switch for it: to keep a skill out of agent sessions, remove or move it from those folders. A built-in agent can also be denied the skill tool altogether in your global opencode config ("agent": {"build": {"tools": {"skill": false}}}), which hides every skill from it, including galopin's own. opencode 2.0.30 and later honour OPENCODE_DISABLE_CLAUDE_CODE_SKILLS for the ~/.claude/skills/ folder.

enroll --allow-project-config (the pairing dialog's checkbox) opts in for computers whose repositories you trust: the repo's config loads, including its plugins, which run as you. galopin still pins the gateway provider, enabled_providers, model and small_model above whatever the repo sets, so a repo cannot redirect your prompts; an attempt to (a repo's opencode.json setting provider, enabled_providers, model, small_model or an agent's model) is written to the computer's audit.log as project_config.override_attempt, listing the keys and never the values. An agent-level model naming a provider outside the allowlist fails with an error rather than routing anywhere. With --allow-opencode-provider as well there is no allowlist to pin and this last guarantee does not hold (known: no guarantee): a repo agent can name the repo's own provider.

What the file explorer may see

run lets the /code explorer browse each workspace, read-only, confined to the workspace directory (symlinks that leave it are listed, never followed), with secrets redacted: private keys, .netrc/.npmrc/.pypirc, cloud credentials files, *.tfstate and the like. .env files are shown (add --file-deny '.env' --file-deny '.env.*' at enroll to hide them). Redaction keeps secrets off screens and out of logs; it is not a boundary against the agent, which can read any file. The flags that turn it off or extend the list are in The computer policy.

Saving files

The explorer is also a minimal editor for text files, when three things allow it: the computer allows it (on by default; --no-file-write turns it off, and --allow-file-write only restates the default), the deployment has not set CODE_FILE_WRITE_ENABLED=false (on by default: the computer's policy is the real gate), and the computer runs a galopin that can save files. When any of them says no there is no Edit button; a save that reaches a computer that refuses answers with the exact flag to re-enroll with.

Edit turns the file view into an editor, with an unsaved-changes marker, Save (or Ctrl+S, ⌘S on a Mac) and Discard. Leaving the file, the Files pane or the page with unsaved edits asks first. Files over 1 MiB, binary files and images stay read-only, and say so. Files on the deny list are never written, whatever the request, and the same confinement as for reading applies: a path outside the workspace, or through a symlink, is refused.

A save is atomic (the new text goes to a temporary file in the same folder, which then replaces the file, keeping its permissions), and it happens only if the file has not changed on disk since you opened it: every save carries the revision you read. If an agent (or anything else) changed the file in between, nothing is written and the editor says Changed on disk, with two choices:

  • Reload drops your edits and shows the file as it is now;
  • Overwrite saves your text over the new version.

There is no live reload yet: until you save or refresh, the editor does not know the file changed. The revision check is what keeps a save from silently overwriting an agent's edit. Each save is recorded, with the path and the size but never the content, in the deployment's /code audit and in the computer's own audit.log. To turn saving off again on a computer:

galopin policy set --no-file-write

The terminal

/code can also open a real, interactive shell on the computer, but only when both sides say so. The computer allows it unless it was enrolled with --no-terminal (or enrolled before terminals were on by default; then every terminal request is refused until it is enrolled again), and the deployment must set CODE_TERMINAL_ENABLED=true (off by default). Everything in /code needs a sign-in within the last 7 days; the computer's own link is unaffected (The 7-day sign-in). A terminal re-checks that every minute, so one opened just before the window closes ends at the next check rather than living as long as its socket. Turning it on means exactly this: anyone who controls your Cerea session can run commands as you on this computer. There is no model and no permission rule standing in the way once a terminal is open — it is strictly more power than any permission rule or the selector's Allow, which only ever covers the model's tool calls (within the floor and any local deny). enroll prints a warning (not a refusal) whenever terminals are on: "terminals are on: the /code panel can open a remote shell here, outside every permission rule (--no-terminal turns them off)."

A terminal's shell starts in the workspace's own directory and never sees galopin's own secrets (the opencode server password, the shim secret, or anything shaped like a token or credential) — but once it's running, it is an ordinary shell: it is not sandboxed to the workspace the way file browsing is.

Once a terminal is allowed, you can locally tighten its policy again without a full re-enroll (the flags are in The computer policy):

galopin policy show                       # what this machine currently allows
galopin policy set --no-terminal          # turn it back off
galopin policy set --max-terminals 2      # lower the cap

policy set can only tighten — turn files, file writes or the terminal off, lower the cap, or add a --file-deny entry. Loosening anything back requires enroll again: the policy is never writable over the link.

Permission classes

(ADR 0107, amended by ADR 0109.) Every request that is not an in-project read belongs to a class of action: edit, command, web, outside, secrets, agents, tools, loop. The class labels the card, routes it (to you or to the reviewing model your deployment runs, per your Workspace → Agents rows) and fills the audit; the computer does not limit it. galopin decides from the session's word: Plan refuses, Ask raises a card, Allow runs, and Auto runs the classes the user chose to run (Cerea sends that per-class map with the word) and raises a card for the others.

A fixed floor in galopin's code, not configurable: galopin's own state and configuration (the state directory, policy.json, the credentials, the opencode config it owns) are refused in every word; a write into .git or a CI configuration file, a .env file or a credential store, and a repeating agent are a card even on Allow and Auto (a human's own "Always allow" on a credential card stands); read-only commands always run.

Local deny (advanced). An owner who does not fully trust the Cerea server can refuse a class on the computer: galopin enroll <cerea-url> --deny command (repeatable), galopin policy set --deny web and --no-deny web. It is refuse-only, stored locally in policy.json, never settable over the link, and listed by galopin policy show. It is not in the Pair dialog. loop cannot be denied.

policy.json limits and the flags --limit, --permission-max and --permission-rule are retired: for one more release they are accepted, ignored and warned about, and the next write drops them. Re-enrolling keeps every setting except the options you pass; galopin reset puts the whole policy back to the defaults and keeps the credentials and the pairing.

The permission posture

galopin answers every permission ask; it writes no rules into opencode. (ADR 0097 in the ai-stack repository) opencode is configured, once per start, so that every tool call asks — one constant "everything asks" floor, the same for every agent, subagents from their first call included. galopin receives each ask and answers it itself: reject, once, or a card for you. It never answers "always", and it never writes a rule into a session. Your setting (Plan · Ask · Allow · Auto), your exceptions and any local deny are galopin's own state, read at each ask, so a change takes effect at the very next call, mid-turn included. The approvals galopin keeps for itself are the ones with no opencode equivalent (session_read, session_spawn and session_send, see Sessions that talk to sessions), plus hard limits that are not permissions at all (hop and rate limits, the same-workspace check on sends).

How a call is decided, first match wins: (1) galopin's own state → refused; (2) a class you denied locally with --deny → refused; (3) on Plan, a read inside the project is fine and anything that changes something is refused; (4) a spawner and the session it spawned talking to each other → fine; (5) a schedule's coordination grant; (6) a matching exception → fine (a credential ask only on an exception you gave on its card); (7) on Allow, fine; (8) on Auto, fine when every class of the call is one the user's map runs; (9) on Ask, a read inside the project is fine and everything else is a card. Then the floor: a write into .git or CI config, a credential ask and a loop alert are a card even where (7) or (8) said fine. A subagent is decided by its main session's setting, from its first call.

Reads ask too, and it is cheap. Measured on this stack, each ask costs about 0.2 s at the median (0.3 s at the 95th percentile) for a local round trip, small next to a model step.

What enroll and run put where. enroll writes edit, bash and webfetch as ask into the galopin-owned opencode.json of a new computer; under the floor that file is now harmless redundancy. Every run rebuilds the OPENCODE_CONFIG_CONTENT opencode is started with: the ask floor plus the built-in read-only agents' own denies restated (opencode merges the global config after an agent's rules, so a bare ask would otherwise turn plan's and explore's denies into asks). At start galopin also reads every agent back; a custom agent's own allows are no longer honoured natively — they are closed with one more start — because the way to give a standing allow is the session's Allow or Auto word, which galopin applies itself. A call no agent routes through a permission (a custom tool you installed in opencode's own config directory) still runs unasked; galopin logs and reports any native allow it finds. enroll writes --output whole (it asks before replacing an existing file; --yes does not ask), so point it at a dedicated path.

Computers enrolled before this model. A computer's old limits and rules are ignored (see Permission classes); nothing needs a re-enroll to behave, and every session starts on Ask on every computer. A computer whose policy file predates limits entirely may still show a notice in the panel that reads "Re-enroll this computer to set limits…"; the wording is older than the model, and what it asks for is a fresh enroll, which replaces the old policy with the current one.

Sessions from before this version. An older galopin wrote rules into each session, and opencode keeps them for good. On the first message after the update galopin adds one final "everything asks" rule to such a session and never writes again. It cannot remove what is stored: an old Deny a subagent inherits can still refuse that session's new subagents, and an old external_directory allow can still pass to them. The panel shows the stuck Deny banner for those sessions only; the way out is to start a new session. New sessions never have the problem.

What the floor and the local deny are, stated plainly. Until the OS sandbox exists (ADR 0109: the hard limit is the sandbox, not a computer setting), they are galopin refusing or asking about calls it is shown, and a shell can do anything the ask for it does not name. A local --deny command is meaningful against a hijacked Cerea session; --deny web while commands may run is not (a command can fetch). The embedded terminal is a machine-side setting outside every permission, and enabling it lets a Cerea session change anything; so does a bash that is allowed to run, because a command can edit policy.json and the config galopin starts opencode with (galopin refuses the ones it can see name those files, which is a safety rail, not a boundary). An agent reached over ACP asks only as far as its own policy does: a tool it runs without asking never reaches galopin.

Permissions

Every session has one permission setting, the Plan · Ask · Auto · Allow selector in the composer. It covers the whole session until you change it, and new sessions start on Ask, on every computer, whether enrolled yesterday or long ago. It is the computer's word that the selector shows: a click is reflected once the computer has confirmed it.

Setting What it does with a tool call the session has no rule for
Plan the agent plans and reads; every call that would change something is refused, with no card. The agent is told. Choosing Plan switches the session to opencode's plan agent
Ask asks you, with the approval card (a read inside the project does not ask)
Auto each kind of action runs, goes to your reviewer model or comes to you, as you set in Workspace → Agents (without a reviewer, the kinds set to Reviewer come to you)
Allow lets it through without asking; only the fixed floor and a local --deny still stop or ask

What the setting covers. Editing (writes and patches), bash, webfetch, websearch, codesearch, the task tool that starts a subagent, and every MCP and custom tool follow the setting. Reading inside the project stays allowed on every step, and so does the rest of what the agent does to look around and plan: read (its .env files still ask, as the agent's own rules say), glob, grep, list, lsp, question, the todo and plan tools and skill keep their normal behaviour whatever you choose, and starting a subagent (task) is not a change in itself: on Plan an explore-style subagent can look around, and its own edits are refused by the same Plan setting.

What still asks on Allow (the fixed floor, ADR 0109): a write into .git or a CI configuration file, a .env file or a credential store (your own "Always allow" on that card stands), and the stuck-agent brake (doom_loop, an agent repeating itself). Allow never silences them. Reading and writing outside the project runs on Allow; on Ask it is a card. A local --deny is the other thing Allow cannot pass. Nothing in the panel can set a deny or lift one; changing it means galopin policy set or enrolling again on the computer.

What never asks, on every computer: the safe directories the computer was enrolled with — by default /tmp, the temp directory and the toolchain caches (~/.cache/go-build, ~/.cache/pip, ~/.cache/uv, ~/.cache/pnpm, ~/.cache/node-gyp, ~/go/pkg/mod, ~/.npm, ~/.local/share/pnpm), those that exist. Never the whole ~/.cache: it also holds programs other tools run (opencode's own bin/, Playwright's browsers). A computer enrolled before this list existed gets the default with the galopin update. The Permissions dialog names them in the Work outside the project folder row ("Allowed: /tmp, ~/.cache/go-build, …; elsewhere asks"), and no session — Deny, Ask or Allow, a fresh one or an old one, a subagent's first turn included — is shown the external_directory card for anything inside them. They are computer policy, not a setting: re-enrolling with --safe-dir PATH replaces the list, --no-safe-dirs empties it, and galopin policy set --no-safe-dir PATH removes entries without a re-enroll. Because external_directory does not separate read from write, a safe directory is one the agent may write into: a build cache it can poison, which is the residual risk of the convenience, and why the default list stays short. A local --deny outside still has the last word: the safe directories are refused like everything else.

A subagent follows the session's setting from its first turn. Its asks are decided by its main session's setting and exceptions, like any other call: on Allow it runs without a card, on Ask it asks, on Plan it is refused at its first call that would change something. The floor and a local deny hold for it as for any call.

The approval card. When a tool call asks, the card offers Allow once, Always allow (this session) and Deny. The panel relays your answer unchanged to galopin, which answers opencode once.

  • Always allow (this session) adds an exception for that command or pattern, for this session only, on top of the setting: it is not a standing grant, and it is no longer shared with other sessions in the workspace. The button is not offered where an exception would store nothing (a call with no concrete file, command or directory to scope it to): those cards offer Allow once and Deny. An exception is scoped to what the card shows: an edit to that file, a command prefix or the exact command, or an outside path's directory.
  • Deny refuses that call and tells the agent. It ends every other pending ask in the session.

Exceptions are kept while the setting is Plan and blocked then: Plan beats them. Switching back to Ask restores them, and on Allow they are redundant but harmless. They are removed one by one from the Permissions dialog.

The Permissions dialog opens from a session's ⋯ menu (the Permissions… item, not offered on a subagent, which follows its main session) or from the shield button beside the Plan · Ask · Auto · Allow selector in the composer; the transcript and its approval cards stay in view while you read it. It is titled Permissions — and starts with one sentence on what reaches you at the selected step, then a table of each kind of action (edits, commands, web, outside access, secrets, agents, tools, loops) with what happens (Runs, Reviewed, Comes to you) and why (Set in Workspace, Agents, Always comes to you, or that no reviewer is switched on for you or you turned it off for this computer). It is computed from your rows, the fixed floor and the reviewer's availability, only for a session on Auto. Nothing is listed from the backend's own rules. Under the table come the session's exceptions, each Allowed for this session: git status * with a Remove that makes that command ask again (Remove can only tighten, and the computer records each removal; an exception the computer marks as not removable shows "held by the computer"), and a Coordination section: two switches, Can find, read and message other sessions without asking and Can start new sessions without asking. Without a switch those agents actions come to you or the reviewer like any other ask; a switch is the standing answer, from the session's next turn. Still asks, grant or not: another workspace, and the rate limits apply. A session it spawns starts with the same grant. The sheet reads permission.rules for the exceptions and the grant only. While the sign-in is stale the shield button and Remove are hidden, and the server refuses them anyway. The shield button carries the session's exceptions count and is named for the step ("Permissions: Auto"). A computer whose galopin predates permission reporting says so in the sheet.

The panel in detail

The Agents side of the sidebar is a tree: paired computers, each with its workspaces, each with its sessions. Everything that changes something lives here: pairing, revoking, adding a workspace, starting or archiving a session, renaming. The main pane shows whatever you have selected in the tree. A computer that is not currently connected renders as offline: the tree never tries to load its workspaces, so one offline computer never freezes the rest of the list.

Each computer row has a ··· button (Computer settings). Today its one item is Profiles, the computer's delegate profiles.

The 7-day sign-in

Everything in /code needs a sign-in within the last 7 days; the computer's own link is unaffected. Older than that, the panel draws one card — "Your sign-in is older than 7 days. Sign in again to see your computers." — with a Sign in button, and nothing else: no device tree, no Needs-you inbox, no terminal tab, no counts. The server is what enforces it: every request under /api/v2/code/ except /status answers 401 {code: "reauth_required"} while the sign-in is stale (or has no recorded time at all), so no part of the panel can be asked for a computer's data, whichever way it asks. A tab that is open when the window closes finds out by itself: its event stream ends with a reauth_required frame at the 7-day mark, and the page also flips on its own timer, with no request. Composer drafts stay in the browser but are not shown until you are back.

A tab whose chat session merely expired (no session at all — the ordinary hourly expiry of a chat without a refresh token) shows a different card — "You were signed out. Sign in again to see your computers." — and its Sign in goes through a plain /login?next=/code, never the forced reauth=1 prompt: with the provider's SSO session alive (tick Remember me at the bundled Authelia) the re-login is silent and the panel comes back on its own; an open tab even leaves for that sign-in by itself, once per minute at most. Only the 7-day staleness forces the password.

Your computers keep working while you are signed out of the panel. The computer's link to Cerea is its own credential and is not part of this; what stops is you seeing and driving them from the browser.

The Needs-you inbox

The inbox lists, across all your computers, the approvals and questions that are waiting for you, each answerable in place and linked to its session. It starts as a one-line bar ("2 need you") that opens on a click, and it leaves out the session you have open, whose cards that view already shows. It is part of /code, so it goes dark with the rest when the sign-in is older than 7 days. That is a trade: nothing in the browser will tell you that a session is waiting, and a session waiting for an approval waits (it does not proceed) until you sign in again. For work you leave running for days, either keep the sign-in current, or put the session on Allow for the tool calls you are content to have run without asking (questions still wait for you, and so does the fixed floor).

Workspaces

A workspace is a directory on the paired computer. Add one by typing its absolute path — a relative path would resolve against whatever working directory the agent process was born with, which is unguessable from here, so the server refuses it. The computer refuses a path outside its own configured roots (its own veto, never overridable from the panel) or one it cannot see, and either refusal arrives as a visible failure rather than a row that pretends.

Sessions

A session ("agent") belongs to one workspace on one computer. The backends offered are the ones this computer says it can run.

A session reads exactly like a chat: the same transcript column, the same message rendering, the same composer. That is deliberate — the panel is the chat's own machinery pointed at a different source, which is why updates arrive live without a refresh.

A long session opens on its newest 40 messages under a "Loading chat…" skeleton; scrolling up fetches the earlier ones (on a computer with the current galopin; an older one sends the whole log). The last 8 sessions you had open are kept in the browser tab's memory, so going back to one shows it at once; that copy is display only until the fresh one arrives, and nothing is stored on disk. While a turn works the animated mountain is the progress mark, and answers use the full row on desktop. The agent's task plan is one card per message, after its thinking and tool steps and before the answer. Fork from here on a finished answer starts a new session from that point. When the model provider refuses a turn, the failed turn shows the provider's own text — the newest failure expanded, earlier ones folded to one line you can reopen — and while the agent retries a rate limit or overload the view says "Retrying — …" beside Stop (both need the galopin update).

The composer's pills

Inside the prompt box, in the chat's own pill idiom:

  • mode — the backend's own modes (plan, build, …), listed live rather than from a hardcoded set that would drift from what it enforces.
  • model — the models the session's backend offers.
  • permission — the Plan · Ask · Auto · Allow selector: what this session does about a tool call it has no rule for, until you change it. A new session starts on Ask. It shows what the computer says (never a guess), and a click is only reflected once the computer has confirmed it. Nothing is written beside it: what the step means and what the computer still holds back is in the shield's sheet. On a subagent's view it is disabled, showing its main session's setting. It is hidden while the sign-in is stale. Beside it a shield button, hidden on a subagent and while the sign-in is stale, opens this session's Permissions dialog and shows the exceptions count when there is one. What each setting means is under Permissions.

Mode, model and the permission setting apply to the whole session, not only to the next message, and stay in force until you switch again.

Slash commands

Type / at the start of the prompt box and a menu opens with the commands this session can run. It is also the help: what it lists is exactly what there is. The menu groups them under headings, in this order:

  • Panel — commands this panel runs itself: /compact, /undo, /redo, /model, /mode, /effort, /new. Each is listed only when the agent's backend can do it. They drive the same routes the pills and the transcript use; nothing new runs on the computer.
  • Project (badge "from this repo") — commands defined in the open workspace's repository (.opencode/command/*.md). Listed only on a computer enrolled with --allow-project-config; by default a repo's config is ignored (see A repo's own opencode config). Running one is running repository code on your computer; the first run asks (see below).
  • Computer — commands from your user-level or the computer's config.
  • Skills and MCP — when the agent exposes them.

A command whose template expands shell carries a shell icon in the menu. A computer command whose name a panel command already uses is shadowed: it stays in the list, greyed with the reason, and cannot be selected, because the panel's own command wins.

Type to filter (prefix first, then substring), ↑/↓ to choose, Enter or Tab to accept. A command that takes arguments shows its placeholder as ghost text (/review ‹$ARGUMENTS›); type the arguments after the name and Enter sends. A message that begins with a path, such as /etc/hosts is wrong, is not a command unless its first word names one, and sends as ordinary text.

The computer can refuse a command, and says why. The message names the flag or the file; nothing runs halfway:

What you see Cause
"Needs a setting this computer was not enrolled with" (403) the template expands shell (or its shell behaviour is unknown, such as an MCP prompt) and the computer was enrolled with --no-command-shell; a command pinned to a non-gateway model without --allow-free-models; or a file the command reads through @path matches the computer's file deny list
"This command changed since you approved it; review it again" (409) the command's template changed since you reviewed it: the confirmation is asked again
"no longer listed" the computer's list is re-read at run time, so a command that was deleted after the menu opened is refused
"The agent is mid-turn. Stop it, or wait for it to finish." commands are refused while a turn is running (the send button is hidden then, too)

The first run of a project command (or any command that expands shell) opens a confirmation sheet showing the exact shell snippets it would run and the files it reads. The template itself never crosses to the chat: the snippets and a hash of the template are what you confirm against. Accepting is remembered in this browser, for this computer and this exact version of the command. On another browser, your phone included, you will be asked again. If the definition changes afterwards, the next run is refused until you confirm the new version. The sheet is a speed bump for a user; the computer's slash-command shell policy is the real veto.

A command cannot move the session out of a more restrictive mode: while the session is in plan, a command that names the build agent runs under plan anyway, and a command that would run unsupervised in another agent is refused.

On a computer whose opencode is too old for slash commands, the menu shows the panel commands only; updating opencode on that computer brings the rest.

Agent images

When a tool call produces an image, its card shows it inline under "Output": most often a screenshot from a browser tool such as the Playwright MCP, or an image the agent read. Only PNG, JPEG, GIF and WebP are shown; SVG is never displayed inline, whatever the computer calls it.

The computer keeps the bytes and the chat fetches each image from it whenever it is shown; the chat keeps no copy. Before showing one it checks the file's own header rather than the type the computer claimed, and refuses anything over 8 MiB or past about 50 megapixels, or that does not match the checksum the tool listed. At most 8 images are shown per tool call. When some are left out, the card says so: "3 images not shown (too many, too large or not a supported type)." A broken image usually means the computer no longer has it: an ACP agent keeps images in memory only, so a galopin restart loses them. An image the agent saved as a file is the file explorer's business, not this.

Stopping a turn

While a turn is live, the send button's place carries the stop control, exactly as a chat does. It stays there while a permission card is up, because stopping a request you do not want to answer is most of the point of it.

The diff pane

The diff control opens the shared side pane — the same frame artifacts open in — with the session's file changes, rendered by the same diff machinery the artifact panel uses. The before/after pairs come from the computer; nothing is reconstructed here.

Archiving and revoking

Archiving a session or a workspace confirms first, then asks the computer and redraws that device's subtree from its answer — never an optimistic removal, because the computer owns the listings. If the archived row was the one open in the address, the panel navigates away from it.

Revoking a computer tombstones the pairing record: it is refused if it tries to reconnect, and its live socket (if any) is closed immediately. Re-running galopin enroll mints a new computer id — it re-pairs as a fresh pending row, not a resurrection of the revoked one.

A computer the gateway refused (a disabled account, or sessions invalidated since it enrolled) stays listed as access revoked, with the re-enroll command beside it, until you remove it with its bin icon; removing it only takes it off the list. A refused token is not a refused account: when the gateway refuses the token a computer holds, the chat checks the token on its own first, and if that token no longer validates (it expired, or the identity provider revoked it on refresh, as GitLab does) the link is closed with 4401 and the computer renews and reconnects, with nothing revoked.

A computer refused for access groups or capabilities is neither: the link is closed with 4404 and the device stays enrolled — no re-enroll, no bin icon. Regaining the group, or a group granting the coding-agents capability, reconnects it by itself. What you see depends on which one bit:

  • outside the required groups: the gateway's join gate refused the account;
  • no coding-agents capability: the account may use the chat but not /code (a chat-only user — the gateway console's Users page badges them Chat only). The Agents switch is gone from the sidebar and /code answers "not available for your account"; the chat itself keeps working throughout.

Delegating work

A session can hand work to others. Four mechanisms, from lightest to heaviest:

Mechanism What it is
Subagents a task inside the session's own turn (opencode's task tool), shown as a card in the transcript
Delegates a separate session the model starts with session_spawn; its answer comes back to the session that started it
Recipes and the committee handoff, second opinion and committee: ready-made ways to use delegates
Delegate profiles which model a delegate runs on, set per computer

All of the session-to-session tools are on by default and turned off with --no-agent-tools at enroll.

Subagents

When the session spawns a subagent, the tool call that spawned it is replaced in the transcript by a card for that subagent — title, status, from the computer's own roster. Expanding one fetches its own transcript and renders it as a nested read-only chat.

A subagent may run in the background (the task tool's background:true, behind opencode's experimental OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS). The computer allows that unless enrolled with --no-background-subagents; then the flag is never set and a background task fails closed. A background child keeps running after its parent turn ends — the parent reads idle while the child is still working, and the panel says so — and its result is injected back into the parent as a synthetic message the panel folds into the task's own card as completed or failed, never as model text. Passing task_id follows up the same running child; the card names it a follow-up rather than a new spawn.

A subagent has no setting of its own: it follows its main session. Its rules are the main session's setting and exceptions, under any local --deny, never the computer's own allows. They are applied when the subagent appears and re-applied to every live subagent whenever you change the main session's setting or remove an exception, so Deny stops a running child too. Until they land, the computer starts a subagent from an ask floor rather than from allow-everything; galopin answers the first turn's asks that the main session's rules allow (above). On a subagent's own view the selector shows the main session's setting, disabled (opencode itself hands a subagent only its parent's deny rules, which is why galopin sets the rest explicitly).

Sessions that talk to sessions

A session's model can start another session on the same computer (session_spawn), send a message to one (session_send) or read what one has said (session_read). All are on by default and turned off with --no-agent-tools at enroll (or galopin policy set --no-agent-tools later), which installs no tool into the backend at all. There are four of them (session_profiles and session_committee are described under Delegate profiles and Recipes and the committee):

  • session_list reads the computer's sessions and needs no approval.
  • session_read returns another session's most recent user and assistant text as plain text (the last 1 to 50 messages, ten by default, each with its role and time). It never returns tool calls or their output, reasoning or attachments; a message over 4 KB is cut, and the whole answer is capped at 32 KB (the oldest messages go first, and the answer says so). It cannot read the session that calls it; a subagent of the caller's own tree is allowed. Its card shows the target and how many messages.
  • session_spawn starts a new session. Its approval card shows the new session's title, its mode and the full prompt.
  • session_send sends a message to an existing session. Its card shows the target session, the full message and the hop (how many agent sends deep this chain is).

Whether a spawn or send asks is decided from the asking session's effective rules as galopin composes them (the agent's, then the session's: the computer's own rules, what you set for it, and any local --deny last), looked up under the names session_read, session_spawn and session_send:

  • allow — no card, and the card in the transcript carries an allowed by this computer's rules badge.
  • deny — refused.
  • ask, or no rule — a card, as for any other ask. Nothing you answer on it is remembered: it has no "Always", and answering "once" is the only form.
  • A blanket "*": allow never grants a read (otherwise opencode's permissive default would let any session read any transcript), but under the session's Allow word it grants a spawn or a send, like any other tool; a blanket "*": deny refuses them. A transcript can hold secrets, which is why a read stays as hard to get as a send.
  • A send or a read into another workspace always asks.
  • A session can also be granted them without a rule: see below. Prompts and messages over 8 KiB are refused before any card is raised, so a card never shows a truncated text.
  • A chain longer than three messages, sent back and forth, asks at every step after the third instead of stopping; the card says why. More than five messages, and separately more than five reads, a minute from one session to the same target are refused outright.

Granting a session its coordination tools

A scheduled run has nobody to answer a card. Cerea's schedules therefore have two options, both off: Can find, read and message other sessions (session_list, session_read, session_send) and Can start new sessions (session_spawn). At each run Cerea sends the computer session.grantCoordination {sessionId, keys} for the session it just created or chose, before the prompt. galopin records the grant with the session (it survives a restart) and treats those tools as allowed for it. It is never more than you allowed: a local --deny agents still refuses a granted key, another workspace's sessions still ask, the hop limit and the rate limits stand, and Deny on the session wins. A galopin from before this op answers unsupported: the schedule still runs, and its run row says "this computer's galopin is too old to grant coordination; update it". Each grant is a row in audit.log with its keys and outcome.

Questions always wait for a user, and a rule never answers a handoff. An allowed call is not invisible: the badge shows in the sender's transcript and the computer's audit log records it.

Limits

Spawning is bounded so a session cannot fork without end: a spawn chain is at most two deep (a spawned session can spawn once more, its child cannot), at most three spawned sessions are live under one root, and a root that has started six in ten minutes is refused. A spawned session runs in the spawner's own workspace and inherits its permission word, coordination grant and exceptions, never more (an Allow spawner makes a child that runs without a card; a read-only child comes from mode: "plan") — still under any local --deny, which refuses for the child exactly as for the caller. The exceptions are the "always" answers the user gave the spawner, copied as they are; removing one from the spawner later does not remove it from children already spawned. In the sidebar it sits under its spawner, behind a thin connector (a spawned session whose spawner is in another workspace, or gone, stays a top-level row with a "spawned by" line).

Scheduling work

The agent tools include four for the owner's scheduled actions (see Scheduled actions): schedule_list, schedule_create, schedule_update and schedule_delete. They reach Cerea over the computer link as call frames (PROTOCOL.md §5) and act as the computer's owner, only on schedules that run on this computer. They need a Cerea that advertises features.machineCalls: ["schedule"] in its welcome; against an older one each tool answers that this Cerea does not support schedules from agents.

Whether a call asks is the permission key schedule (a local --deny agents refuses all three tools):

  • schedule_list never asks.
  • schedule_create follows the session's word like any tool: Allow covers it, Ask raises a card, Deny refuses. Except in a scheduled run (a session a schedule started or used, or one of its subagents): there it always asks, even under Allow or a computer allow rule, so a schedule cannot multiply itself.
  • schedule_update that only pauses, or schedule_delete, of the schedule the session is a run of: no card ("stop when you are done").
  • Any other update or delete always asks, even under Allow.
  • A local deny of the agents class wins over all of the above.

The card shows the whole schedule (name, timetable, timezone, workspace, session, agent mode (plan or build, default build), permission mode, coordination and the full prompt), the changed fields for an update, the name for a delete.

Cerea then holds every call to its own rules, whatever the card said: the store's (15-minute floor, per-user cap, timezone, CHAT_SCHEDULES_ENABLED); a permission mode no looser than the calling session's own and coordination no wider than its grant; at most five running agent-made schedules per computer; at most ten changes per computer per hour (a pause or a delete is never refused). Each change is in the /code audit trail with the calling session, and the Schedules list shows the schedule as Created by an agent in ‹session›.

Steering

A message that arrives while the target is mid-turn is folded into that turn as steering; to an idle target it starts a turn. The message reaches the target marked as coming from another agent session, and the target's transcript shows it as a distinct "From agent" bubble with a link back to the sender. The same steering applies to your own messages: with a turn running, the composer's Send sits beside Stop (its chevron is stop-and-send), and slash commands are still refused mid-turn.

What this means, plainly: where a computer's rules allow session_send (or a session was granted it), a session can message others in its workspace without you seeing a card. The cost is noise and one agent's text steering another's context, bounded by the hop and rate limits and visible in the badges and the audit log. A computer that leaves these two on ask, which is the default, shows a card for every one.

What these gates do not cover

An approved shell command can do anything you can on that computer (opencode's server password sits in its process environment, readable by same-user processes); the coordination gates constrain the model's tools, not an approved shell.

Delegate profiles

A profile is a name and a model, and nothing else. It answers one question for a delegate: which model does it run on? A computer has up to 32 profiles and at most one default. Profiles belong to the computer, not to your account or to a workspace, because the models a computer offers are its own.

Setting them up. On the computer's row in the Agents sidebar, open the ··· menu (Computer settings) and choose Profiles. The Delegate profiles dialog lists one row per profile:

  • Default: the radio button that marks the profile used when none is named.
  • Name: 1 to 32 characters, lowercase letters, digits, _ and -.
  • Model: one of the models that computer offers for sessions (the gateway's models, plus others only if the computer allows models from elsewhere).

Add profile adds a row, the bin icon removes one, and Save replaces the computer's whole set. The computer checks it and the dialog then shows what the computer stored, not what you typed; a refusal says why and nothing is written. The dialog is read-only, with the reason, when the computer is offline, was enrolled with --no-agent-tools (there are no profiles without the tools), or runs a galopin too old to have them (update it, see Updating galopin). Only the computer's owner can open it.

How delegation uses them. session_spawn and session_committee take an optional profile name:

  • a name runs the delegate on that profile's model;
  • no name (or an empty one) uses the default profile's model;
  • with no default, or no profiles at all, the delegate runs on its spawner's own model, as before.

An unknown name, or a model the computer no longer offers, refuses the spawn before anything starts, and the refusal lists the profiles there are. The model can list them itself with session_profiles, which needs no approval.

A model is not a permission. A profile changes which brain a delegate runs on and nothing about what it may do: its permission word, coordination grant and "always" answers come from its spawner as ever, any local deny and the computer's rules still apply, and the approval card is the same one. For read-only delegation use the plan mode, not a profile: session_spawn and session_committee take a mode, and a delegate started in plan can read and look around but every call that would change something is refused.

Where they are kept. In profiles.json in the computer's state directory (see The state directory), written by galopin and read at every use, so a saved change applies to the next spawn without a restart:

{
    "default": "fast",
    "profiles": { "fast": { "model": "pystino/small" }, "deep": { "model": "pystino/large" } }
}

The menu is the way to edit it. An older file, from before profiles were just a name and a model (it had built-in worker, reviewer and explorer entries, modes and notes), is read gracefully: each entry that has a model keeps it, and the file is rewritten in the new shape at the next save.

Recipes and the committee

Three ready-made ways to use delegates. The first two are guidance galopin's delegation skill gives the model, built from session_spawn and report-back; only the committee needs a tool of its own.

  • Handoff. The whole job goes to one delegate with a full brief, using the default profile or the one you name. The spawning session tells you where it is running and ends its turn; the delegate's report arrives in the spawner's transcript by itself. Use it to pass off a long or separate job and free the chat.
  • Second opinion. A fresh reader checks what the session did or concluded: a delegate started in plan mode (so it can read but not change anything), briefed with the question and the work to check, optionally on a profile with a different model when you want a different brain. The session weighs the report against its own view and tells you where they differ. It is for when the stakes or the doubt justify a second reader, not for every answer.
  • Committee. Several delegates answer the same question independently. session_committee {title, question, profiles} takes 2 to 3 members, each a profile name (or "" for the default; vary them for different models), an optional mode for all of them (plan for read-only members) and an optional timeoutMinutes (1 to 60, 15 by default). The tool returns at once. galopin gathers the reports and sends the spawner one message, Committee "title": k of N members answered, followed by each member's report, once every member has reported or the time is up; a member still working is named, and its late report arrives on its own afterwards. The model is expected to lay out where the answers agree and differ rather than average them.

A committee goes through the very same checks as session_spawn: the same permission key and grant, one approval card per member under Ask (naming the committee), Plan refuses it, and the live-delegate limit (three under one root) applies. It is all or nothing against the limits: if depth, live sessions or the spawn rate would cut it short, nothing starts, and a declined card stops the rest. It costs one delegate per member, so it suits questions that merit that.

Coordination tools over MCP

opencode gets galopin's coordination tools (session_list, session_read, session_spawn, session_send, session_profiles, session_committee and the schedule_* tools) as custom tools it loads from a directory galopin owns. An ACP agent (Gemini CLI, Claude Code, Codex through an adapter, and so on) cannot load those, so galopin offers the same tools to it as an MCP server it runs on loopback, when the agent can use an HTTP MCP server.

  • One endpoint and one token per session. galopin listens on 127.0.0.1 only, and gives each session its own address and bearer token when the session is created. A token acts as its own session only, so an agent cannot call the tools as another one. Deleting the session revokes it, and so does stopping galopin; tokens are never stored, so a restart makes new ones.
  • The same approvals. A call over MCP goes to the same decision as opencode's: Plan refuses, a local deny and the computer's rules apply, a coordination grant or Allow lifts a card, the delegate rules apply, and the same cards and audit rows appear. There is no second, looser path. A delegate's report still comes back to the session that started it.
  • opencode keeps its own route and does not start the MCP server.
  • A busy ACP session cannot take a folded-in message, so a session_send to one is refused; reports wait until the spawner is idle.
  • --no-agent-tools turns it off with the rest. A computer reports the tools only when its agent can use an HTTP MCP server; otherwise it offers none.

The design is in the "Coordination tools over MCP" paragraph of agent/PROTOCOL.md.

The delegation skill

Tools alone do not tell a model when to use them, so galopin installs one opencode skill beside them, named delegation (skills/delegation/SKILL.md in the directory galopin owns for its tools). It is written with the tools and only with them: a computer enrolled with --no-agent-tools gets neither. Like any skill, its description rides every turn and the model loads the full text when a task matches. It tells the model:

  • subagents first: fan out read-only work as several task calls in one message, keep every edit in the parent, and summarise what the children found before acting on it;
  • other sessions: find a target with session_list rather than guess an id, expect every read, spawn and send to raise a card, never route around a declined card, and treat a message from another session as a peer's request, not the user's;
  • recipes and profiles: a handoff, a second opinion in plan mode or a committee of two or three when the user asks for one or the stakes justify it (Recipes and the committee), and a profile only when the user names a model or the job clearly calls for it (Delegate profiles);
  • background subagents: use background: true for long work where the computer allows it, and do not promise it where it does not;
  • long waits: never sleep-and-poll in the foreground (CI, a release, a full test suite): hand the wait to a background task, end the turn and pick up when its result arrives;
  • when it runs on a schedule (its prompt starts with the [Scheduled run …] line, see Scheduled actions): nobody is watching, so look with session_list and session_read before steering, do not resend a message the target has not acted on yet, reuse a session with the same job instead of spawning a new one, and end with a short summary of what the run checked, sent, started and left waiting for the user;
  • scheduling work: list schedules before creating one, prefer session: "this" for a follow-up, write a prompt that stands on its own, name each schedule so the user knows what it does, and pause or delete the schedule it is a run of when the job is done.

The skill only describes what the tools and opencode's own task tool do; the gates above are what enforce it.

Scheduled actions

Schedules (under the devices in the sidebar) send a prompt to one of your sessions on a timetable. Pick the computer (online or offline), then a workspace on it (or New workspace…, created when you save, under the computer's own workspaceRoots), then a new session each run (titled "name · date", and marked with a clock in the tree) or one existing session. Then the mode, model and permission word, the prompt, and when: every N hours, daily, weekdays, weekly, or a cron expression, never closer than 15 minutes, in the timezone you choose with the next three run times shown as you edit. "Schedule this…" in a session's ⋯ menu starts the editor on that computer, workspace and session.

  • The computer pays. Cerea only sends the prompt over the link you already have; the run uses the computer's own credential and policy, and a local --deny still refuses what it names.
  • Ask stalls. Nobody is watching a scheduled run. On Ask it stops at its first approval and waits; the card is in the Needs-you inbox. Pick Allow for work that must finish unattended (a key your computer caps at Ask still asks). For "always this session", the word is set on that session each run and stays.
  • The agent knows it is a scheduled run. Each prompt arrives with one line above what you wrote: [Scheduled run of "<name>", <timetable> (<timezone>); previous run <3 h ago | none>; coordination: <granted tools | none>]. The delegation skill tells the agent what to do with it.
  • Missed runs. Computer offline: recorded as missed, not queued. Previous run's session still working (or waiting on an approval): skipped. Chat down: one late run is made up only if it is under half the interval late (at most an hour), otherwise recorded as missed. The workspace or session is gone: the run fails with that reason, and three failures in a row switch the schedule off. A revoked computer switches its schedules off at once.
  • History. Every occurrence is a row in the schedule's history, with why, and a link to the session it reached. Run now fires one run immediately (the skip rule applies).

Installing and running galopin

Getting the binary

From your deployment (the usual way). Every Cerea image builds the four binaries from agent/ and serves them itself, with no sign-in needed, so nothing has to be published anywhere. On the computer:

curl -fsSL "$CEREA_BASE_URL/galopin/install.sh" | sh

(The /code pairing dialog shows this command with your addresses filled in.) The script picks the binary for this OS and CPU (Linux or macOS, amd64 or arm64), downloads it and SHA256SUMS from the same origin, refuses to install on a checksum mismatch, installs to ~/.local/bin/galopin (GALOPIN_INSTALL_DIR overrides), clears macOS's quarantine flag, and prints the galopin enroll command for this deployment. It needs curl or wget, and sha256sum or shasum.

The same files are there to fetch by hand (the pairing dialog's "Download manually" lists them): <base>/galopin/galopin-<os>-<arch>, <base>/galopin/SHA256SUMS and <base>/galopin/version. Nothing else under that path is served. The binaries are revalidated on every use (ETag), so a cache in between never pairs an old binary with a new checksum file.

Building it yourself needs Go 1.24+ and nothing else:

agent/packaging/build-dist.sh ~/galopin-dist

It writes static binaries (CGO off, so each one runs on any computer of its OS/architecture), plus a manifest:

File What
galopin-linux-amd64, -linux-arm64, -darwin-amd64, -darwin-arm64 the binary
REVISION the Cerea commit it was built from (-dirty if agent/ had local changes)
SHA256SUMS for sha256sum -c SHA256SUMS (macOS: shasum -a 256 -c SHA256SUMS)
galopin.service, org.cerea.galopin.plist the user-service files below, from agent/packaging/

Use this for a computer that cannot reach the deployment, or to build from a checkout you changed; anyone with Go can also run go build -o galopin . in agent/.

Installing on a computer (Linux or macOS)

Prerequisite: opencode on the PATH, at the release galopin is tested against (currently 1.19.3; the one place it is written is agent/packaging/opencode-version): npm i -g opencode-ai@1.19.3, or curl -fsSL https://opencode.ai/install | bash -s -- --version 1.19.3. A newer opencode usually works, but it can change something galopin relies on; a daily CI job runs galopin's real-opencode suite against each new release (agent/README.md, "opencode releases"). galopin run supervises opencode serve, and it is the only runtime dependency. opencode is a separate open-source project (MIT licence) installed from its own installer; Cerea does not redistribute it and is not affiliated with it.

With the installer above, skip to enroll. From a build of your own:

install -d ~/.local/bin
install -m 0755 galopin-darwin-arm64 ~/.local/bin/galopin   # the file for this OS/arch
xattr -d com.apple.quarantine ~/.local/bin/galopin 2>/dev/null || true   # macOS only

~/.local/bin/galopin enroll \
  --issuer "$OIDC_ISSUER" \
  --gateway "$GATEWAY_BASE_URL" \
  --cerea "$CEREA_BASE_URL"
~/.local/bin/galopin run

enroll opens your browser to sign in (or prints a link and code to open on any device, your phone included), asks which billing group to use if you have several, and writes two files: the opencode config, by default <config-dir>/galopin/opencode.json (beside the credential, whatever directory you ran enroll from; --output moves it), and a refresh credential (mode 0600, default <config-dir>/galopin/credentials.json, where <config-dir> is ~/.config on Linux and ~/Library/Application Support on macOS). enroll records the opencode config's absolute path in the credential file, and run finds it there and hands it to opencode, so neither command needs a path. run then supervises opencode and dials out to the chat. Open the sidebar's Agents panel: the computer is listed as Pending until you confirm it with the green check (Confirm this computer).

Flag When
--cerea …/chat always include /chat when the chat is served there. The computer dials <cerea>/api/v2/code/machine, and without the base path it reaches the gateway instead
--output PATH where the opencode config goes (default <config-dir>/galopin/opencode.json). The file is replaced whole: enroll asks before replacing an existing one, and --yes skips the question. Its absolute path is recorded, so run finds it. Never point it at your own ~/.config/opencode/opencode.json unless you mean to replace that
--device force the device flow, which prints a URL and a code to open on any other device (the bundled Authelia's opencode-enrollment client allows it). Without a flag, enroll picks the loopback sign-in in the local browser when there is a display, and the device flow when there is none. --loopback forces the browser
the policy flags --no-terminal, --deny CLASS, --allow-free-models, --workspace-root PATH and the rest are the computer's own vetoes, fixed at enroll time, and all of them are in the pairing dialog: see The computer policy

With GitLab as the identity provider

GitLab mints its own application ids, so the default computer client opencode-enrollment cannot exist there. Signing in with it ends on GitLab's page with Client authentication failed due to unknown client. Set it up once:

  1. On GitLab, add an application for agent computers: not confidential (galopin has no secret), scopes openid profile email, the device authorization grant enabled, and redirect URI http://127.0.0.1/callback (used only by the browser flow on a laptop).
  2. In the kit's .env, set OIDC_MACHINE_CLIENT_ID (or run ./configure --machine-client-id) to that application's Application ID, then docker compose up -d. The gateway then accepts the computer's tokens, and the pairing dialog prints --client-id with it.
  3. Enroll with the dialog's command, or by hand with --client-id <Application ID>.

GitLab's access tokens are opaque, not JWTs, so neither the chat nor the gateway can check their signature. Both ask GitLab instead, at its /oauth/token/info, with the token itself: it must belong to the computer application and expire. The chat does this when a computer connects (from 0.10.14; before, a GitLab computer's link was refused with HTTP 401 right after a successful enroll), and the gateway on every /v1 call (Pystino 0.4.2).

GitLab's discovery lists the device grant but does not advertise its device endpoint. galopin uses <issuer>/oauth/authorize_device, GitLab's, and says so (note: the identity provider does not advertise a device endpoint; using …); a headless box needs no --loopback. For any other provider that leaves the endpoint out, name it with --device-endpoint URL.

A computer enrolled before enroll recorded the path has no opencode_config in its credential file, and run says so on stderr: the supervised opencode may have no pystino provider, so the panel reports that the daemon lists no models. Re-run enroll, or pass run --opencode-config PATH with the opencode.json enroll wrote.

If enroll warns that model discovery failed, no model is available to you yet. Ask your administrator to grant your group one, then run galopin models refresh — or just restart run, which re-discovers the gateway's models at every start. Neither re-enrolls, so the computer id stays.

Updating galopin

Re-run the install line, the same one the Pair a computer dialog prints:

curl -fsSL "$CEREA_BASE_URL/galopin/install.sh" | sh

It replaces ~/.local/bin/galopin with the build your deployment serves (checked against its SHA256SUMS), and nothing is re-enrolled: the computer keeps its id, its credential, its policy and its pairing. Then restart galopin run so it runs the new binary: systemctl --user restart galopin on Linux, or stop and start the run you started by hand. On macOS, launchctl kickstart -k gui/$(id -u)/org.cerea.galopin.

Updating the deployment is what makes a newer galopin available, so a computer that was paired long ago gets new features (delegate profiles, the committee, the MCP tools) only after this. A panel feature that needs it says so ("update galopin on that computer"). Re-enroll only to change the computer's own policy (loosening needs it) or to recover a revoked computer.

Using opencode yourself: galopin launch opencode

galopin launch opencode [--opencode PATH] [--port PORT] [-- opencode arguments…]

Starts your own interactive opencode, in the current directory, set up the way galopin sets up the one it supervises, but without dialing Cerea: no pairing, no computer link, nothing in the Agents panel. It uses

  • the opencode.json enroll wrote (as OPENCODE_CONFIG), so the same gateway provider and models;
  • the same local proxy, so the same refreshed credential and billing group. A run or serve already listening on the shim port is reused; otherwise galopin starts the proxy for as long as this opencode runs and stops it when it exits (never two on one port);
  • the computer's project-config setting (--allow-project-config; off by default, in which case opencode starts with --pure), and a private TMPDIR of its own, so it never disturbs a running galopin run.

Everything after -- goes to opencode untouched, and galopin exits with opencode's exit code; SIGINT, SIGTERM and SIGHUP are forwarded. A computer that is not enrolled stops with "run 'galopin enroll' first". --opencode PATH picks the binary (default opencode on PATH, then $HOME/.opencode/bin); one that differs from the version galopin is tested against only draws a warning.

Provider credentials and routing selectors in your shell (ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, OPENAI_API_KEY, OPENROUTER_API_KEY, the other well-known provider keys and base URLs, OPENCODE_PERMISSION, OPENCODE_MODEL) are removed from opencode's environment, so nothing is billed outside the gateway by accident. Your own opencode configuration (~/.config/opencode, opencode auth) still applies, exactly as under run.

Not supervised. galopin prints, on stderr: galopin: local opencode, billed to <group>; not supervised by Cerea (no Plan/Ask/Auto, no review log). opencode is given the "everything asks" permission floor, so every tool call asks you, in the terminal. What cannot apply is anything that needs galopin to answer for you: the refusal of galopin's own state and credentials, the Plan/Ask/Auto selector, the delegated permissions and the review log. Treat it as opencode with the gateway wired in. The command is launch <tool> so other tools can follow; only opencode exists.

Keeping it running

Linux, as a systemd user service, so galopin starts at boot, restarts if it crashes, and keeps running after you log out. Run these as the user that enrolled the computer (not root), once galopin run works in a terminal:

  1. Stop the galopin run you started by hand (Ctrl-C in its terminal), so two copies don't fight over the computer's link.
  2. Write the unit. It's the same file as agent/packaging/galopin.service in the Cerea repository:
mkdir -p ~/.config/systemd/user
cat > ~/.config/systemd/user/galopin.service <<'UNIT'
[Unit]
Description=galopin: this machine's link to the chat's /code panel
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=%h/.local/bin/galopin run
Restart=on-failure
RestartSec=10
# 78 = this machine was revoked in the /code panel: restarting cannot fix it.
RestartPreventExitStatus=78
# opencode must be on this PATH; add its directory if it lives elsewhere
# (`command -v opencode` tells you where).
Environment=PATH=%h/.local/bin:%h/.opencode/bin:%h/.npm-global/bin:/usr/local/bin:/usr/bin:/bin

[Install]
WantedBy=default.target
UNIT
  1. Start it now and at every boot, and keep it running while you're logged out:
systemctl --user daemon-reload
systemctl --user enable --now galopin
loginctl enable-linger "$USER"

enable-linger is what lets a user service run with nobody logged in. Some distributions ask for sudo loginctl enable-linger <user>.

  1. Check it:
systemctl --user status galopin       # "active (running)"
journalctl --user -u galopin -f       # its log; Ctrl-C to stop following

The computer shows as online in the /code panel within a few seconds.

After updating galopin (re-running the install line from the pairing dialog), restart the service so it runs the new binary: systemctl --user restart galopin.

Over SSH, if systemctl --user answers "Failed to connect to bus", your session has no user bus. Log in again, or run export XDG_RUNTIME_DIR=/run/user/$(id -u) first.

macOS, as a LaunchAgent:

sed "s/USER/$USER/g" org.cerea.galopin.plist > ~/Library/LaunchAgents/org.cerea.galopin.plist
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/org.cerea.galopin.plist
tail -f ~/Library/Logs/galopin.log

Both run ~/.local/bin/galopin run with a PATH that includes the usual places opencode installs to. Edit that PATH if yours is elsewhere.

To keep it back up after a revoke, re-enroll (or delete machine-id) and start run again. The systemd unit does not restart it after that exit (RestartPreventExitStatus=78, kept from the pre-rename unit); launchd cannot filter on exit code, so it restarts once, finds the computer id marked revoked, and exits 0 to end the loop — launchctl kickstart the job after re-enrolling.

If the agent says your enrollment expired (after a redeploy, for instance), sign in again with galopin auth login on that computer — no re-enroll, no new computer id, and a running agent picks the new credential up without a restart. Only a computer revoked in the /code panel needs the pairing command again. The agent reports this at once instead of hanging; the details are under "How it works".

Removing a computer

  1. Revoke it in the /code panel. The chat tombstones the pairing and closes the link, the agent logs machine revoked, not reconnecting, and a later connection under the same computer id is refused. The agent then revokes its own refresh token at the IdP (RFC 7009, the discovery document's revocation_endpoint) and clears the tokens from its credential file, so the revoked computer can no longer reach the gateway either. If the IdP can't be reached, it says so in the log; the tokens are cleared anyway, and the refresh token lapses at its own expiry.
  2. Stop the service: systemctl --user disable --now galopin, or launchctl bootout gui/$(id -u)/org.cerea.galopin.
  3. Delete <config-dir>/galopin/. Re-enrolling later mints a new computer id, which is paired afresh.

What is kept where

Where it lives
your code, your working tree, the session's live transcript your computer
the pairing record — name, computer id, policy, owner the chat's database
prompts, replies, file contents in flight only, over your computer's own outbound TLS connection
your LLM spend the gateway's ledger, against your account and billing group

Pairing records are scoped to you: nobody else's panel can see or revoke your computers.

How it works

Background

You do not need any of this to pair a computer or use the panel. It is here for anyone who wants to know what runs where.

The shape of it

your laptop                          the deployment
┌───────────────────────┐            ┌───────────────────────────┐
│ opencode               │──── /v1 ─▶│ the gateway (bills you)    │
│   ▲                    │           │                            │
│   │ shim               │           │ Cerea (the machine link)   │
│ galopin ───────────────┼─ outbound ┼──── WSS ───────────────────▶
└───────────────────────┘            └───────────────────────────┘

One binary, one credential. galopin supervises opencode serve, dials out to this deployment over WSS and authenticates with the same OIDC access token its enrollment minted — the identical credential that authenticates its /v1 calls to the gateway. There is no relay and no second process to run: revoking your account at the identity provider kills both the control link and the LLM link within one access-token lifetime. The wire protocol both ends speak is agent/PROTOCOL.md; the gateway facts galopin's enrollment relies on (the IdP client opencode-enrollment, its refresh-token lifetimes, x-bill-to) belong to the Pystino gateway and are documented there.

One binary, two jobs

Job What it carries Command
LLM opencode → the local refreshing shim → gateway /v1, billed to the signed-in user galopin enroll, then the shim starts with run (or alone with serve)
control the computer dials out to the chat over WSS (/api/v2/code/machine), so the chat's /code panel can drive it galopin run

Nothing else needs pairing: the computer connects outbound with its own enrollment token, and you confirm it in the /code panel (agent/PROTOCOL.md §4).

Why a shim at all

An OIDC access token expires in minutes; opencode holds a static apiKey in its config and has no notion of renewal. So the token never reaches opencode. enroll stores the refresh credential, serve (which run also starts) holds it, and the written opencode.json points its baseURL at http://127.0.0.1:<port>/v1 — the shim. Every request gets a fresh bearer and the recorded x-bill-to injected on the way past.

The default shim port is 41871, bumped upward while occupied and then recorded in the config, so enroll's own loopback callback listener and the shim never collide.

Which scopes enroll asks for

enroll asks for openid profile email groups offline_access, narrowed to what the issuer's discovery lists in scopes_supported (openid always stays; without a list, all five are asked for). Some providers, Infomaniak among them, answer invalid_scope to a scope they do not list, and publish groups as a claim only. enroll prints a note when it leaves something out.

offline_access is how most IdPs are told to issue a refresh token. One that does not list it but lists the refresh_token grant is asked without it, and enroll checks the token response: no refresh token means no enrollment. It stops with "the IdP issued no refresh token … so this computer could not stay signed in" rather than write a computer that would lose its sign-in within the hour. Enable refresh tokens for the opencode-enrollment client at the IdP, then enroll again. Without a groups scope, billing by group needs the IdP to put a groups claim in its tokens anyway (the gateway's --oidc-groups-claim).

When the refresh token dies

A gateway or IdP redeploy can revoke the refresh token the shim holds. That is expected and recoverable — sign in again with galopin auth login (only a computer revoked in the /code panel needs a re-enroll) — but the failure has to be fast and legible, not a 502 that opencode's client quietly retries into a long hang:

  • A 401, not a 502, once the refusal is permanent. The token endpoint's invalid_grant (RFC 6749 §5.2) answers 401 with an OpenAI-shaped body ({"error":{"message","type","code":"enrollment_expired"}}), which opencode's client never retries — a 401 stops it cold and surfaces the message verbatim. A transient refusal still answers the old 502, which opencode does retry.
  • Refreshed proactively, not just on request. The shim refreshes once at startup and then every 15 minutes, well inside the access-token lifespan, so its state is already known before opencode ever sends a request.
  • A status file and a health endpoint, kept in sync. Every state change (ok / expired / unreachable) is written atomically to <state-dir>/status.json as {"state","checkedAt","message"}, and the same JSON is served at GET http://127.0.0.1:<port>/galopin/health.
  • The dead state is not for ever. Once the shim has latched expired it answers from that verdict instead of hitting the IdP again — but galopin auth login rewrites the credential file's token fields under the same lock the shim refreshes under, and the shim notices the newer file on its next request or 15-minute tick: it adopts the new tokens, clears the verdict and serves again, with no restart.

The device flow prints the code-bearing URL

Both bundled IdPs issue verification_uri_complete, which carries the user code as a query parameter — what enroll prints as open:, so one paste does what open-plus-type would; the bare code is still printed on its own line, because a wrapped URL pastes broken and a typed code does not. An IdP that omits the complete URI falls back to verification_uri.

What enroll writes into opencode.json

{
    "$schema": "https://opencode.ai/config.json",
    "enabled_providers": ["pystino"],
    "provider": {
        "pystino": {
            "npm": "@ai-sdk/openai-compatible",
            "name": "Pystino",
            "options": { "baseURL": "http://127.0.0.1:41871/v1" },
            "models": {
                "my-model": {
                    "name": "My Model",
                    "limit": { "context": 131072, "output": 16384 }
                }
            }
        }
    }
}

Three things in there are load-bearing:

  • No apiKey. opencode would send it verbatim and it would expire. The shim owns the credential instead (its own per-install secret goes in apiKey, never the gateway bearer).
  • enabled_providers: ["pystino"]. opencode's own allowlist — without it a built-in provider with ambient credentials (an ANTHROPIC_API_KEY in the environment, a logged-in Copilot) offers models that bypass the gateway entirely, and that spend never lands in the account the enrollment exists to bill. --allow-opencode-provider omits the key for operators who want both.
  • limit.context and limit.output on every model. Both keys are required by opencode's schema for custom-provider models: omit either and opencode refuses the whole file. Where the gateway publishes no hint the CLI writes defaults (131072 context, 16384 output) rather than omitting the key.

Only chat-kind models make the list. An embedding tier in a coding agent's model picker is one accidental keypress from a 400.

The state directory

Credentials and everything else galopin writes on its own behalf — credentials.json (carries the shim secret), machine-id, policy.json, revoked, opencode-overlay.json, workspaces.json, status.json and audit.log — live in <config-dir>/galopin/, and so does the opencode.json enroll writes by default (its path is recorded in credentials.json as opencode_config, which is how run finds it). opencode's own config, ~/.config/opencode/opencode.json, is left alone.

audit.log is the local record of terminal opens/closes and policy refusals — rotated JSON lines, never a byte of keystrokes, output, or file content. It is the one record of a computer's /code activity that Cerea itself cannot rewrite.

opencode-tmp/ there is opencode's TMPDIR while galopin supervises it, emptied on every (re)start: opencode is a Bun binary that extracts its native libraries (~5 MB) into TMPDIR on each start and never removes them, which on a computer whose /tmp is tmpfs slowly fills RAM.