OceanCode — Developer Guide
Contents
Getting started Configuration Plan mode and todos Session ledger and export Headless and server Bounding a long run Capabilities and plugins Hooks MCP servers Permissions and sandbox Jev decisions Platform mode (PlungeAI connectivity) Testing and gates Distribution and updates Operations Ocean pager — the terminal UI Xcode and other ACP editors — `oceancode acp` CLI reference Ocean pager reference HTTP API referenceThe Ocean coding agent: the
oceancodeengine (headlessrun,serve,goalandworkflow; an ACP agent for Xcode) and Ocean pager, its terminal UI. 20 chapters: build and configure it, drive it from the terminal, bound a long run, extend it with capabilities, hooks and MCP servers, and the HTTP API (218 operations, read from the route table). Also: One API docs · MCP guide · llms.txt · This guide as markdown. Source:apps/ocean-coding-agent-rust/docs/user-guide/(build date 2026-10-17).
Contents
- Getting started
- Configuration
- Plan mode and todos
- Session ledger and export
- Headless and server
- Bounding a long run
- Capabilities and plugins
- Hooks
- MCP servers
- Permissions and sandbox
- Jev decisions
- Platform mode (PlungeAI connectivity)
- Testing and gates
- Distribution and updates
- Operations
- Ocean pager — the terminal UI
- Xcode and other ACP editors —
oceancode acp - CLI reference
- Ocean pager reference
- HTTP API reference
Getting started
oceancode is one binary and one process. This page builds it, shows the two ways to start
it, and lists where it keeps its files.
Build
cargo build --release -p ocean-cli
The binary is named oceancode and, because the workspace builds into a shared target
directory, it lands at the repository-root target/release/oceancode (that is
../../target/release/oceancode from this app directory). Put it on your PATH or call it by
that path.
The three boot shapes
oceancode starts in one of three shapes, chosen by the subcommand.
| Shape | Command | What runs |
|---|---|---|
| Pager | oceancode |
Ocean pager (ocean-pager) with the Ocean engine in the same process over ACP — the terminal UI. See 19 · Ocean pager. |
| Editor | oceancode acp |
The engine as an Agent Client Protocol agent on stdin/stdout — what Xcode and other ACP editors launch as a subprocess. See 20 · Xcode and other ACP editors. |
| Headless | oceancode serve, or a non-interactive subcommand (run, goal, workflow, ledger, export, …) |
The engine only, no UI: serve exposes the HTTP surface, the others run one job and exit. |
# 1. Pager + engine (the usual way)
oceancode
# 2. Headless server on port 4096
oceancode serve
# 3. One headless run
oceancode run "summarise the README"
The default mode accepts --db, --directory, --listen and --verbose (-v) and passes them
to the engine. --tui pager is accepted for compatibility and changes nothing;
--tui ocean fails with "the native TUI was removed; oceancode starts Ocean pager". Binding beyond
loopback needs --listen and a server password; see
07 · Headless and server.
First run
Authenticate once, then start a session.
oceancode auth # interactive: API key or Claude Pro/Max subscription
# or, non-interactively:
oceancode login # Claude Pro/Max subscription login
oceancode key <key> # store an Anthropic API key
The credential is written to auth.json in the data directory (below) and reused on every
later run. Then launch oceancode and type at the prompt.
Starting a session
A plain oceancode opens the pager on a fresh session. Earlier sessions are listed and resumed
from inside the pager — see its own user guide under
ocean-pager/crates/ocean-pager-ui/docs/user-guide/. Headless runs pick a session with
oceancode run --continue / --session <id>; see
07 · Headless and server.
Updating and removing
Keep the binary current, verify a download, or remove it:
oceancode update # update to the newest release for your channel (verified, atomic, rollback on failure)
oceancode verify-release oceancode-macos-aarch64.tar.gz # check a download's signature before trusting it
oceancode uninstall # remove the binary (add --purge to also remove config + data)
See 17 · Distribution and updates for install one-liners, channels, and the signature/checksum trust model.
Where files live
Paths follow the XDG layout on every platform (macOS included — ~/.config, not
~/Library). <x> below is the matching $XDG_*_HOME when set, otherwise the home-relative
default shown.
| What | Location |
|---|---|
| Global config | $OC_CONFIG, else <XDG_CONFIG_HOME | ~/.config>/oceancode/oceancode.json |
| Credentials | <XDG_DATA_HOME | ~/.local/share>/oceancode/ — auth.json |
| Models catalog cache | <XDG_CACHE_HOME | ~/.cache>/oceancode/models.json |
| Session database | oceancode.db in the working directory, unless --db <path> is given |
| Project config and assets | <project>/.oceancode/ — oceancode.jsonc, oceancode.json, skills/, plans/ |
Next
- 02 · Configuration — the config keys and environment variables.
- 19 · Ocean pager — the terminal UI; its slash commands and keys are in
ocean-pager/crates/ocean-pager-ui/docs/user-guide/.
Configuration
oceancode reads a JSON config file and a set of environment variables. Nothing needs to be
configured to start — every key falls back to a built-in default.
Config file locations and precedence
There are two config files, and both are optional.
| Scope | Path | Notes |
|---|---|---|
| Global | $OC_CONFIG, else <config-dir>/oceancode/oceancode.json |
One file for every project you run. |
| Project | <project>/.oceancode/oceancode.jsonc, then <project>/.oceancode/oceancode.json |
Per-repository, kept next to the code. |
<config-dir> is $XDG_CONFIG_HOME when set, otherwise ~/.config — on every platform,
macOS included (the binary uses the XDG layout everywhere, not ~/Library).
Precedence, lowest to highest: built-in default → global file → project file. Within the
project directory, oceancode.json is merged over oceancode.jsonc when both exist. Merging is
per section, last writer wins.
Comments and trailing commas are allowed in the .jsonc file.
Top-level keys
Every key is optional; an empty or absent value takes the built-in default.
Unknown keys are ignored, so a file that still carries the native-TUI keys removed on 2026-10-05
(tui, keybindings, disabled_providers, notifications) loads unchanged.
| Key | Type | Default | Meaning |
|---|---|---|---|
$schema |
string | none | JSON-schema URL, for editor completion only. |
model |
string | provider default | Default model, as provider/model. |
default_agent |
string | build |
Agent a new session starts in. |
agents |
object | none | Per-agent overrides, keyed by agent name. Each agent may set model, prompt, temperature, disable, and tool_profile (default / codex — apply_patch is the sole mutation tool / hashline — the content-anchored hashline tool replaces edit). |
providers |
object | none | Per-provider settings (endpoints, options). |
formatter |
bool or object | built-ins on | Code formatters: false/true toggles the built-ins; an object overrides them per formatter. |
instructions |
array of strings | none | Extra system-prompt sources — each a file path (~- or working-dir-relative) or an http(s) URL. |
mcp |
object | none | MCP servers, keyed by name. |
ocean |
object | none | Ocean Platform integration — see 15 · Platform mode. |
permission |
array | * * allow |
Base permission rules (allow / ask / deny); last matching rule wins. An empty config allows every call except the shell's dangerous floor, an external_directory ask and denied .env reads; add bash * ask to make every shell command off the read-only safe list ask (the posture Jev's permission point advises on, 13). |
permission_mode |
string | ask | Default permission mode applied at boot: acceptEdits, dontAsk, bypassPermissions, or empty to ask on every side effect. An empty value never overrides an earlier layer. See 12 · Permissions and sandbox. |
shell |
object | built-in guard | Shell tool-guard overrides: safe_list_extra (extra read-only commands), allow / deny (Tool(pattern) rules layered on the classifier); plus kind (auto / bash / sh / powershell / cmd, default auto) — the shell the bash tool spawns, mainly for Windows (auto = sh on Unix, PowerShell on Windows). See 12 and 17 · Distribution and updates. |
child_env |
object | strip secrets | How a spawned child's environment is composed: inherit (all / core / none), include_only, set (name→value), strip_secrets. See 12. |
sandbox |
object | off | OS isolation profile: profile (off / workspace / readonly / strict), deny (roots refused even inside the workspace), network (off / allowlist), allow_hosts. See 12. |
refs |
object | none | Project-reference aliases: refs maps @name → a path; a @name/… path in any file tool expands to it and its root is auto-allowed through the external-directory prompt. See 12. |
tool_output |
object | built-in thresholds | Overrides for tool-output truncation limits (built in: 2,000 lines / 50 KiB). A result over them reaches the model as a head preview with the full output saved to a spill file it can read; this applies to every tool that does not page itself, bash included. |
claude |
object | none | Claude Code (Pro/Max subscription) transport settings. |
ledger |
object | on, tideline = 40% of the model's context, block = 70% of the line | Session Ledger settings, under ledger.tideline (the aliases ledger.shelve and ledger.window still parse): tideline.percent (the tideline — the line — as a share of the model's context, 1..=100, default 40 — while the window's real-token gauge is at or over it, the oldest closed block leaves the window); tideline.block (how much a block holds before it closes: a token count used verbatim (70000), or a share of the line as a string ("35%", 1..=100) that is re-derived whenever /tideline or the route changes the line — the rule "cut 35 % per cut" stays 35 % on every model; unset = 70% of the line; 0, "0%", "101%" and a string without % are refused at load); tideline.tokens (the fixed-token line used only when the model's context is unknown, default 300k; 0 = the model's own ceiling). The keys of the old band geometry — low, reserve, profile under the tideline block and ledger.hard_tokens — are removed: an old config still loads, they are ignored, and the load logs one warning naming them. ledger.model — the merge/lint model, default the session's (empty): every merge and lint call is a separate full-price request that never touches the session's cache prefix, so an empty value bills it at the session model's price — set a cheaper one (provider/model). Examples {"ledger":{"tideline":{"percent":50}}}, {"ledger":{"tideline":{"percent":20,"block":"35%"}}} (a 200 K line on a 1 M model, 70 K per cut), {"ledger":{"tideline":{"tokens":500000,"block":20000}}} or {"ledger":{"model":"openai/gpt-5-mini"}}. See 06 · Session ledger and export. |
database |
object | no pruning | Session-database retention (retention_days, vacuum). |
compress |
string | safe |
Prompt compression of tool results, safe (the default) or off. safe runs five lossless stages (ANSI strip → fold of identical lines → the session directory written ./ → a result identical to an earlier one becomes [same output as <tool-use id>] → a run of lines found in an earlier result becomes [↑<n> lines same as <tool-use id>:L<a>-L<b>]), on the copy of the request sent to the provider only — the stored session is never changed and the tideline cuts on the same turns to within the token estimate, and never earlier. Files and images are never touched. Overridden by OC_COMPRESS; an unknown value is read as the default and logs one warning. |
subagent |
object | none | Default native sub-agent model as subagent.model = provider/model (e.g. {"subagent":{"model":"openai/gpt-5-mini"}}). The model a session's sub-agent children run when the session sets no /subagent override and the fleet router pins none (env.CLAUDE_CODE_SUBAGENT_MODEL, applied while the subscription slot is live); empty (unset) leaves children on the spawning session's own model. A value without a / is rejected. |
workflows.enabled |
bool | true |
Whether the host-run workflow conductor and goal mode are available at all. false turns the conductor off for the session. |
workflows.max_concurrent_agents |
number | 8 (1..=1024) |
Concurrency ceiling: how many agents a single workflow run may execute at once. A value outside 1..=1024 is rejected at load. |
workflows.agent_budget |
number | 128 (1..=1024) |
Total number of agents a single run may spawn before it stops. A value outside 1..=1024 is rejected at load. |
workflows.max_run_minutes |
number | 120 |
Wall-clock cap on a single run, in minutes. |
workflows.agent_timeout_minutes |
number | 20 (0 = off, else 1..=1440) |
Per-child await bound: how long the host waits on one sub-agent (a single agent() call) before it cancels that child alone and returns it to the script as success:false with stop_reason: "timeout" — the run and its other agents keep going. 0 disables the bound (only the run's max_run_minutes then applies). A non-zero value outside 1..=1440 is rejected at load. |
workflows.orchestrate |
string | off |
off | always. always wraps every prompt in the goal workflow (goal mode): the prompt launches a background goal run and the turn returns at once, with the run's outcome arriving later as a workflow result in the session; off runs each prompt as a plain turn. This is a config key only — Claude Code's ultracode setting does NOT switch it on. |
skills.mode |
string | all |
Skill discovery mode: all (every discovered skill is a candidate) or allowlist (only skills whose id is in skills.enabled are candidates; every other skill is disabled and costs zero prompt tokens). This is the project tier — the per-skill user toggle lives in ~/.oceancode/skill-state.json (oceancode skill enable/disable). See 09 · Capabilities. |
skills.disabled |
array of strings | none | Qualified skill ids the project turns off. A project can disable a skill but can never re-enable one the user disabled — the user tier is resolved first. |
skills.enabled |
array of strings | none | The allowlist consulted only when skills.mode = "allowlist": the skill ids that stay enabled — each a stable <tier>.<eco>:<name> id (see 09 · Capabilities) or a bare name (matching every copy of that name). |
skills.roster |
string | full |
How the system prompt lists skills: full (an <available_skills> element with name, description and location per skill) or names (one line of names, no descriptions — the deterministic control arm the Jev bench compares against). A Jev-narrowed roster (jev.relevance = "act") wins over names and renders as full. |
jev |
object | off | The TypeSafe Jev decision layer: enabled (needed in BOTH the global and the project config), endpoint, model (versioned id), budgets, one mode per decision point (permissions, output_view, supervisor, triage, tools, relevance, visibility = off/shadow/act), thresholds. A project config may set only enabled and the modes. See 13 · Jev decisions. |
update |
object | auto-check on, stable |
Self-updater settings: auto_check (bool, default true) checks for a newer release on boot; channel (stable / beta, default stable) selects release visibility; endpoint (string, unset → GitHub Releases) overrides the release-metadata source. Overridable per run by OC_NO_UPDATE_CHECK / OC_UPDATE_CHANNEL. See 17 · Distribution and updates. |
queue |
object | default_delivery: queue |
Prompt-queue defaults: default_delivery (queue — after the live turn, or steer — into the live turn) is how a prompt with no explicit delivery is admitted. |
jobs |
object | max_concurrent: 4, spill_max_bytes: 5242880 |
Background-job limits for the bash background:true / monitor / loop / scheduler tools: max_concurrent running jobs and spill_max_bytes per-job output-spill cap. There is no job timeout: a job runs until it ends or job_kill, unless the run has a wall clock (OC_MAX_MINUTES), which reaps it. |
checkpoints |
object | enabled: true |
File checkpoints: enabled toggles the per-project snapshot/restore behind /undo·/redo (the user's own .git is never touched). |
context |
object | durable_rules: true |
Context-epoch options: durable_rules injects each entered directory's rule file once as a durable message so it survives context compaction. |
model_fallback |
array of strings | none | Ordered provider/model routes tried, in order, after the primary model fails. Paired with fallback. |
fallback |
object | cooldown_seconds: 60 |
Provider-fallback options: cooldown_seconds is how long a model_fallback route is skipped after it fails. |
Environment variables
Set these in the shell before launching. A slash command or an oceancode run flag, where one
exists, overrides the environment for that session (see
08 · Bounding a long run).
| Variable | Meaning | Default |
|---|---|---|
OC_CONFIG |
Absolute path to the global config file, overriding its default location. | unset → <config-dir>/oceancode/oceancode.json |
OC_CONFIG_DIR |
Directory that holds the project oceancode.jsonc / oceancode.json. When set (non-blank), the project config is read from $OC_CONFIG_DIR instead of <cwd>/.oceancode/, so config can live outside the repo — a read-only checkout, or a benchmark board that grades the whole workspace tree (B6). Only the project tier moves; the user and managed tiers are unaffected. |
unset → <cwd>/.oceancode/ |
OC_MANAGED_CONFIG |
Absolute path to the managed (admin/enterprise) config file, overriding its OS default. It is the highest-precedence layer, so it overrides the project and global files. See 12 · Permissions and sandbox. | unset → macOS /Library/Application Support/oceancode/managed_config.json, Linux /etc/oceancode/managed_config.json |
OC_REQUIREMENTS_PUBKEY |
Trusted ed25519 public key (64 hex chars) that a managed requirements.json signature is verified against; an unsigned or tampered file is refused, never applied. |
unset → no key; managed requirements are not enforced |
OC_SERVER_USERNAME |
Basic-auth username the HTTP server accepts. | oceancode |
OC_SERVER_PASSWORD |
Basic-auth password. Empty disables auth; a non-loopback bind refuses to start without it. | unset (auth off) |
OC_MODELS_URL |
Override URL for the models catalog. | the built-in models.dev URL |
OC_MODELS_PATH |
Path to the local models-catalog cache file. | a file under the cache dir |
OC_DISABLE_MODELS_FETCH |
Any non-empty value skips fetching the remote models catalog (offline). | unset (fetch on) |
OC_NO_UPDATE_CHECK |
Any non-empty value suppresses the boot-time check for a newer release. The check is also skipped on first run and when update.auto_check is false; the first launch is telemetry-free regardless. See 17 · Distribution and updates. |
unset (check on) |
OC_UPDATE_CHANNEL |
Force the release channel for oceancode update: stable or beta. Overrides the update.channel config key for that run. |
unset → update.channel (default stable) |
OC_INSTALL_DIR |
Install / uninstall destination directory for the installers and oceancode uninstall. |
unset → $HOME/.local/bin (Windows %USERPROFILE%\.oceancode\bin) |
OC_MINISIGN_SECRET_KEY |
Release lane only — path to the minisign secret-key file scripts/sign/sign-artifacts.sh signs artifacts with. Read by the release scripts, never by the running binary; never committed. |
unset |
OC_WINDOWS_CERT_THUMBPRINT |
Release lane only — Authenticode code-signing certificate thumbprint scripts/sign/authenticode-windows.ps1 uses. Read by the release scripts, never by the running binary. |
unset |
OC_TUI |
Compatibility only. pager is accepted and changes nothing (grok is refused as unknown) — oceancode always starts Ocean pager; ocean fails with "the native TUI was removed; oceancode starts Ocean pager". --tui takes the same values. |
unset → Ocean pager |
OC_PAGER_BIN |
Absolute path to the Ocean pager binary (ocean-pager) that oceancode execs. |
unset → beside oceancode, then PATH |
OC_PAGER_HOME |
Set by oceancode for the pager it execs: the pager's state directory. Read by ocean-pager, not the engine; every pager variable is in 19 · Ocean pager. |
<data>/oceancode/pager |
OC_RG_BIN |
Absolute path to a ripgrep (rg) binary. The bash tool rewrites a plain recursive grep -r … to rg and puts a directory holding that binary as rg first on the child PATH (a symlink/copy shim under <data dir>/bin/ when the file is named otherwise). The pager passes its embedded ripgrep automatically; set this only to override it or on a machine without one (Windows). |
unset → the pager's embedded rg, then rg beside oceancode, then rg on PATH |
OC_MAX_MINUTES |
Wall-clock bound for a run, in minutes. Background jobs the run starts (bash background:true, monitor, loop, scheduler) are reaped with it; unset, they run until they finish or job_kill. |
unset (no clock bound) |
OC_MAX_USD |
Spend bound for a run, in US dollars. | unset (no budget bound) |
OC_MAX_STEPS |
Per-turn step cap at the root, 1–1000. A turn boundary the run auto-continues past, not a stop. |
300 |
OC_MAX_SUBAGENT_STEPS |
Per-turn step cap for a sub-agent. Sub-agents stop at it. | 300 |
OC_PLAN |
off disables the planning discipline for the run: no todo directive in the system prompt on either route, no plan-missing reminder, and a settled turn with open todos is not worked off. The PLAN panel, /plan and any todo block the model writes keep working. See 05 · Plan mode and todos. |
unset (on) |
OC_COMPRESS |
Prompt compression profile for the run, safe or off; overrides the compress config key. An unknown value is read as the default and logs one warning. |
unset → the compress key (default safe) |
OC_READ_STEER |
off lets pure file reads through the bash tool again. By default cat <paths>, head [-n N], tail [-n N] and sed -n '<range>p' on files that exist under the command's working directory are refused before any spawn, with the exact read call (offset/limit derived from the range) in the error — read adds line numbers, paging and truncation guards, and its freshness tracking is what a later edit checks. Pipes, redirects, chains, globs, missing paths, tail -f and sed -i always run. POSIX shells only. |
unset (steer on) |
OC_MAX_OUTPUT_TOKENS |
Overrides the model's per-request output-token cap, clamped to the model's own limit. | the model's published maximum |
OC_EXPERIMENTAL_BACKGROUND_SUBAGENTS |
0 or false disables background sub-agents (the task tool's background: true); on by default since 2026-10-13. Foreground and background sub-agent runs alike now emit spawned/finished lifecycle frames — foreground runs also stream progress frames — that the Ocean pager renders as a live sub-agent roster. |
unset (on) |
OC_WEBSEARCH_PROVIDER |
Force the web-search provider: exa or parallel. |
chosen per session |
OC_MCP_META_TOOLS |
1 swaps the flat MCP tool catalog for the search_tool/use_tool meta-dispatch once the connected tool count passes 64. Above 128 tools the swap always happens (providers reject a longer tools array). See 11 · MCP servers. |
unset (off below 128; forced on at 129+) |
OC_MCP_WAIT_SECONDS |
Whole seconds a prompt's first model request waits for the MCP servers configured at boot to finish connecting, so their tools are in that request instead of arriving in the next one (a tool list that grows between two steps of a prompt re-writes the provider's cache). 0 never waits; a server still connecting at the bound is not waited for. Sub-agents never wait. See 11 · MCP servers. |
5 |
OC_LOG |
Engine log level for the redacting tracing subscriber: trace, debug, info, warn, or error. Overrides the obs.log_level config key; an unrecognized value logs a one-line notice and falls back to info. Secret-shaped values are masked before any log write. See 18 · Operations. |
unset → obs.log_level (default info) |
OC_TERMINAL |
Set to 1 by the app in every shell and tool it spawns, so tools can detect they run under oceancode. Exported, not read. |
set by the app |
OC_HOOK_EVENT |
Set by the app on every hook command handler to the firing event's name (PreToolUse, Stop, …). Exported, not read. See 10 · Hooks. |
set per hook invocation |
OC_PROJECT_DIR |
Set by the app on every hook command handler to the project directory (also exported as CLAUDE_PROJECT_DIR, and used as the handler's working directory). Exported, not read. See 10 · Hooks. |
set per hook invocation |
OC_MODEL_FORMAT |
Legacy tool-result format switch; no longer read — every result renders in the native markdown format. | inert |
OC_TUNNEL_TOKEN |
Platform build. The OceanTunnel session token, read at the highest priority — above OCEAN_TUNNEL_TOKEN, then ocean.tunnel_token_file, then the inline ocean.tunnel_token. It supplies a token; it does not turn platform mode on. See 15 · Platform mode. |
unset |
OCEAN_TUNNEL_TOKEN |
Deprecated: still read as a legacy fallback for OC_TUNNEL_TOKEN (logs one deprecation warning per process); set OC_TUNNEL_TOKEN instead. See 15 · Platform mode. |
unset |
OC_SSO_TOKEN |
Platform build. A per-request gateway SSO session token. When set, the route layer sends it as the X-Ocean-SSO-Token request header (no Bearer prefix) on the configured https gateway host only, leaving the wallet Authorization: Bearer (your ozk_ key) intact; read at runtime, never persisted. NOTE: the inference gateway does not yet read X-Ocean-SSO-Token, so it is ignored server-side until that lands. To store one, use oceancode login --sso-token. See 15 · Platform mode. |
unset |
OC_JEV |
off, shadow or act: overrides every Jev decision point's mode (off turns the layer off whatever the config says). It never turns the layer on by itself — jev.enabled must still be set in both the global and the project config. |
unset → the config |
OC_JEV_ENDPOINT |
Overrides jev.endpoint (the Jev POST URL). The only way to use plain http, and only to a loopback host (127.0.0.1, localhost, ::1). |
unset → jev.endpoint, else the build default |
OC_TYPESAFE_API_KEY |
The TypeSafe API key, sent only to api.typesafe.ai (never to another host); never in config; stripped from child processes like every *KEY* variable. The direct API without it keeps the layer off. |
unset |
Test and bench knobs
These steer test, benchmark, and profiling runs; they are not part of normal operation.
| Variable | Meaning | Default |
|---|---|---|
OC_DETERMINISTIC |
1 pins ids, clocks, and other non-determinism for reproducible runs and replay. |
unset |
OC_METRICS_ADDR |
host:port to serve runtime gauges (uptime, worker/task counts) for profiling. |
unset (nothing listens) |
OC_TEST_HOME |
Overrides the home directory used for path resolution, for tests. | the real home directory |
OC_CACHE_HINTS_MUTATE |
Toggles prompt-cache-hint mutation, for cache experiments. | unset |
OC_CACHE_KEEPALIVE_MINUTES |
Minutes between prompt-cache keep-alive touches. While a step waits (a sub-agent, a long bash) or the session idles, the engine re-sends the last request with max_tokens: 1 at this interval so the provider's cached prefix is read again and its one-hour clock restarts — a cache read instead of the full re-write a cold return pays (about 1:80 on Fable 5.1: 0.025× read against a 2× one-hour write, ~$0.06 against ~$5 on a 250k prefix). 0 or off disables it. Each touch is a session.cache.touched event and its cost joins the session's spend. |
50 |
OC_CACHE_KEEPALIVE_MAX |
Touches per request before the keep-alive gives up (≈ 40 h at the default interval; a model's break-even against one 2× re-write is 2× ÷ its cache-read price: 80 touches on Fable 5.1, 20 on a 0.1× model). | 48 |
OC_CONFORMANCE_TARGET_URL |
Target server URL for the conformance harness. | unset |
OC_TEST_TIMEOUT_SCALE |
Multiplies harness timeouts (ocean-testkit, dev-only); floored at 1.0, so it can only lengthen, never shorten, a timeout. |
unset (1.0) |
OC_LIVE_CONFIRM |
1 arms the money-gated live benchmark drivers (scripts/bench/task11-stratified.sh, swebench-parity.sh, marathon-month-gate.sh, run-official-board.sh) to spend against a real model; unset, each refuses to spend and only --self-tests. Never set by check.sh. |
unset (never spends) |
OC_SQLITE_CACHE_KB |
Per-connection SQLite page-cache size in KiB (PRAGMA cache_size = -<kb>), applied to every writer and read-pool connection. A soak/measurement knob for isolating engine memory from the box's page cache; leave unset in normal use. |
unset → SQLite default (≈64 MB) |
OC_READ_POOL_SIZE |
Number of connections in the session store's read pool. A soak/measurement knob to bound per-connection memory when profiling; leave unset in normal use. | unset → available_parallelism() |
OC_JEV_FIXTURE_DIR |
Directory the Jev policy-table tests read their recorded fixtures from, instead of each crate's tests/fixtures/jev (read by ocean_jev::testkit::load_fixture; set by scripts/gates/check-jev.mjs --self-test to a planted copy). Test-only; the binary never reads it. |
unset (the crate's own fixtures) |
OC_LLM_CACHE_DIAG |
1 adds "diagnostics": {"previous_message_id": …} (the newest Anthropic response this process received) to every Anthropic Messages request and logs each response's cache read / creation / input tokens and cache_miss_reason at info as OC_LLM_CACHE_DIAG (read by ocean_llm::route::client). Diagnostic for prompt-cache audits; scripts/probes/live-steer-cache-probe.sh uses it. |
(unset) |
OC_LLM_DUMP |
Directory to write every provider-native request body into as <seq>.json (a process-wide counter), so two consecutive steps can be diffed byte for byte in a prompt-cache audit (read by ocean_llm::route::client). Diagnostic; bodies contain the whole context — use a private directory and delete it afterwards. |
unset (off) |
OC_TOOL_REPLAY |
Path of a recording ({"<tool call id>": {"ok": bool, "text": string}}); the tool registry then answers every call from it by the model's call id instead of running the tool (an unrecorded id is an error result), so a recorded bash or edit never runs again. Compiled in only with ocean-agent's test-util feature (cargo build --release -p ocean-cli --features test-util, which scripts/probes/real-replay-probe.sh does into target/replay-bin) — the shipping build never reads it. An unreadable or malformed recording stops the process. |
unset |
Claude Code settings mirror
On the subscription route (claude-code/… models, run through the claude CLI) OceanCode follows
Claude Code's own settings, so both tools start the same way. It reads three files, lowest
precedence first, and merges them per key (modelSettings per model):
| Tier | Path |
|---|---|
| user | ~/.claude/settings.json |
| project | <project>/.claude/settings.json |
| local | <project>/.claude/settings.local.json |
| Key | What OceanCode does with it |
|---|---|
model |
With no model in oceancode.json and claude on this machine, the catalog default and every new session are pinned to claude-code/<model> — the id verbatim, [1m] suffix included (--model claude-fable-5-1[1m]). An explicit model in oceancode.json wins. |
effortLevel, modelSettings.<model>.effortLevel |
The effective effort (modelSettings over effortLevel, matched with and without the [1m] suffix) becomes the pinned variant and reaches the CLI as --effort <level> — low, medium, high, xhigh or max; any other value is shown but not passed. ultracode plays no part: since Claude Code 2.1.284 it is its own toggle at any effort level, so the written effort (the fleet router's max) is what the row, the pinned variant and the wire carry (2026-09-29) |
ultracode |
Drives the status bar's ⚡ ultracode on mark (and the persisted preference). /ultracode still toggles it by hand until the next settings change. Claude Code honours the same key ("ultracode": true makes every Claude Code session ultracode). |
env.CLAUDE_CODE_SUBAGENT_MODEL |
The sub-agent model the fleet router pins. Applied on the native route while the subscription slot is live (2026-09-29): shown on the identity row as sub-agents <name> and the model a session's children run, as anthropic/<id> at that id's modelSettings effort — unless /subagent overrides it per session (the override holds until the router pushes a new value, then it is cleared). A router push re-seeds running sessions without a restart. |
env.CLAUDE_CODE_SUBAGENT_MODEL_FORCE |
"1" marks the router's sub-agent model as forced. Reported (GET /claude/settings) and read alongside the model; not enforced — an explicit model on a task call still outranks the router's default. |
alwaysThinkingEnabled |
Reported (GET /claude/settings); not yet acted on. |
hooks |
See 10 · Hooks. |
Live. The engine watches both .claude directories. Saving any of the three files re-reads the
mirror and the hooks within about a quarter second: sessions still on the previously mirrored
model move to the new one (a model you picked by hand in /models is kept), and the UI shows
Claude Code settings reloaded — <model> · effort <level> · ultracode on|off when a mirrored value
changed (the fleet router rewrites the file periodically; an unchanged rewrite is silent).
Nothing is mirrored when claude is not installed (the settings are still read and reported) or
when the files name no model. Managed (enterprise) settings are not read. The watcher follows the
.claude directories that exist when the engine starts — a project .claude/ created afterwards is
read at the next start (or /hooks → r).
See also
- 01 · Getting started — where these files live on disk.
- 07 · Headless and server —
OC_SERVER_USERNAME/OC_SERVER_PASSWORD.
Plan mode and todos
Two separate things share the word "plan": the step list the model keeps as it works, and plan mode, a read-only agent for thinking before touching anything.
For the full design and the code paths, see PLAN-MODE.md (apps/ocean-coding-agent-rust/docs/PLAN-MODE.md); this page is the
operator's summary.
The step list (todos)
For work with three or more distinct steps the model keeps a todo list — one item per step — and Ocean pager's todos pane shows it live. A single edit, one command or a plain question gets no list: the same rule Claude Code, opencode, Kilo Code and Grok Build use. Each row ticks through four states:
| Glyph | Meaning |
|---|---|
[ ] |
pending |
[»] |
in progress |
[✓] |
completed |
[✕] |
cancelled |
The model writes the list as a fenced
```todo block in its reply (or through its todo tool); the newest complete block replaces
the whole list, so the pane always reflects the latest one. In the pager, ctrl+t toggles the
todos pane (see 19 · Ocean pager).
If the model skips the list and then does real multi-step work anyway (twelve or more tool
calls with no plan), the next step carries a reminder to write it; a plain answer is never
reminded. A settled turn that still has open items is worked off (see
08 · Bounding a long run). OC_PLAN=off turns the directive,
the reminder and the work-off off for a run; the todos pane and /plan keep working.
The always-plan floor
If the model has not planned yet, oceancode shows one synthesized row — your prompt text — so
the list is never empty. The moment the model writes a real plan, that floor steps aside and
does not come back for the session.
Editing the plan yourself — /plan
You can edit the list while a step runs; the model merges your changes with its own on the next step.
| Command | Effect |
|---|---|
/plan add <text> |
Add a pending row. |
/plan done <n> |
Mark row n completed. |
/plan cancel <n> |
Mark row n cancelled. |
/plan edit <n> <text> |
Rewrite row n. |
/plan remove <n> |
Delete row n. |
A row you add is open work: the run keeps going until it is done or a bound trips (see 08 · Bounding a long run). If you add a to-do while the agent has already finished, it counts as progress — the run picks the work back up for one more turn instead of stopping.
Plan mode
Plan mode is a distinct agent, not a key. In it the model may read and search but may not
edit or run side-effecting tools — the one exception is writing plan files under
.oceancode/plans/.
- Enter it by selecting the
planagent — in Ocean pager, a bare/planor/plan <description>(the row-edit verbs above are the only/planforms that do not switch mode). Every turn then carries a reminder that editing is off. - Leave it when the model calls
plan_exit: it asks "switch to the build agent?", and on your confirmation the session flips to thebuildagent, which may edit and run. The approval surface lets you approve (y), reject (n), or attach inline comments that are appended to the plan as user-authored rows before you decide.
The step list keeps working throughout — plan mode changes what the model may do, not how progress is tracked.
See also
- 06 · Session ledger and export — where the plan and turns are recorded.
Session ledger and export
A long session would grow to a huge, ever-slower context. oceancode avoids that with the
session ledger: the model sees a small, fixed window of recent turns, and everything older
is kept verbatim in Markdown files beside the session — never summarized away.
The full concept is in SESSION-LEDGER-CONCEPT.md (apps/ocean-coding-agent-rust/docs/SESSION-LEDGER-CONCEPT.md); this page is
the operator's view.
What the ledger is
- A window of the raw conversation. The model always sees the newest part of the real
conversation, up to a bounded size — by default 40% of the model's context (400k on a 1M
model, 80k on a 200k model). The window is measured in the provider's real tokens after every
step, and the oldest content leaves it one block at a time — inside a long turn
too (see The tideline below).
tokens: 0instead tracks the model's own ceiling — its context window minus the output allowance; 120k only when the route reports no limit. A turn is your message plus the model's full answer, tool outputs included. - Per-turn transcripts on disk. Every step is written verbatim to its turn's transcript file as it ends, and the index files are updated when the turn completes. As the window grows the oldest blocks leave it — they are already on disk, so nothing is lost and no model call is made.
SESSION.md, one living page. Goal, decisions, environment facts, errors and their fixes, a code map, open questions, and the next step — each line stamped with the turn it came from. Facts the database already holds (files touched, commands and exit codes, tasks, the log) are written by the engine. The page is edited, never rewritten, and by one writer; reversed decisions keep the old text struck through rather than deleted.
The ledger ships enabled (ledger.enabled: true), so every session is laddered from its first turn with no setup; its per-turn transcripts are retained for 30 days by default (ledger.retention_days). See 02 · Configuration to change either. OceanCode never summarizes a session — the legacy compaction (summarization) path runs only when ledger.enabled is false.
Find a session's ledger directory with:
oceancode ledger path <session-id>
The tideline — /tideline
You set one number: the share of the model's context where the tideline sits
(ledger.tideline.percent, default 40) — the line. After every step the ledger reads the
window's size in the provider's real tokens (the gauge: input + cache read + cache write of the
step that just ended, plus what that step added — its output and tool results; with compress on, plus the
compressor's saved estimate, removed bytes ÷ 4, so the gauge still measures the uncompressed window). Steps are grouped
into blocks: a block closes once the gauge has grown by ledger.tideline.block since it opened — a
token count, or a share of the line written "35%" (unset = 70% of the line, so 70 000 at a
100 000-token line). While the gauge is at or over the line, the oldest closed block leaves the
window, so the window drops to about line − block and grows again. The block still being filled is
never cut, so the window overshoots the line by at most one block. A single turn larger than the
line is cut block by block while it runs; your message in the oldest block leaves with it.
After a cut the window opens with one line that points at the ledger:
[Earlier content of this session is past the tideline: every row up to seq <n> left this window. The verbatim record is <ledger>/transcript/ (one file per turn) and SESSION.md carries the merged state. …].
A turn whose every row is past the tideline is merged into SESSION.md; a turn cut only in part is
not. If the provider still refuses a request as too long, every closed block is cut before the one
retry.
Between cuts every step only appends, so the provider's prompt cache hits; each cut re-writes the
retained window once. The block size sets how often: a 50k block cuts about 20 times per 1M new
tokens past the line, a 20k block 50 times. When the model's context is unknown the fixed tokens
(default 300000) is the line. A line above what the model can hold (its context minus the output
allowance, at most 32k) is clamped to that, with one note in the log.
The status bar's context row measures against the model's own context (…/1.0M on
claude-fable-5-1[1m]); the ┃ on its gauge sits at the tideline — tideline @40% (400k): 238k to go, or tideline @300k: 138k to go under a fixed limit — and it reads tideline off when the ledger is disabled.
Config examples (ledger.shelve and ledger.window are still read as aliases):
{ "ledger": { "tideline": { "percent": 50 } } }
{ "ledger": { "tideline": { "tokens": 500000, "block": 20000 } } }
tokens: 0 means the model's own ceiling. The older keys — the floor (low), reserve, the
profile presets (wide · narrow · strict · auto) and ledger.hard_tokens — were removed
with the band geometry: an old config still loads, they are ignored, and the load logs one warning
naming them.
Change the tideline for a session at runtime with /tideline:
| Command | Effect |
|---|---|
/tideline |
Show the current tideline state. |
/tideline now |
Move the tide now (a cut). |
/tideline 50% |
Set the limit as a percent of the model's context. |
/tideline 500k |
Set a fixed limit. |
/tideline default |
Restore the configured limit. |
Minimum fixed limit 20k; k and m suffixes are accepted. The floor and the presets were removed;
a second number or a preset word is refused with the usage line. A change applies from your next
message on, which the confirmation says. Headless, the same set is
--tideline "<percent>% | <limit> | default". A sub-agent (child) session inherits its parent's
/tideline override — the percentage, or the limit.
The meter's denominator is the model's context window under auto and the effective window
otherwise.
The session meter
Ocean pager has no cockpit panel; its status bar and /context show the session meter:
- the context meter — what the current window costs against the session's window size;
- token counts — input and output for the session;
- spend —
$spent / $capon a metered route. On the Claude Pro/Max subscription route the bill is a list-price estimate, shown as≈$spent list · subscription; alimitsrow appears when a step or time bound is set.
On the Claude Code route the meter's context number is Claude Code's own real context — its
system prompt, CLAUDE.md files and MCP schemas, ~100k+ before any turn. When it reaches the limit
OceanCode drops the --resume id and cold-starts Claude Code with the ledger page plus the retained
turns' text (tool outputs are not re-sent — the page's links carry them), so the number restarts near
that baseline and climbs again.
Recovering a step — /undo and /redo
/undo reverts the previous message and /redo re-applies a reverted one, stepping a bookmark
through the session so you can back out a bad turn without losing the history.
Exporting a session
Write a whole session out as one file:
oceancode export <session-id> --md [--out <path>|-] # one Markdown document
oceancode export <session-id> --atif [--out <path>|-] # one ATIF-v1.8 trajectory (JSON)
Pass exactly one of --md or --atif. --out - (the default) writes to stdout; --out <path> writes a file. The Markdown export carries frontmatter, every turn, and a ## Summary
table, and is built from the ledger's own transcript files when they exist.
Rebuilding the ledger
If a ledger directory is lost or a session predates the ledger, rebuild it from the database:
oceancode ledger rebuild <session-id> # mechanical: transcripts, indexes, page
oceancode ledger rebuild <session-id> --merge # also fold the merge backlog (model calls; asks y/N)
oceancode ledger rebuild <session-id> --merge --yes # skip the confirmation
The plain rebuild makes no model calls; --merge does, so it is never implied.
See also
- 02 · Configuration — the
ledgeranddatabaseconfig sections. - 08 · Bounding a long run — spend and clock bounds.
Headless and server
oceancode runs without a UI two ways: a one-shot run for scripts and CI, and a long-lived
serve that exposes the engine's HTTP surface to clients. The terminal UI is Ocean pager, which
embeds the engine in its own process (see 19 · Ocean pager); serve is the
headless surface.
One-shot — oceancode run
Run a single non-interactive turn and exit.
oceancode run "explain what this repo does"
oceancode run --format json "fix the failing test"
oceancode run --continue "and now add a test for it"
Flags go before the prompt (a flag after it is sent to the model as text, with a warning). When
stdin is not a terminal, run reads it to EOF and appends it to the prompt — the piped body is
the instruction — so a script that passes the prompt as an argument must close stdin
(oceancode run "hi" </dev/null) or the run waits forever (observed 2026-10-05: four minutes of
nothing with stdin left open).
Useful flags:
| Flag | Meaning |
|---|---|
--output-format <fmt> |
stream-json (newline-delimited events, then the final result envelope), json (a single buffered result object), or text (human, default). Supersedes --format. |
--format json |
Deprecated alias of --output-format stream-json — output unchanged. Emits newline-delimited events, then one final result envelope (ok, steps, durationMs, messages, tokens, cost) — written on the failure path too. |
-r <title> / --resume-title <title> |
Resume the most recent session whose title matches <title>. |
--tools <a,b> |
Restrict the advertised tool set to these ids (an unknown id is an error). |
--disallowed-tools <a,b> |
Remove these ids from the advertised set. |
--allow <Tool(pat),…> |
Append allow permission rules for the run. |
--deny <Tool(pat),…> |
Append deny permission rules (last-match-wins; a --deny beats an overlapping --allow). |
--append-system-prompt <text> |
Append text to the run's system context. |
--json-schema <path|json> |
Structured output: validate the model's final JSON against this JSON Schema (a file path or inline JSON). Threads the schema into the prompt, re-prompts with the validation errors up to a bounded retry cap. On success the validated object is the result — printed bare in text framing, or as result.structured on the --format json result envelope (exit 0). An unsatisfiable or uncompilable schema exits 6 with the typed error; a run cut short keeps its typed exit code (budget 3 / time 4 / no-progress 5 / permission 2 / interrupt 130). |
--continue / -c |
Continue the last session. |
--session <id> / -s |
Continue a specific session. |
--fork |
Fork the session before continuing (needs --continue or --session). |
--agent <name> |
Start in a specific agent. |
--model <provider/model> / -m |
Use a specific model. |
--directory <dir> |
Working directory (defaults to the current one). |
--db <path> |
Session database file. |
--atif <path> |
Write the session's ATIF-v1.8 trajectory (JSON) to <path> at the end of the run — the same document oceancode export --atif produces. Empty/unset = off. See 06 · Session ledger and export. |
--subagent <provider/model> |
/subagent for a headless run: the sub-agent model this session's children run (e.g. openai/gpt-5-mini). Written onto the session (survives --continue). A value that is not provider/model is refused with the same words /subagent uses. Empty/unset leaves whatever the session already carries. |
Bound flags (--max-usd, --max-minutes, --max-steps, --tideline, --subagent) must come before the
prompt — the parser stops at the first non-flag word, and a bound placed after the prompt is
ignored with a warning. --tideline <pct>% sets the tideline as a share of the model's
context (e.g. --tideline 50%); --tideline <limit> or default also work. The floor and the
presets were removed: a second number or a preset word is refused with the usage line.
The old --window name is refused with the hint use --tideline; --shelve still works as an alias. See
08 · Bounding a long run.
The engine also serves GET /session/{id}/subagent → {"model": <ModelRef> | null} — the session's
effective sub-agent model (the /subagent override, else the fleet router's CLAUDE_CODE_SUBAGENT_MODEL
while the subscription slot is live, else the subagent.model config, else the session's own model). A client
reads it for the identity row's sub-agents <name> segment; an engine without the route answers as
"nothing to show".
Host-run workflows — oceancode workflow
Launch and control a host-run Rhai workflow without a UI. workflow run boots the engine the
same way oceancode run does, launches the workflow, streams each workflow_updated body as
one JSON line on stdout, and exits by the run's outcome.
oceancode workflow run pipeline --args '{"steps":[{"prompt":"say A"},{"prompt":"say B"}]}'
oceancode workflow run ./research.rhai --budget 64 --model openai/gpt-5-mini
oceancode workflow run pipeline --validate-only --args '{"steps":[{"prompt":"x"}]}'
oceancode workflow runs --all
oceancode workflow stop wf-…
The workflow name (or a path to a .rhai file) comes first, before any flags. A registered
name (a built-in such as pipeline, or a discovered .oceancode/workflows/<name>.rhai) is
resolved by the launcher; a path you type is read and run as an inline script.
Flag (of workflow run) |
Meaning |
|---|---|
--args <json> |
A JSON object bound to the script's args global (e.g. a pipeline's steps). Empty = {}. |
--budget <n> |
Absolute cap on cumulative child-agent calls for this run, 1..=1024. Unset uses the workflows.agent_budget config default. |
--validate-only |
Run a metadata + compile smoke check only — prints a validated body and launches no agent (no provider call). |
--model <provider/model> / -m |
Fill this model in for workflow steps that specify none (a pipeline's args.steps[].model, else top-level args.model). It never overrides a model a step already carries, and there is no default provider — a step left unset follows the session policy. |
--directory <dir> |
Working directory the run and its sub-agents use (defaults to the current one). |
--db <path> |
Session database file. |
--verbose / -v |
Trace outbound LLM provider calls to stderr. |
Exit codes (the run's outcome): 0 complete; 2 failed, cancelled or
budget_limited; 3 a pause (user_paused, back_off_paused, no_progress_paused,
infra_paused, blocked). The last streamed line always carries the terminating body, so a
script can read the outcome from stdout as well as from $?.
workflow runs [--all] lists the persisted runs (each workflow_updated body, one per line),
read from the run directories under the app data dir — scoped to the current project unless
--all lifts the filter. workflow pause|resume|stop <run_id> drive the launcher's control
seam; resume takes an optional --budget <n> to lift a budget-limited run. These control a
run live in the engine this command boots — a run launched by a separate oceancode workflow run has already exited in its own process, so cross-process control is a shared-server
concern not wired here.
Goal mode — oceancode goal
Prove an objective is done, not merely claim it — headless. oceancode goal "<objective>" boots
the engine like oceancode run does, launches the built-in goal workflow (Plan → Work →
Verify: plan the objective into verifiable deliverables, work in rounds with one resumed
worker, and converge only when a cold panel of adversarial skeptics fails to refute completion),
streams each goal_updated body as one JSON line on stdout, and exits by the run's outcome. It is
the headless twin of the pager's orchestrate: always goal mode (19-pager.md).
oceancode goal "add a --json flag to the status command and prove it works"
oceancode goal --budget 64 --model openai/gpt-5-mini "port the parser to the new API"
The objective is the positional argument — quote it, or let the words after the flags form it. Go-style flag parsing stops at the first positional, so any flag must come before the objective.
| Flag | Meaning |
|---|---|
--budget <n> |
Absolute cap on cumulative child-agent calls for the whole goal, 1..=1024. Unset uses the workflows.agent_budget config default. |
--model <provider/model> / -m |
Run-level default model: pins every one of the goal's agents (planner, worker, skeptics) whose own step set no model. There is no default provider — unset, each agent follows the session policy. |
--directory <dir> |
Working directory the goal and its sub-agents use (defaults to the current one). |
--db <path> |
Session database file. |
--verbose / -v |
Trace outbound LLM provider calls to stderr. |
Exit codes (the run's outcome): 0 complete; 2 failed, interrupted (a stopped
run) or budget_limited; 3 a pause (user_paused, back_off_paused, no_progress_paused,
infra_paused, blocked) — a goal that stalls on identical gaps two rounds running, or spends
its budget, pauses rather than looping forever. The last streamed line always carries the
terminating body, so the outcome reads from stdout as well as from $?. A finished goal is also
listed by oceancode workflow runs --all (it shares the workflow run store).
Long-lived server — oceancode serve
oceancode serve # loopback, port 4096
oceancode serve --port 8080
| Flag | Meaning | Default |
|---|---|---|
--port <n> |
TCP port to listen on. | 4096 |
--host <ip> |
Interface to bind. Loopback by default; a non-loopback host refuses to start without OC_SERVER_PASSWORD. |
127.0.0.1 |
--db <path> |
Session database file. | oceancode.db |
--directory <dir> |
Working directory. | current directory |
--verbose / -v |
Trace outbound LLM provider calls. | off |
The default shape (oceancode, the pager) passes --listen <host:port> through to the engine it
embeds; a non-loopback bind refuses to start without a server password (below). The guard is
enforced inside the server for both oceancode --listen and oceancode serve --host.
Authentication
HTTP basic auth is off until you set a password.
| Variable | Meaning | Default |
|---|---|---|
OC_SERVER_PASSWORD |
The basic-auth password. Empty means auth is disabled; any value turns it on for every route, SSE included. | unset (off) |
OC_SERVER_USERNAME |
The basic-auth username. | oceancode |
A non-loopback bind (--listen on the default shape, or serving to a public interface) refuses
to start unless OC_SERVER_PASSWORD is set.
Managing capabilities from the CLI
Besides run and serve, the binary answers a set of non-interactive management
subcommands that boot no session. Each is read-only or writes a single config file, and exits.
| Subcommand | What it does |
|---|---|
inspect [--json] |
Print the provenance of every config key, capability, MCP server and instruction file for the current directory. See 09 · Capabilities and plugins. |
doctor |
Report crash-recovery state: orphaned sessions (registry entries whose process died without a clean exit) and captured panic reports found under the data dir; prints no orphaned sessions when clean. Read-only. See 12 · Permissions and sandbox. |
plugin … |
Manage plugins and marketplaces: marketplace add|list|update|remove, install, list, enable/disable, trust [--off], validate, update/uninstall, and import --from claude-code [--trust] [--no-repo-trust] [--dry-run] (copy Claude Code's plugins, register its marketplaces, seed trust). See 09 · Capabilities and plugins. |
mcp … |
Manage MCP servers: add (--command/--url), list (every discovered server with name kind tier trusted enabled source), enable/disable, remove, login/logout, doctor. See 11 · MCP servers. |
skill … |
skill create <name> [--description <text>] scaffolds .oceancode/skills/<name>/SKILL.md; skill list [--json] prints every discovered skill with its tier, trust and enable state. See 09 · Capabilities and plugins. |
hooks … |
hooks list [--json] prints every hook source file the boot loader reads (tier, trust, loaded or held and why) with its handlers and ends with HOOKS_OK sources=<n> handlers=<m> loaded=<k>; hooks add <Event> --command <cmd> [--matcher <re>] [--timeout <s>] [--scope user|project|local] and hooks remove … edit the same settings.json files Claude Code writes. See 10 · Hooks. |
jev status [--json] | probe [--record …] | labels | questions --json | verdicts --in <file> |
The Jev decision layer: each decision point's mode and endpoint host (JEV_STATUS_OK points=7), a one-call live probe (JEV_PROBE_OK) and fixture recorder (probe --record, JEV_RECORD_OK), the export of every labelled decision from the per-session jev-decisions.jsonl files into bench/jev/labels.json (JEV_LABELS_OK), and the seams' question sets and policy verdicts for the bench. Reads the current directory and your environment (no --dir/--home); only probe calls Jev. See 13 · Jev decisions. |
trust … |
The persisted per-project trust record (~/.oceancode/trust.json): show (ends with TRUST_OK), repo on|off, mcp <name> on|off|clear. See 11 · MCP servers. |
migrate [--db <path>] |
Open and migrate a session database, then exit — the standalone form of the boot-time schema upgrade (--db defaults to oceancode.db). |
update [--self-test] |
Self-update to the newest release for the configured channel: verify the release signatures, swap oceancode and ocean-pager atomically, roll both back on a failed health check. --self-test proves the pair swap+rollback offline and prints UPDATE_OK from=<old> to=<new> rollback_tested=1 then UPDATE_SELF_TEST_OK binaries=2. Channel/opt-out via update.*, OC_UPDATE_CHANNEL, OC_NO_UPDATE_CHECK. See 17 · Distribution and updates. |
verify-release <artifact> [--pubkey <base64>] [--sha256sums <path>] |
Verify a release artifact's minisign .minisig signature (and, with --sha256sums, its SHA256SUMS entry) in-process — no external tool. Prints VERIFY_RELEASE_OK file=<name>. The portable trust check on macOS, Linux and Windows. See 17. |
uninstall [--purge] [--yes] [--dry-run] |
Remove the binary the installer placed; --purge also removes the app's config and data dirs. Requires a typed oceancode confirmation unless --yes; --dry-run reports the plan and removes nothing. Prints UNINSTALL_OK removed=<n>. See 17. |
login [--provider claude|copilot|codex] [--no-browser] |
Log in and store the credential in auth.json. claude (default) runs the Claude Pro/Max subscription OAuth; copilot and codex run the OAuth 2.0 device-code flow. See 01 · Getting started. |
logout [provider] |
Clear the stored credential for a provider (default anthropic) from auth.json; the counterpart to oceancode login / oceancode auth. See 01 · Getting started. |
claude |
Preflight the claude-code provider: report whether the Claude Code CLI is installed and logged in. Advisory only — it never blocks a boot. |
Every management subcommand takes --dir <d> (the project; default: the current directory) and
--home <h> (the home the user tier and ~/.oceancode are read from; default: your home) so a
script — or a test — can point it at another tree.
Permission mode
A client reads and switches the session's permission mode over HTTP — the boot
default is the permission_mode config key, and these two routes change it live
(the pager switches it from its own prompt):
| Route | Meaning |
|---|---|
GET /api/session/{id}/permission/mode |
Returns { mode, bypass_locked } for the session. |
POST /api/session/{id}/permission/mode |
Set the mode; body { "mode": "acceptEdits" }. An unknown mode is 400; bypassPermissions while a managed requirements file has locked it is 409. |
The four modes are default, acceptEdits, dontAsk, bypassPermissions — see
12 · Permissions and sandbox.
Managed (enterprise) configuration
For a shared or locked-down deployment, an administrator layers a managed config and a signed requirements file over every project:
OC_MANAGED_CONFIG— path to the managed config file (default: the OS admin path). It is loaded last, so it overrides the project and global files.OC_REQUIREMENTS_PUBKEY— the trusted ed25519 public key (64 hex chars) that arequirements.jsonsignature is checked against; an unsigned or tampered file is refused. With no key set, managed requirements are not enforced.
Both are described in 02 · Configuration; the requirements
file can disable bypassPermissions, allow-list MCP servers, and pin config keys.
See 12 · Permissions and sandbox.
See also
- 01 · Getting started — the three boot shapes.
- 02 · Configuration — the server variables in one table.
Bounding a long run
A run is bounded by the money and time you set, not by an arbitrary step count. Set nothing and a run keeps going until the work is done (or the no-progress guard stops it, below).
The bounds
| Bound | Environment variable | oceancode run flag |
Slash command (this session) |
|---|---|---|---|
| Spend cap (USD) | OC_MAX_USD |
--max-usd 25 |
/budget 25 |
| No spend cap | — | --max-usd off |
/budget off |
| Subscription (no cap; spend is a list-price estimate) | — | --max-usd subscription |
/budget subscription |
| Wall clock | OC_MAX_MINUTES |
--max-minutes 8h (also 45, 90m, 2d, off) |
/limits time 8h |
| Steps per turn (root), 1–1000 | OC_MAX_STEPS |
--max-steps 100 (default clears) |
/limits steps 100 |
| Steps per sub-agent turn | OC_MAX_SUBAGENT_STEPS |
— | — |
| Tideline (% of context) | ledger.tideline.percent (config) |
--tideline 50% |
/tideline 50% |
| Tideline (fixed tokens) | ledger.tideline.tokens (config) |
--tideline 60k |
/tideline 60k |
| Tideline block (what one cut removes) | ledger.tideline.block (config: a token count, or a share of the line as "35%"; unset = 70 % of the line) |
— | — |
Precedence: slash command / flag > environment variable > built-in default. Flag and slash
values are stored on the session, so they survive --continue.
The tideline's old names still work as aliases: --shelve, /shelve and ledger.shelve.
The step cap is a boundary, not a stop
OC_MAX_STEPS (default 300) caps the steps in one root turn, but a root turn that reaches it
auto-continues into a fresh turn — no budget or clock needs to be set. The ledger cuts by
blocks after every step, so even one long turn stays under the tideline (see
06 · Session ledger and export). 0 is
rejected: bound a long run with a budget or a clock instead.
Sub-agents are different: they simply stop at OC_MAX_SUBAGENT_STEPS (default 300), so a
raised root cap does not deepen them.
Output tokens
OC_MAX_OUTPUT_TOKENS overrides the per-request output-token cap in either direction, clamped
to the model's own published maximum. It bounds how much a single model response may emit, not
the run as a whole; leave it unset to use the model's full maximum.
What happens at a bound
Exceeding a bound is a clean, non-error stop, distinct from a crash, and the ledger records
it as a step-limited turn. The internal stop reasons are BudgetExceeded and TimeExceeded.
Over the Ocean tunnel the response carries a typed stopped_by:
| Bound reached | stopped_by |
|---|---|
OC_MAX_MINUTES / /limits time |
time-exceeded |
OC_MAX_USD / /budget |
budget-exceeded |
| step cap | step-limit |
The plan drives the run
The model keeps its plan as a todo list (see 05 · Plan mode and todos). A root turn that settles while any todo is still open does not end the run — the engine works off the next open todos and drains again. A run ends only when:
- the work settles with no open todo; or
- a spend or clock bound trips; or
- you interrupt it; or
- the no-progress guard fires — two consecutive continued turns that leave the open and completed counts unchanged stop the run cleanly with a "Run stopped" toast.
So a model that never plans and never settles runs a few turns and stops; one that plans and makes progress runs until its list is done or a bound trips.
The Jev supervisor (opt-in)
With the Jev decision layer on (see 13 · Jev decisions), a supervisor
reads a root turn's clean finish and, every jev.supervisor_every_steps settled steps (default
4), the last six steps. With jev.supervisor = "act" it may keep a finish from ending the turn —
a synthesized continuation, at most jev.supervisor_max_continuations per human prompt
(default 1), only when the goal is clearly not complete and progress is real — and may send one steer message when the steps repeat without
progress. It never stops a run and never moves a bound: the spend, clock and step bounds above,
the exact-repeat doom-loop trip, the hook cap and the no-progress guard decide exactly as they do
without it. In shadow, the default once the layer is enabled, it only records what it would
have done.
Stopping a run stops all of it
esc esc (or the palette's abort) stops the session's own drain at its next step boundary,
and every sub-agent stops with it — foreground ones immediately, background ones cancelled by
the parent. A late background result is recorded as cancelled and never restarts the run.
Background jobs and long waits have no clock of their own
A job the model starts — bash with background: true, a monitor, a loop, a scheduler —
runs until it ends or the model calls job_kill. It dies with the run only when the run has a
wall clock (OC_MAX_MINUTES / /limits time); without one a build, a watch or a server runs for
hours or days, and job_output reads it back whenever the model asks. (Until 2026-10-05 every
job was reaped ten minutes after launch while the run had no clock at all.)
A long wait does not lose the prompt cache either. Every request's cache markers carry the
provider's one-hour TTL, and while a step waits — on a sub-agent, on a long bash — or the
session idles, the engine re-reads the last request every OC_CACHE_KEEPALIVE_MINUTES
(default 50) so that clock restarts: a cache read of the prefix (session.cache.touched, cents)
instead of re-writing it when the wait ends. It stops after OC_CACHE_KEEPALIVE_MAX touches (default 48, ≈ 40 h; past a model's break-even — 80 touches on Fable 5.1, 20 on a 0.1× model — a re-write is the cheaper bet). A touch that fails is sent once more a few minutes later before the keep-alive gives up, and pressing Esc on a running turn does not stop it — the next prompt reads the same prefix. See 02 · Configuration.
See also
- 02 · Configuration — the
OC_MAX_*variables in one table. - 07 · Headless and server — the
--max-*flags onoceancode run.
Capabilities and plugins
Skills, sub-agents, personas, commands and MCP servers are all capabilities, and oceancode
finds them the same way: one discovery walk over five tiers, across four ecosystems, with a
single rule for names that collide and a separate rule for what a capability is allowed to do.
The five tiers
A capability is discovered from one of five tiers. The walk runs from the narrowest scope out to the widest, so a name defined closer to the code wins.
| Tier | Where it comes from | Example root |
|---|---|---|
| Local | The project's own .oceancode/ — your machine's private copy, git-ignored |
.oceancode/skills, .oceancode/agents |
| Repo | The project you are in, checked into the repo | .claude/, .opencode/, .grok/ |
| User | Your home directory, read across every ecosystem | ~/.oceancode/…, ~/.claude/…, ~/.grok/… |
| Bundled | Shipped inside the binary | (built in) |
| Plugin | An installed plugin's own tree | <plugin>/skills/, <plugin>/agents/ |
Plugin-tier roots are enumerated from disk — one root per installed plugin directory — so a
plugin contributes its skills, agents and hooks only while it is installed. For plugins Claude
Code installed, the roots are exact: only the version directories its
~/.claude/plugins/installed_plugins.json records for the user scope, or for this project, are
walked (one root per plugin even when the file lists the same install twice), and a plugin you
have disabled in ~/.claude/settings.json (enabledPlugins) stays disabled here. A project's
own .claude/settings.json can never re-enable a plugin you disabled.
The .oceancode/ directory in a project is the local tier — your machine's private copy,
trusted precisely because it is not shared repo content — and is git-ignored in this monorepo.
Four ecosystems
Each tier is read for four ecosystems at once, so a capability written for another tool is picked up without conversion:
- OceanCode — the native
.oceancode/layout. - Claude Code —
.claude/(skills, agents, rules,settings.json). - OpenCode —
.opencode/(opencode.json, agents). - Grok Build —
.grok/(agents, personas, rules, memory).
Frontmatter and directory conventions from each ecosystem are translated into the one internal shape as they are read; you do not have to rewrite a Claude Code skill or a Grok persona to use it here.
Rule files may pull in other files with @path imports (up to four hops); an import is bounded to
the project directory and your home directory, so a rule can never inline a file from elsewhere on
disk.
A rule may scope itself to a file glob in its frontmatter — paths: "**/*.py" (Claude Code / OpenCode)
or globs: "**/*.ts" (a Cursor .mdc). A scoped rule is loaded only when the working tree actually
contains at least one file matching its glob; in a checkout with no match it is left out entirely, so a
TypeScript project does not carry Python or Ruby rule bodies. The check honours your ignore rules
(.gitignore, .git pruned) and runs once per session. Scope today is resolved for the whole checkout,
not per individual file the current turn touches. A rule with no paths/globs is always loaded — with
one Cursor exception: a .mdc under .cursor/rules that says alwaysApply: false and has no globs is
Cursor's agent-requested or manual rule, which Cursor attaches only when asked, and the engine leaves it
out too (a globs: rule keeps its scope whatever the flag says; the key means nothing outside
.cursor/rules).
Workflow scripts
Workflow scripts are discovered the same way, from two OceanCode roots in the usual precedence
(built-in > project > user). Every *.rhai file under a root is one workflow, identified by
the meta.name its script declares — not by its file name:
| Scope | Root | Files |
|---|---|---|
| Project | .oceancode/workflows |
*.rhai |
| User | ~/.config/oceancode/workflows |
*.rhai |
Two workflows ship built in to the binary — the highest tier — so they run by name with no
*.rhai file on disk. Built-ins cannot be shadowed: a project or user script with the same
meta.name as a built-in is ignored (dropped from the picker catalog with a warning), and the
built-in still runs — that is what "built-in > project > user, first wins" means. A duplicate
meta.name across two discovered files likewise keeps the first (project before user).
| Name | Args | What it does |
|---|---|---|
deep-research |
query (or objective); breadth 2–6, default 4 |
Researches the query with bounded parallelism across four phases — Plan, Research, Verify, Report — independently cross-checks every candidate claim, and writes a cited report. |
goal |
objective; skeptics 1–5, default 3; max_rounds, default 10; optional worker_model, verifier_model |
Proves an objective is done, not just claimed done: plans it into verifiable deliverables, works in rounds with one resumed worker, then has a panel of adversarial skeptics try to refute completion. Converges only when a strict majority of the cold panel fails to refute; pauses on identical gaps two rounds running or a spent agent budget. |
pipeline |
steps: [{ prompt, label?, model?, agent_type?, output_schema? }]; stop_on_failure, default true |
Runs an ordered list of agent steps one after another; each step is given the previous step's output. Stops at the first failed step unless stop_on_failure is false. |
Names that collide are qualified; unique names stay bare
A capability keeps its bare name when that name is unique across everything discovered. Only
when the same name appears more than once is it qualified, and only the colliding members are
rewritten:
| Situation | Qualified name |
|---|---|
| Same name at two different tiers | tier:name (e.g. repo:review) |
| Same tier, different ecosystems | tier.<ecosystem>:name (e.g. repo.claude_code:review, repo.open_code:review) |
| A plugin tier | <plugin>:name |
| A residual same-tier, same-ecosystem tie | tier.<ecosystem>.<root-dir>:name |
The pass reads only each item's bare name and source, so it is idempotent — running discovery again yields the same qualified names in the same order.
The slash name of a plugin skill is always <plugin>:name, even when the name is unique —
Claude Code's convention, so /superpowers:writing-plans lists and runs in the Ocean pager's /
popup exactly as it does there. Typed as a prompt, /<plugin>:name and the bare /name both run
an enabled plugin skill; a <plugin>: prefix that names no installed plugin does not resolve (a warning toast; the text is then sent as a normal message).
Enable is not trust
Two independent gates decide what a capability does:
- Enabled — whether the capability is discovered and offered at all. Disabling a plugin removes its capabilities from the walk — including its rule bodies, which no longer inject into the system prompt (its skills, agents, commands, hooks and MCP servers drop too).
- Trusted — whether a capability that can execute code on your behalf is allowed to run without a prompt. Only hooks and MCP servers gate on trust; a skill or a persona is inert data and needs no trust decision.
By default, Local, User and Bundled tiers are trusted; the Repo tier is untrusted (code that arrived with a checkout has not been vetted by you); a Plugin is trusted only if you installed it with trust. So enabling a repo-supplied hook is not the same as trusting it — it is discovered, but it will not run its command until you trust it.
Trust decisions for a project persist in ~/.oceancode/trust.json, keyed by the project's
absolute path: repo (may the repo tier's hooks and .mcp.json servers execute?), mcp
(per-server overrides that beat repo) and disabled_mcp (servers held disabled at any tier).
Plugin trust lives beside the plugin in ~/.oceancode/plugins/installed.json. Set them with
oceancode trust repo on|off, oceancode trust mcp <name> on|off|clear, oceancode plugin trust <name> [--off], or from the /hooks, /mcp and /plugins pickers; read them with
oceancode trust show.
Enabling and disabling skills
Loading every skill from every tier costs prompt tokens — one <available_skills> line and one
skill_* tool per skill, plus a disabled plugin's rule bodies. Skills, plugins and MCP servers
share one enable interface so you can turn off what you are not using; a disabled skill costs
nothing (it is not in <available_skills>, registers no skill_* tool, and its rules do not
inject).
A skill's effective state is the fold of four tiers, resolved in this order — the first that turns it off wins, so a project can disable a skill but can never re-enable one you disabled:
| # | Tier | Turned off by | skill list reason |
|---|---|---|---|
| 1 | Plugin | The skill's plugin is disabled (oceancode plugin disable) |
disabled (plugin) |
| 2 | User | You disabled the skill (~/.oceancode/skill-state.json) |
disabled (user) |
| 3 | Project | skills.disabled in .oceancode/oceancode.json |
disabled (project) |
| 4 | Allowlist | skills.mode = "allowlist" and the skill is not in skills.enabled |
not in allowlist |
The per-skill user toggle is written by the CLI (or the /skills picker) into
~/.oceancode/skill-state.json — OceanCode's own store, never Claude's ~/.claude/settings.json.
The project policy (mode, disabled, enabled) lives in .oceancode/oceancode.json under
skills (see 02 · Configuration); default mode all keeps every skill a
candidate. The project policy is a default: an explicit enable of one skill (Space on its row,
a plugin-group header's Space, or oceancode skill enable <id>) is also recorded for this project
as enabled_skills in ~/.oceancode/trust.json and beats the project's skills.disabled and
allowlist (tiers 3–4), so a skill the allowlist leaves out can be switched on without editing the
config; it never beats the master switch or a disabled plugin, a disable drops it again, and
enable-all stays user-tier only. MCP keeps its own disabled_mcp / enabled_mcp model
(oceancode mcp enable|disable, see 11 · MCP servers); the verbs are the same shape.
oceancode skill enable <id> # clear the user-tier disable; beats the project policy
oceancode skill disable <id> # disable this skill at the user tier
oceancode skill enable-all # clear every user-tier skill disable
oceancode skill disable-all # disable every currently-discovered skill
<id> is a skill's stable id <tier>.<eco>:<name> — e.g. user.claude_code:agents-sdk,
local.ocean_code:tdd, repo.open_code:deep — shown in the id column of skill list. It is
computed the same way whether or not the name collides with another tier, so the store never loses
a disable when a session sees only one tier. You may also pass a bare name when exactly one
discovered skill has it (oceancode skill disable wrangler); a name shared by more than one skill
is refused with the candidate ids, so pass the full id to pick one. A bare name is still accepted
in skills.disabled/skills.enabled and as a legacy store entry (it matches every copy of that
name). If you disabled skills before this change, re-run oceancode skill disable-all once to
rewrite the store with stable ids.
A toggle takes effect on the next step of the running session. The /skills and capabilities
pickers, the /skill invocation gate and the Ocean pager's composer / completion update at once
(an enabled skill is offered as /<skill>, a disabled one is not); the <available_skills> block,
the skill_* tools, the MCP tool list and the hook dispatcher are rebuilt before the agent's next
model call, so the very next request no longer carries what you turned off. A step already running
finishes with what it started with. Each change made mid-session costs one system-prompt cache
rewrite (the cached prefix changes once), so flip several things together rather than one per
turn. A change made from a shell (oceancode skill disable-all) lands on the next step too.
Whole groups and everything at once. In the Ocean pager's capabilities picker (/skills,
/plugins, /hooks, /mcps) Space works at four levels:
| Row under the cursor | Space toggles |
Asks first |
|---|---|---|
| A skill, plugin, hook or MCP server | that one item | no |
A Plugin: <name> header (Skills, MCP tabs) or a hook source header (Hooks tab) |
every row in the group | above 10 rows |
All skills / All plugins / All MCP / All hooks (second row of the tab) |
the whole kind | yes |
Everything (first row of every tab) |
the master switch | yes |
Everything off removes skills, MCP tools, hooks and plugin rules from the agent's context; the
built-in tools (bash, read, edit, …) stay. Turning it back on restores exactly the per-item state
you had. A group whose rows are all off shows [off] on its header, and the picker's last row
shows the live counts — skills 22/259 · plugins 2/21 · mcp 1/10 · hooks 8/8 — with
EVERYTHING OFF in front while the master switch is off. Group and master state is per project,
stored next to disabled_mcp in ~/.oceancode/trust.json; per-skill disables stay in
skill-state.json. The Plugins tab lists Claude Code's cached plugins (under User (Claude))
beside the OceanCode-store ones, and Space turns a whole plugin — its skills, hooks, MCP servers
and rules — off or on.
Start from nothing. To strip the prompt down and add capabilities back deliberately, disable
everything and re-enable one by one: oceancode plugin disable-all (then re-enable from
/plugins), and oceancode skill disable-all with skills.mode = "allowlist" + a short
skills.enabled list for the handful of skills you actually want. With every plugin disabled and
an empty allowlist, the first turn carries no <available_skills> block and no skill_* tools at
all.
oceancode inspect — where did this come from?
To see the winning source of every capability, config key, MCP server and instruction file for the current directory:
oceancode inspect # a human-readable provenance table
oceancode inspect --json # a stable JSON array (for scripts)
Each row names the kind, the id, the layer that won, and the file it came from — the fastest way to answer "why is this skill loaded and not that one?". It boots nothing and is read-only.
instruction rows list what the directory contributes to the system prompt: one primary file —
CLAUDE.md, else AGENTS.md, never both, so a generated AGENTS.md mirror is not loaded twice —
then CLAUDE.local.md and CONTEXT.md when present. The same rule applies at every level from the
worktree root down to the working directory, and to the reminder that fires when a tool first touches
a directory this session. The system prompt is fixed for the rest of a prompt, so an edit to these files
(or a URL instruction expiring, or midnight passing) made while a prompt runs applies from the next
prompt, not the next step; switching a capability or the model still applies on the very next step.
Plugins and marketplaces
A plugin bundles skills, agents and hooks under one installable tree. Manage them from the CLI:
oceancode plugin marketplace add <spec> # register a marketplace
oceancode plugin marketplace list
oceancode plugin marketplace update <name>
oceancode plugin marketplace remove <name>
oceancode plugin install <name-or-path> [--trust] [--marketplace <m>]
oceancode plugin list
oceancode plugin enable <name> # / disable <name>
oceancode plugin enable-all # / disable-all — flip every installed plugin
oceancode plugin trust <name> [--off] # grant / revoke trust
oceancode plugin validate <name> # check the manifest
oceancode plugin update <name> # / uninstall <name>
install without --trust enables the plugin but leaves it untrusted, so its hooks and MCP
servers stay dormant until you run plugin trust. Pass --root <dir> to any plugin subcommand
to point at a project other than the current directory.
In Ocean pager, /plugins opens the Plugins tab of the capabilities picker; see
19 · Ocean pager.
Importing from Claude Code
If you already use Claude Code, one command brings its setup here:
oceancode plugin import --from claude-code --dry-run # show what would happen, write nothing
oceancode plugin import --from claude-code --trust # do it, trusting the imported plugins
What it does, and what it deliberately does not:
- Plugins are copied from Claude Code's cache (
~/.claude/plugins/cache/<marketplace>/<name>/<version>/) into~/.oceancode/plugins— only the versions installed for the user scope or for this project — and each is enabled or disabled as your~/.claude/settings.jsonsays. Without--trustthey are enabled but untrusted, so their hooks and MCP servers stay dormant. - Marketplaces Claude Code knows (
known_marketplaces.json) are registered. - Trust is seeded from Claude Code's own record in
~/.claude.json: the project'shasTrustDialogAcceptedbecomesrepo=true(pass--no-repo-trustto opt out), servers indisabledMcpServers/disabledMcpjsonServersare held disabled,enabledMcpjsonServersare trusted. - Skills, commands, agents, rules, hooks and MCP configs at the user and repo tiers are not
copied.
~/.claude/skills,~/.claude/commands,~/.claude/settings.json,.claude/**and.mcp.jsonare read where they are, so they stay in sync with Claude Code.
Every line the command prints names one action (IMPORT, SKIP, MISSING, MARKETPLACE,
TRUST); the last line is IMPORT_OK plugins=<n> marketplaces=<m> repo_trust=<bool> (with
--dry-run, IMPORT_DRY_RUN_OK …). A cache tree that is not a plugin — Claude Code's
rust-analyzer-lsp / typescript-lsp entries carry neither a manifest nor a SKILL.md — is
reported SKIP <name> not a plugin and one entry that fails to install is reported
SKIP <name> install failed: <why>; neither stops the run, so the trust seeding at the end always
happens. Nothing under ~/.claude is ever modified.
/skills — browse and run
Every discovered skill — yours, the project's, a plugin's, from any of the four ecosystems — is a
slash command: /<name> <arguments> runs it as a command turn whose prompt is the skill body
prefaced by a one-line instruction, with $ARGUMENTS and the positional rules applied. A name
that collides across tiers is invoked by its qualified form (/repo:review). The completion popup
lists skills with the other commands.
In Ocean pager, /skills opens the Skills tab of the capabilities picker and the composer's /
completion lists your enabled skills; see
19 · Ocean pager. Without a session:
oceancode skill list # id tier trusted|untrusted <state> description
oceancode skill list --json # each row carries "id", "enabled" and a "reason" tag when disabled
The id column is the stable <tier>.<eco>:<name> id (--json also keeps the collision-resolved
qualified name); copy it straight into oceancode skill enable|disable <id>.
The state column reads enabled, or why the skill is off — disabled (plugin),
disabled (user), disabled (project) or not in allowlist (see
Enabling and disabling skills).
See also
- 10 · Hooks — the one capability that gates on trust and can change a run.
- 11 · MCP servers — the other trust-gated capability, and its own translation table.
- 19 · Ocean pager —
/plugins,/skills,/mcps,/hooksin the terminal UI; the pager's other commands are inocean-pager/crates/ocean-pager-ui/docs/user-guide/. - 07 · Headless and server — every management subcommand in one table.
Hooks
A hook runs your own command (or an HTTP endpoint) at a fixed point in a run and hands back a
typed decision the engine acts on. Hooks are configured in a Claude Code settings.json /
hooks.json shape, so a hook you already wrote for Claude Code runs here unchanged.
The nine events
A hook is registered against one of nine lifecycle events:
| Event | Fires |
|---|---|
PreToolUse |
Before a tool runs — can deny it or rewrite its input. |
PostToolUse |
After a tool returns. |
Stop |
When a turn is about to settle — can continue the run instead. |
PermissionDenied |
When a permission check refuses an action. |
UserPromptSubmit |
When you submit a prompt — can add context. |
SessionStart |
When a session opens. |
SessionEnd |
When a session closes. |
SubagentStop |
When a spawned sub-agent settles. |
PreTide |
Before the ledger cuts context out of the window (past the tideline). The old event name PreShelve still fires it as an alias. |
PreToolUse and PostToolUse carry the tool_name, so a handler can scope itself to one tool.
The settings.json shape
Handlers live in a settings.json (or hooks.json) under the same keys Claude Code uses — the
event name maps to a list of hook groups, each group a matcher plus one or more handlers:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "./scripts/guard.sh", "timeout": 10 }
]
}
]
}
}
A command handler is run through <shell> -c <command> (shell defaults to sh) in the
project directory, with the event payload as JSON on stdin and these variables on its
environment: OC_HOOK_EVENT (the event name), OC_PROJECT_DIR and CLAUDE_PROJECT_DIR (both
the project directory, so a Claude Code hook script that reads $CLAUDE_PROJECT_DIR works
unchanged). An http handler POSTs the payload to a URL with your headers. Hooks are discovered
as capabilities (see 09 · Capabilities and plugins) and, because they
execute on your behalf, they run only from a trusted source.
The payload is the Claude Code shape: every event object carries hook_event_name and cwd
(inserted only when absent, never overwriting a field the event already set); SessionStart
carries source: "startup", SessionEnd carries reason: "exit". A group's matcher is a
regex tested against tool_name for PreToolUse / PostToolUse / PermissionDenied, against
source for SessionStart and against reason for SessionEnd; the other events ignore it.
An absent or empty matcher matches everything. One documented gap: session_id is not available
at the hook seam, so a handler that keys on it sees no such field.
Where hooks are read from — and when they run
At boot (and on every reload) oceancode walks every ecosystem's hook files — its own
.oceancode/, Claude Code's .claude/ and Grok Build's .grok/ — in this order, and merges
their groups per event, so earlier tiers' handlers run first:
| Source | Tier | Runs when |
|---|---|---|
.oceancode/hooks.json in the project |
local | always — the local .oceancode/ tier is your machine's own copy, trusted. |
.claude/settings.json, .claude/settings.local.json in the project |
repo | only when the project is trusted (oceancode trust repo on, or t in /hooks); otherwise held. |
~/.claude/settings.json |
user | always — the user tier is trusted. |
.grok/hooks.json in the project, ~/.grok/hooks.json |
repo / user | Grok Build's hook files, gated exactly like the repo and user tiers above. |
<plugin>/hooks/hooks.json of an installed plugin |
plugin | only when the plugin is enabled and trusted; ${CLAUDE_PLUGIN_ROOT} in its commands is replaced with the plugin's root. |
A held source is still parsed and listed — with its handlers — so you can see exactly what
trusting it would enable. The reasons are held: untrusted (repo tier, project not trusted),
held: disabled plugin, unreadable and invalid.
Since 2026-09-10 the three Claude Code settings files are also watched: saving one reloads the
hooks (the same path r in /hooks and POST /hooks/reload take) together with the
Claude Code settings mirror — no reload needed
after editing ~/.claude/settings.json.
On the claude CLI route there is no double firing. When a session's provider route is the
claude command-line protocol, the claude subprocess reads the very same ~/.claude/settings.json,
.claude/settings.json, .claude/settings.local.json and its installed plugins' hooks/hooks.json,
and runs those hooks itself. On that route oceancode therefore dispatches only its own
.oceancode/ hooks (the ocean_code ecosystem, local and user tiers) and leaves every Claude
Code-shared source to Claude Code — a UserPromptSubmit handler in .claude/settings.local.json
fires once per prompt, not twice. Every other route (Anthropic API, OpenAI-compatible, Grok, …)
dispatches everything in the table above, since nothing else would. /hooks and GET /hooks
still list all sources on every route; the by-source report marks the shared ones
(shared_with_claude_code).
/hooks and oceancode hooks list
In Ocean pager, /hooks opens the Hooks tab of the capabilities picker (toggle and reload go
through the engine); see 19 · Ocean pager.
Without a session:
oceancode hooks list # one line per source, then its handlers indented
oceancode hooks list --json # {"repo_trusted": bool, "sources": [...]}
oceancode hooks add PreToolUse --matcher Bash --command ./scripts/guard.sh [--timeout 10] [--scope user|project|local]
oceancode hooks remove PreToolUse --matcher Bash --command ./scripts/guard.sh [--scope user|project|local]
list ends with HOOKS_OK sources=<n> handlers=<m> loaded=<k>; add prints HOOK_ADDED <path>
and remove prints HOOK_REMOVED <path> (--scope defaults to project; remove of a handler
that is not there exits 1 with hooks remove: no such handler); --dir <d> and --home <h>
select another project or home. The scope names are Claude Code's: user, project and
local are the three files. Note that both project files — .claude/settings.json and
.claude/settings.local.json — are listed at tier repo: they live in the checkout, so they run
only when the project is trusted. (local the scope is Claude Code's name for the git-ignored
project file; local the tier is .oceancode/, your machine's private copy.) Over HTTP a client reads GET /hooks, reloads with
POST /hooks/reload, toggles repo trust with POST /trust/repo {"on": true|false}, adds with
POST /hooks {event, matcher, command, timeout?, scope} and removes with
POST /hooks/remove {path, event, matcher, target} — every one answers the same
{repo_trusted, sources} document.
The decision contract
A handler communicates back two ways: its exit code and a JSON object on stdout.
| Signal | Meaning |
|---|---|
exit 2 |
Deny. On PreToolUse the tool does not run; on Stop it is a deny that does not count toward the Stop cap. |
| any other non-zero, or a spawn / timeout / IO error | No opinion — fail-open, the run proceeds. |
stdout hookSpecificOutput.permissionDecision |
An explicit allow / deny / ask. |
stdout updatedInput |
Rewrites the tool input before it runs. |
stdout additionalContext |
Extra text folded into the model's context. |
stdout stopReason |
On Stop, the reason the run should continue rather than settle. |
Grok Build handlers may also return updatedToolOutput (honoured only for a model-visible copy)
and defer. Everything but an explicit deny fails open: a handler that crashes, times out or
prints garbage never wedges a run.
Only command and http handlers run here, and only the nine events above. A handler of another
type — Claude Code's prompt and agent hooks — or one registered under an event this runner does
not implement (Claude Code's Notification, PreCompact, SubagentStart, …) is skipped and
counted (skipped: <n> unsupported handlers beside the source in /hooks), never fatal: the other
handlers in the same file still load.
Continuing a Stop
A Stop handler that wants the run to keep going returns a stopReason — the engine treats that
as a continuation and drains again. To keep a runaway handler from looping forever, at most 8
Stop continuations are honoured per run; past that the run settles. An exit-2 on Stop is a
plain Deny and does not count against those 8.
Provenance and replay
Every hook outcome is recorded with its provenance — which source, which handler, what it
decided — so a run can be replayed and audited: you can see exactly which hook denied a tool or
continued a turn, and from which tier it came. oceancode inspect lists every hook source with
loaded or held beside it.
See also
- 09 · Capabilities and plugins — discovery, tiers, and the trust gate hooks depend on.
- 11 · MCP servers — the other trust-gated capability.
- 02 · Configuration —
OC_HOOK_EVENTandOC_PROJECT_DIR, set on every command handler. - 19 · Ocean pager —
/hooksin the terminal UI. - 07 · Headless and server —
oceancode hooks listandoceancode trust.
MCP servers
An MCP server is an external process (or HTTP endpoint) that contributes tools, resources and
prompts to a run. oceancode reads the same server-config files the other tools use, so a server
you configured for Claude Code, Cursor or OpenCode is picked up as-is.
Where servers are configured
Servers are read from several files at once and merged by precedence:
| File | Ecosystem | Shape |
|---|---|---|
.mcp.json |
Claude Code (project) | mcpServers { … } |
~/.claude.json |
Claude Code (user) | mcpServers { … } |
.cursor/mcp.json |
Cursor (project) | mcpServers { … } |
opencode.json (mcp {}) |
OpenCode | mcp { … }, local and remote |
<plugin>/.mcp.json |
an installed plugin | mcpServers { … } |
oceancode.json (mcp {}) |
OceanCode | mcp { … } — declared servers win over discovered ones of the same name |
Precedence, highest first: local > project > user > plugin. A server defined closer to you
overrides a same-named server from a wider scope. Every server from every file is folded into
the one manager at boot, so /mcp, GET /mcp and oceancode mcp list all show the same set —
but a name declared at two tiers runs once: the copy that lost its name (qualified
<tier>:<name>, say user:cloudflare-dev for a ~/.claude.json copy of the project's
.mcp.json server) is folded held disabled — listed, never spawned, its tools never offered a
second time under a user_cloudflare-dev_* prefix — and mcp list reads shadowed on it. Only
when the winning copy cannot run (held untrusted or disabled) does the next copy run in its place.
Before 2026-10-13 both copies were started.
Held until trusted
A server that arrived with a checkout (the project's .mcp.json) or with a plugin can run code
on your machine, so it is listed but held — status untrusted — until you trust it. Nothing is
dialed or spawned for a held server. Three ways to release one, all persisted in
~/.oceancode/trust.json under the project's absolute path:
oceancode trust repo on # trust the repo tier: its .mcp.json servers and its hooks
oceancode trust mcp <name> on # trust one server (beats the repo flag); `off` holds it again; `clear` removes the override
oceancode trust show # repo=<bool>, one `mcp <name>=<bool>` per override, one `disabled <name>` per held-disabled server, then TRUST_OK
or trust it from the pager's /mcps tab. A server Claude Code has switched off for this
project (disabledMcpServers / disabledMcpjsonServers in ~/.claude.json, seeded by
oceancode plugin import --from claude-code) is held disabled at every tier and reports
disabled. User-tier servers (~/.claude.json) are trusted by default.
Enabled and trusted are independent: oceancode mcp disable <name> (or the pager's /mcps tab) switches a
server off — disconnecting it and recording the hold in trust.json — without changing its trust,
and enable switches it back on; a server that is enabled but still untrusted stays held and
reports untrusted, and connect refuses to dial it until it is trusted. The engine reads a
trust.json change made from the CLI at its next boot; the pager and the HTTP routes apply it
live.
oceancode mcp enable|disable is the MCP arm of the one enable interface skills, plugins and MCP
servers share (skill enable|disable, plugin enable|disable, and enable-all/disable-all on
each) — see Enabling and disabling skills. MCP
keeps its own disabled_mcp hold model rather than the skills' four-tier fold; the verbs are the
same shape so there is one thing to learn. An explicit enable is remembered per project as
enabled_mcp in ~/.oceancode/trust.json and beats a config entry's enabled: false, so a server
your project config ships off can be switched on from the picker or the CLI and stays on.
Any ${VAR} or ${VAR:-default} in a server's command, args, env or headers is expanded from
your environment when the server starts; an unresolved ${VAR} is left literal rather than
blanked, so a misconfigured variable is visible instead of silently empty.
Like hooks, MCP servers execute on your behalf, so they run only from a trusted source (see 09 · Capabilities and plugins).
Resources, prompts and elicitation
An MCP server offers more than tools:
- Resources — the server's
resources/listandresources/readare surfaced so a server can hand documents into a run. - Prompts — each server prompt appears as a slash command named
/mcp__<server>__<prompt>. - Elicitation — a server that asks the user a question mid-call is routed to the same
questionseam the rest of the app uses, so the prompt shows up in your UI and the answer goes back to the server.
Meta-dispatch for large tool sets
When many servers are connected, the flat tool catalog gets unwieldy for the model — and some
providers reject a tools array longer than 128. Past 128 connected MCP tools the engine always
replaces the flat catalog with two meta-tools — search_tool (find a tool by keyword) and
use_tool (invoke one by id). Setting OC_MCP_META_TOOLS=1 does the same from 64 tools up.
See 02 · Configuration.
Managing servers — oceancode mcp
# A stdio server (a local command):
oceancode mcp add <name> --command <cmd> [--arg <a>]... [--env K=V]...
# A remote server (an HTTP endpoint):
oceancode mcp add <name> --url <url> [--header K=V]...
oceancode mcp list # name stdio|remote tier trusted|untrusted enabled|disabled|shadowed source
oceancode mcp enable <name> # / disable <name> — switch any server off or on (config-declared or discovered; held disabled in ~/.oceancode/trust.json, honoured at boot)
oceancode mcp remove <name>
oceancode mcp login <name> # / logout <name> — OAuth for a server that needs it
oceancode mcp doctor # probe every server and report reachable / failed (a shadowed copy is not dialed; it reads shadowed)
list walks every config file above for the current directory (--dir <d>, --home <h> to
point elsewhere) and shows, per server, its tier, whether it may run and whether it is enabled.
add writes a Claude-Code-shaped .mcp.json, so the server is immediately visible to any tool
that reads that file. Pass exactly one of --command (stdio) or --url (remote). --root <dir>
and --home <dir> override where the config and home are read.
doctor gives each server a short connect budget and reports it reachable or failed quickly,
so one unreachable server does not hang the check.
In Ocean pager, /mcps opens the MCP tab of the capabilities picker (add, enable/disable and
trust go through the engine); see 19 · Ocean pager.
A server whose handshake is still in flight reads connecting (the status list is
connected / connecting / needs-auth / failed / disabled / untrusted); a status read
gives the startup pass at most two seconds and then answers with the live rows (the handshake
itself may take up to 30 s, the timeout default). Disconnecting, disabling or untrusting a
server is bounded to about two seconds even when the server process left a child holding its
output pipe open.
A prompt's first model request waits up to OC_MCP_WAIT_SECONDS (default 5; 0 never waits;
sub-agents never wait) for the servers configured at boot to finish connecting, so their tools are
already in that request. The tool list is then fixed for the rest of the prompt: a server that
connects, or lists new tools (tools/list_changed), while a prompt runs reaches the model on the
next prompt, because a changed tool list at the front of the request would make the provider
re-write its whole prompt cache. Toggling a server, connecting or disconnecting it by hand, trust or
any capability still applies on the very next step.
Over HTTP a client reads GET /mcp (each entry carries trusted, enabled, provenance and
kind beside status), GET /mcp/{name}/tools, GET /mcp/{name}/prompts,
GET /mcp/{name}/resources, and posts POST /mcp/{name}/trust {"on"},
POST /mcp/{name}/enable {"on"}, POST /mcp/{name}/connect|disconnect, POST /mcp/{name}/auth
and DELETE /mcp/{name}/auth.
See also
- 09 · Capabilities and plugins — discovery, tiers, and the trust gate.
- 10 · Hooks — the other capability that runs your code.
- 02 · Configuration —
OC_MCP_META_TOOLSand themcpconfig key. - 07 · Headless and server —
oceancode trustandoceancode mcp list.
Permissions and sandbox
OceanCode decides what a tool may do in three layers, from softest to hardest:
- Permission rules and modes — an ask/allow/deny policy over each tool call.
- The shell and filesystem guards — a classifier that reasons about what a
bashcommand or a file edit actually does, on top of the rules. - The kernel sandbox — OS-level isolation the process cannot talk its way out of, off by default.
An empty config is not an asking one: the base rule is * * allow, so a tool call
is allowed unless something fences it — the shell's dangerous floor, an
external_directory ask, a denied .env read. Secret-named environment variables
are stripped from child processes and .env files are never read into the model.
Add bash * ask under permission to make every shell command off the read-only
safe list ask; that is also the posture the Jev permission advisor works on. This
page is how to loosen or tighten each layer.
Permission modes
A mode is a coarse posture layered over your permission rules. Set the boot
default with permission_mode in the config (see
02 · Configuration), or switch it live over the HTTP API
(see Switching the mode live below).
| Mode | What it does |
|---|---|
(empty) / default |
Evaluate the rules as written; with the base * * allow rule a call no rule fences is allowed. |
acceptEdits |
Auto-allow edit; everything else is unchanged. |
dontAsk |
Never block on an interactive ask (a residual ask resolves to deny in a headless posture). |
bypassPermissions |
Auto-allow everything — except the dangerous floor below, which no mode can reopen. |
The Ocean pager's auto (the Shift+Tab Auto stop, /auto) is acceptEdits and its
always-approve (ctrl+o, /always-approve) is bypassPermissions; the pager sends every
change to the engine (see 19 · Ocean pager).
A mode is expressed as ordinary last-match-wins rules, so it can never silently
override the safety floor: bypassPermissions appends an allow-all rule, but the
dangerous-floor deny is evaluated first and wins regardless.
Jev advisor. With the opt-in Jev decision layer on (see
13 · Jev decisions), a residual ask on a bash command — one
that no allow, deny or specific-pattern ask rule settled — is first put to Jev as
four bounded questions (in act the prompt waits at most jev.deadline_ms, 1.5 s by
default). With the empty config's * * allow base there is no such ask, so the
advisor sees nothing until a rule such as bash * ask makes the shell guard ask.
In shadow, the default once the layer is enabled, the call runs in the background, its
answer is only recorded and you are asked as before, without waiting. With jev.permissions = "act" a command inside a bounded
safe class — not destructive, no network egress, easily undone, a single-file or
project blast radius — runs without the prompt. The dangerous floor and deny rules
run before the advisor and never reach it, dontAsk gets no advice, a command that
names a path outside the project (or runs in a workdir outside it) gets none
either — the shell guard checks that in code — and a fixed hold list (package
installs and publishes, git push, recursive rm, ssh, an uploading curl, an
interpreter run on a file, …) always asks. Under headless --auto, an act advice
that the command is destructive, sends data out, reaches the system or a third
party, or tripped the injection guard rejects the call instead of approving it, and
the rejection carries the reason and the Jev scores back to the model as feedback
(see 13). Your answer to an advised prompt is
recorded as that decision's label.
Rule syntax — Tool(pattern)
Individual permission entries and the shell.allow / shell.deny lists take a
Tool(pattern) form, e.g. Bash(git push:*), Edit(src/**), Read(*.md). The
tool name is matched case-insensitively; an empty pattern means *.
A tool whose last rule for its key is a * deny — bash * deny, or a read-only
agent's blanket deny — is not offered to the model at all: its schema is left out of
the request (write, apply_patch and hashline count as edit). A narrower last
rule keeps the tool, so the plan agent still sees edit for its plan file. Sub-agent
children are sent the built-in tools only, never MCP or skill tools.
Switching the mode live
The permission mode is switched at runtime over the HTTP API —
GET/POST /api/session/{id}/permission/mode (see
07 · Headless and server). A managed
(enterprise) requirements file can lock out bypassPermissions: once locked,
selecting it downgrades to default.
The shell guard
Before any bash command runs, a classifier splits it into segments, peels
wrappers (sudo, env, nice, timeout, xargs, bash -c "…", VAR=val …),
and decides allow / ask / deny per segment. The whole command runs only if
every segment is allowed (a chaining guard — git status && rm x still asks).
- Read-only safe list. Commands that only read (
git status,git log,ls,rg,grep,find, …) auto-approve with no rule. The list looks at flags:findwith-delete,-exec,-execdir,-ok,-okdir,-fprint,-fprint0,-fprintfor-fls;git branchthat deletes, renames, copies, forces or re-points (-d,-D,-m,-M,-c,-C,-f,-u,--delete,--set-upstream-to,--unset-upstream);git remotewithadd,remove,rm,rename,set-url,set-head,set-branches,pruneorupdate; andgit tagthat creates, signs, forces or deletes (a name aftertagcreates a tag unless-l/--listmakes it a pattern) are off the list and ask. Adding output redirection (> file) drops a command off the safe list. - Dangerous floor. A fixed set of destructive shapes is refused outright and
no permission mode can reopen it:
rm -rf /(and obfuscated variants),git push --force,dd of=/dev/*,chmod -R 777 /,mkfs*, the fork bomb, andcurl … | sh. The floor peels wrappers and unwrapsbash -c/command substitutions, so hiding the command behindsudo,/bin/rm, or$(…)does not evade it. shelloverrides.safe_list_extraadds read-only commands;allow/denylayerTool(pattern)rules onto the classifier. Neither can remove the dangerous floor.
Child-process environment
Every bash and PTY child gets a composed environment rather than a raw copy of
the parent's. By default, variables whose name contains KEY, SECRET, or
TOKEN (case-insensitive) are stripped, so a secret in your shell never
leaks into a spawned tool.
Tune it with child_env:
| Field | Meaning |
|---|---|
inherit |
all (default), core (only PATH/HOME/USER/SHELL/LANG/TERM/TMPDIR), or none. |
include_only |
When non-empty, keep only these variable names. |
set |
Extra name→value entries overlaid last. |
strip_secrets |
Drop secret-named variables (default true). |
The sandbox
The sandbox confines a spawned child at the OS level — sandbox-exec (Seatbelt)
on macOS, bubblewrap (with Landlock where the kernel supports it) on Linux ≥
5.13. It is off by default; turn it on with the sandbox config.
| Profile | Reads | Writes | Network |
|---|---|---|---|
off |
unrestricted | unrestricted | unrestricted |
workspace |
workspace + writable roots | workspace + writable roots | per network |
readonly |
workspace + writable roots | denied | per network |
strict |
workspace only | workspace only | per network |
denylists roots refused even inside the workspace (e.g..git, a secrets dir).networkisoff(no egress) orallowlist; with an allow-list, only the exact hosts inallow_hostsare reachable, enforced by a TLS-SNI relay (no wildcards, no suffix matches).
On a Linux kernel older than 5.13, Landlock is unavailable and bubblewrap alone provides the confinement (a warning is logged, never a silent disable).
Reference aliases
refs maps a short @name to a directory. A path of the form @name/rest in any
file tool (read / edit / write) expands to <refs[name]>/rest, and that
reference root is auto-allowed through the external-directory prompt — so a shared
docs or vendor tree can be reached without a prompt for every file. An unknown
@name is left literal and still prompts, and an alias that walks out of its root
(@name/../escape) is caught by the external-directory boundary just like any
other outside path.
Filesystem guards
The file tools carry their own safety, independent of the rules:
.envis never read.readrefuses.envand*.env/*.env.*files (secrets);.env.examplestill reads.- Read-before-write.
writerefuses to overwrite an existing file you have not read this session ("file exists but has not been read this session"); a brand-new file writes freely. One exception (2026-09-28): a file under 64 KiB that this session itself created — its birth time is at or after the session's first tool call in this process, e.g. a file abashcommand generated — can be overwritten without a read. A file that existed before the session is refused even when it was edited since, and on a filesystem that reports no birth time the exception never applies. - Stale detection.
editandwriterefuse a target that changed on disk since it was last read — detected by content hash, so a same-size, same-mtime change is still caught. Read it again first. Other files changing — a build, a file abashcommand wrote, the session's own database — never cost you a read: a read stays valid until the file itself changes (2026-09-28).
Managed (enterprise) config
An administrator can layer a managed config over every project. It is loaded
last, so project < user < managed — managed always wins. Its location is the OS
admin path, or $OC_MANAGED_CONFIG (see
02 · Configuration).
Alongside it, a signed requirements.json carries hard requirements — disabling
bypassPermissions, an MCP-server allow-list, and config pins. It is verified
with ed25519 against the trusted key in $OC_REQUIREMENTS_PUBKEY: an unsigned
or tampered requirements file is refused, never applied. With no key configured,
managed requirements are simply not enforced.
oceancode inspectshows the provenance of every effective config key — which layer (project / user / managed / requirements) set it.oceancode doctorreports crash recovery state: orphaned sessions and captured panic reports. See 07 · Headless and server.
See also
- 02 · Configuration — the config keys and variables.
- 07 · Headless and server —
oceancode doctorand the permission-mode routes.
Jev decisions
OceanCode can put small typed questions to TypeSafe Jev (System One, pinned model
jev-1.13.0) at seven decision points — "would this command delete data?", "which block of this
output answers the step?" — and use the answers as advice. Jev answers with probabilities: a
noul (the probability of yes), a choice over named options, or a score over ordered levels.
The policy that turns an answer into a verdict is code beside each decision point, and the engine's
deterministic backstops stay in front of it: Jev can never turn a deny into anything, never reopen
the dangerous floor, never raise a spend, step or clock bound.
The layer is off by default. It needs your consent in two places, and every point starts in
shadow — decide and record, change nothing — until you promote it to act.
What Jev decides
| Point · mode key | Question asked | What act changes |
What stays deterministic |
|---|---|---|---|
permission · jev.permissions |
For the shell rules' residual ask on a bash command: destructive, egress (any network destination), reversible (score 0–2) and blast_radius (single_file / project / system / external), in one request. Whether the command reaches outside the project is not asked: the shell guard checks it in code (see below). |
A would-ask command runs without a prompt only when destructive < 0.6, egress < 0.5, reversible < 1.5 and the blast is single_file or project with confidence ≥ 0.8. Under headless --auto an ask whose advice is an injection refusal, a system/external blast, destructive ≥ 0.6 or egress ≥ 0.5 is rejected instead of approved, and the model reads why: the rejection reaches it as feedback — Blocked under --auto by the permission policy: <likely destructive | likely network egress | blast system | blast external | the command text addresses the judge>. Jev scores: destructive=<d> egress=<e> blast=<b>. Use a safer equivalent that stays inside the project, or stop and explain why this command is required. (a score Jev did not give prints n/a) — instead of a bare "rejected"; the same reason still goes to stderr. |
Deny rules and the dangerous floor run first and never reach Jev. An ask rule with a specific pattern keeps its prompt; every other permission kind (paths, edit, write, …) asks as before; dontAsk gets no advice. A command over 4,000 chars, a truncated task or a command that names a path outside the project (skip reason a path outside the project; see What is sent) gives no advice. The hold keeps these shapes at ask whatever Jev says: package install/add/publish verbs, npx/bunx/uvx, go run; the git verbs push/remote/restore/clean/stash/checkout -- and every history rewrite (reset in any form, rebase, merge, branch -d/-D, tag -d, update-ref, reflog, worktree remove/prune, commit --amend, filter-branch, filter-repo — plain commit is still advised); a git -c that sets an alias, a program (core.sshCommand, core.pager, core.editor, core.fsmonitor, core.hooksPath, core.gitProxy, sequence.editor), credential.*, http.* or url.*, or any value with !; recursive rm, find -delete/-exec, ssh/scp/rsync, an uploading curl/wget; anything that runs code Jev never sees — a path as the command word (./deploy.sh, scripts/x.sh, /abs/tool), a shell without -c (on a file, -s, or fed from a pipe or <), an interpreter (python, node, ruby, perl, deno, bun, tsx, php) on a file, an inline program or stdin (only python -m <module> and --version pass), source/./eval/exec, awk -f, sed -f; and any opaque or non-ASCII command. |
output_view · jev.output_view |
For a bash output over the truncation limits: which of its ≤ 255 line blocks best answers the step (where, a choice over block ids) and whether any does (exists). The step is the assistant text before the call; when the model chained the call with no text before it, the call's own description stands in (the decision row's state then carries intent_source: "description"). |
The model sees the first 20 and last 20 lines, the blocks Jev picked (most probable first until 0.8 of the mass or 8 blocks, each at most 200 lines) and one [… lines a–b omitted; full output: <spill>] marker per gap; the full output goes to a spill file. When exists < 0.5 it gets the registry's head preview and the spill file instead — except that the top block is still kept when its probability is at least 0.8 and exists is at least 0.2. |
Only bash is viewed (read, grep and glob page themselves). Binary output, a command whose last stage already filters its output (grep, rg, head, tail, wc, sort, uniq, cut, awk, sed, jq; skip reason command already filters), neither step text nor a description of at least 12 characters (skip reasons no step intent, step intent too short), an output the head and tail already cover, a view that is not smaller than the registry's cut it would replace and a state that does not fit 48,000 bytes keep today's result — for bash, as for every tool, the registry's cut: the head preview (at most 2,000 lines / 50 KiB) and the spill file. The head, the tail and the spill file are always there when a view acts. |
supervisor · jev.supervisor |
At a clean depth-0 finish, over the goal, the step count, the open todos, the last six steps and the final text: complete, looping, progress (score 0–2). Every jev.supervisor_every_steps settled depth-0 steps the same three questions are the loop check. |
The stop is blocked with a synthesized continuation when complete < 0.3, progress ≥ 1.0 and the progress mass ≥ 0.5 (at most jev.supervisor_max_continuations per human prompt, default one; a further one is asked about only after a state-changing call since the last); complete ≥ 0.85 always finishes. The loop check sends one steer message — never a stop — when looping ≥ 0.7 and progress < 0.6; in act it is asked only of a ring that looks stuck (a call repeated within the last ten, or the newest three steps with no edit / write / apply_patch / bash call), otherwise a skip row loop check skipped: ring healthy and no call (shadow always asks). |
The hard step, spend and clock bounds, the exact-hash doom-loop trip, the eight-block hook cap and the no_progress guard (08 · Bounding a long run). A stop that asks you something — the final text's last line ends with ?, or the turn used the question tool — is never judged (a skip row, asks the user), and neither is a stop after a tool call the permission policy refused among the last six steps — you rejected it, or a permission rule denied it (a tool call was rejected by policy): the refusal is a decision, so continuing would only make the agent retry it. A stop after the cap, or a further one with no edit / write / apply_patch / bash call since the continuation, is skipped too (continuation cap reached, no state-changing call since the continuation). |
triage · jev.triage |
After a bash exit other than 0 and −1: the failure category (env_missing / flaky_transient / syntax_trivial / expected_failure / deep_logic) and hint_needed. |
With category confidence ≥ 0.8: a flaky_transient failure of an allow-listed test or build runner is re-run once (its output appended after the first run's; metadata.exit keeps the first exit code, metadata.rerun_exit holds the second); flaky_transient elsewhere or env_missing adds one hint line. A failure that names something absent (exit 127, <name>: command not found, No module named '<name>', error[E0463]: can't find crate for, Cannot find module '<name>') is env_missing without a request: the hint names it, points at an alternative that is on PATH (pytest → python3 -m unittest, jest / vitest → node --test, cargo-nextest / nextest → cargo test) and tells the model to use what the project has and not to install anything the task did not ask for; an env_missing answer from Jev itself gets the same wording with "a command" for the name and never says to install; a failing install command (pip install, python3 -m pip install, npm i, cargo install, go get, … in any segment of the command) gets no hint. |
The raw output is never removed; the re-run asks the permission gate again; a green, timed-out or killed run never calls Jev. The runner allow-list is exact: cargo test, cargo nextest run, cargo check, cargo clippy, cargo build, pytest, python -m pytest, python3 -m pytest, go test, npm test, npm run test, pnpm test, yarn test, vitest, jest, ./check.sh, optionally after one cd <relative dir> &&, with no other separator, pipe, redirect or substitution and no leading VAR= other than RUST_LOG, RUST_BACKTRACE, NO_COLOR, CI. |
tool_tiers · jev.tools |
At a session's first human prompt, and again only when a later prompt shifts the topic (see Sticky keep-sets): which optional tools the prompt needs (tools, a choice over every non-core tool with a one-line description) and needs_more_than_core. |
The request advertises the core tools (read, edit, write, bash, grep, glob, todowrite, skill, question, tool_search) plus the top 4 other tools with p ≥ 0.02 — the top 8 when needs_more_than_core ≥ 0.6. tool_search's description names every tool left out; tool_search {name} returns that tool's full definition and pins it from the next human prompt. A topic shift adds its tools to the list; nothing is taken away. |
Every tool stays registered and callable. The list is byte-identical across one prompt's requests. off, shadow, a failure, a catalogue over 255 tools and a run whose --tools list drops tool_search advertise every tool, as today; tool_search is registered only in act. See Tool tiers and provider routes. |
relevance · jev.relevance |
At a session's first human prompt, and again only when a later prompt shifts the topic (see Sticky keep-sets), for 12 or more described skills: which skill fits the prompt (skill, a choice over the skill names with their descriptions) and needs_skill — one request. |
The <available_skills> block lists the top 8 skills with p ≥ 0.02 — at most 3, and never zero, when needs_skill < 0.5 — in rank order, then one line naming every other skill; a topic shift appends its skills after the earlier ones. |
Rules stay deterministic; the skill tool still loads any enabled skill by name; fewer than 12 described skills, off, shadow and a failure list every skill. |
visibility · jev.visibility |
At the ledger drain's prune: which prunable completed-turn tool parts the remaining work still needs (needed, a choice over call ids) and whether it needs any (any_needed), over the goal and the open todos. |
When any_needed ≥ 0.4, up to three parts with p ≥ 0.05 are kept verbatim out of the prune — provided the rest still meets the prune minimum. A part kept once stays kept while the plan moves; a part already stubbed is never offered again. The plan's stubs land on the next cut's request only (a prune rides a cut, 2026-10-12); between cuts no sent row changes. |
The tideline cut, the protect window, the emergency plan and the byte-stable prefix. off, shadow and a failure protect nothing. |
Turning it on
The global config —
$OC_CONFIG, else<config-dir>/oceancode/oceancode.json(or the managed document an administrator ships, which wins over it):{ "jev": { "enabled": true } }The project config —
<project>/.oceancode/oceancode.jsoncor.json. The layer is on only when both sections sayenabled: true, so a repository has to opt in:{ "jev": { "enabled": true } }A project section may set only
enabledand the seven mode keys. Itsendpoint,model, budgets and thresholds are ignored with one warning, so a cloned repository can neither send your states or credentials elsewhere nor loosen a threshold.An endpoint the build can reach. The default build's endpoint is the direct TypeSafe API,
https://api.typesafe.ai/v1/systemone, which needsOC_TYPESAFE_API_KEY; without the key the layer stays off. The platform build (--features platform) defaults to the PlungeAI worker,https://typesafe-provider.ap-a98.workers.dev/systemone?backend=cloudflare, and sends it theocean.headersmap of the global config (the managed document or yours — never the project file's). Either build can use the other endpoint throughjev.endpointin the global config orOC_JEV_ENDPOINT. The key goes only toapi.typesafe.ai,ocean.headersonly to the worker, a redirect is never followed, and plainhttpis refused unlessOC_JEV_ENDPOINTnames a loopback host.The worker refuses a public-internet
POST /systemonethat lacks its shared key (401; 503 until the key is set on the worker), so the global config must carry it — the global file only, never a project file (a project'socean.headersis ignored):{ "ocean": { "headers": { "x-ocean-key": "<the worker key>" } } }Every request also names its decision point in an
x-jev-pointheader (AI Gateway metadata on the worker's side) and the worker's own time comes back inServer-Timing.Check it.
oceancode jev statusshould list the seven points inshadowwith the endpoint host;oceancode jev probemakes one live call and printsJEV_PROBE_OK.Leave everything in
shadowuntil the point has its own labels (see Promoting a point toact).
Modes
Each point has its own key: off, shadow or act.
| Mode | What the point does |
|---|---|
off |
Never calls Jev; today's behaviour, no decision row. |
shadow |
Asks Jev, records the verdict it would have given, and changes nothing. The call runs in the background, so the turn never waits for it; its row is written when the answer lands, and every row is in jev-decisions.jsonl by the time the session closes. |
act |
Asks Jev and applies the verdict inside the bounds of What Jev decides. |
With the layer enabled, a point whose key is unset is in shadow — act is never a default. The
project section's key wins over the global section's. Note that two keys differ from the point
names: permission is jev.permissions and tool tiers is jev.tools. The point names are accepted
as the same keys — jev.permission and jev.tool_tiers mean jev.permissions and jev.tools.
OC_JEV overrides all seven at once: OC_JEV=off turns the layer off whatever the config says,
and shadow or act sets every point. It never turns the layer on by itself — jev.enabled must
still be set in both sections — and an unrecognised value is ignored with a warning.
Promoting a point to act
Promote one point at a time, and only after its per-point label run passes:
- Run the point in
shadowfor at least a week of your own sessions. oceancode jev labelscollects every decision you answered (see Reading decisions) intobench/jev/labels.json.- The point's own run —
node scripts/bench/jev-decisions.mjs --set bench/jev/labels.json --point <p>(live,OC_LIVE_CONFIRM=1) — must printJEV_EVAL_OK point=<p> …: accuracy ≥ 0.90 on the unanimous labels, a Wilson 95 % lower bound on that accuracy (wilson_lower=on the line) ≥ 0.80, at least 40 rows forpermissionand 12 for any other point, and no false allow. A handful of right answers cannot promote a point: n = 12 at 12/12 is 0.758 and never passes, and all-correct needs 16 rows. Short of the bound the run printsJEV_EVAL_RED reason=wilson. - Then set that point's key to
actin the project config.
Point 1 needs the posture it advises on: an empty config allows shell commands (the base rule is
* * allow), so nothing asks and the advisor never runs. Add bash * ask under permission and
every command off the read-only safe list asks — and is advised. The safe list is flag-aware
(find -delete/-exec, git branch -D, git remote add, git tag v1 are off it; see
12 · Permissions and sandbox). The default posture
is not changed by this layer.
Point 5 has one more precondition — the provider route check below. Points 5 and 6 change the advertised tools and skills per prompt, which changes the prompt-cache prefix; measure the cache reads of a real session before promoting either.
Thresholds
Every threshold lives in jev.thresholds, is read from the global section only, and is tuned
against labels for the pinned jev.model (a threshold tuned on one model id is not reused on
another; jev.model must be a versioned id — latest and preview are refused). Probability
thresholds are 0–1; the three score thresholds (permission_reversible, supervisor_progress,
supervisor_loop_progress) are on the three-level scale 0–2.
| Key | Default | The risk it guards |
|---|---|---|
permission_destructive |
0.6 |
Ask when destructive is at least this. |
permission_egress |
0.5 |
Ask when egress is at least this. |
permission_reversible |
1.5 |
Ask when the reversible score is at least this. |
permission_confidence |
0.8 |
Ask when the blast_radius choice is less confident than this. |
output_relevant |
0.5 |
Below this exists, keep no block (head preview + spill instead). |
output_mass |
0.8 |
Keep blocks until their probabilities add up to this (at most 8). |
output_top_p |
0.8 |
Below output_relevant, still keep the top block when its probability is at least this… |
output_exists_floor |
0.2 |
…and exists is at least this. |
supervisor_looping |
0.7 |
Steer only when looping is at least this. |
supervisor_complete |
0.85 |
Always let the stop through when complete is at least this. |
supervisor_continue_complete |
0.3 |
Block a stop only when complete is below this. |
supervisor_progress |
1.0 |
Block a stop only when the progress score is at least this. |
supervisor_progress_confidence |
0.5 |
Block a stop only when the progress mass is at least this (see below). |
supervisor_loop_progress |
0.6 |
Steer only when the progress score is below this. |
triage_confidence |
0.8 |
Pass the failure through when the category choice is less confident than this. |
tools_confidence |
0.6 |
Advertise 8 extra tools instead of 4 when needs_more_than_core is at least this. |
tools_min_p |
0.02 |
An extra tool needs at least this probability. |
relevance_needs |
0.5 |
Below this needs_skill, list at most 3 skills. |
relevance_min_p |
0.02 |
A listed skill needs at least this probability (the top one is always listed). |
visibility_relevant |
0.4 |
Protect nothing below this any_needed. |
visibility_min_p |
0.05 |
A protected part needs at least this probability. |
What the numbers mean in practice, from the recorded answers:
supervisor_progress_confidencereads the progress mass, not the API'sconfidence. It is compared with1 − P("0")— the probability above the lowest ("no progress") rung. The Score answer's ownconfidencemeasures how concentrated the answer is on one rung, so it drops whenever the answer spreads over adjacent rungs: a recorded half-done trace split 0.39 / 0.61 between "incremental" and "substantial" progress, nothing on "no progress", and reported confidence 0.42 — a mass of 1.0.relevance_rerankis gone. A key set in an old config is still accepted (one warning, "no longer read"); nothing reads it. Inact, point 6 used to send a second request, a noul per top-three skill over its body excerpt, and it confirmed no candidate in 7 of 8 recorded prompts while adding 0.18–0.86 s at the start of each. The wide choice's rank order stands.permission_scopeis gone. A key set in an old config is still accepted (one warning, "no longer read"); nothing reads it. Against the task "fix the failing unit test in crates/ocean-llm", Jev scoredcargo nextest run --all-features0.73 andls -la0.57 on the oldoutside_scopequestion (both ask), andcargo nextest run -p ocean-llm0.24 (allow): a question about "the task" punished ordinary project commands, so scope became a code check. Re-recorded without it,cargo nextest run --all-featuresis allow.needs_more_than_coreandneeds_skillrarely change the pick.needs_more_than_corescored 0.09–0.35 on the recorded prompts — the web-research and sub-agent prompts included — so point 5 advertises 4 extra tools, never 8, at the default 0.6; the named tools were still among the 4.needs_skillscored 0.21–0.77 on skill prompts and 0.18–0.30 on coding prompts, but the 0.02 floor already leaves few skills on a skill prompt, so the cap of 3 binds only on coding prompts. Both are tuning items for the label run.
The other jev keys:
| Key | Default | Meaning |
|---|---|---|
enabled |
false |
Needed in both sections. |
endpoint |
the build default | The POST URL (global section only). |
model |
jev-1.13.0 |
A versioned model id; the thresholds are tuned on it. |
deadline_ms |
1500 |
One decision's deadline, retries and the response body included. |
max_rpm |
150 |
HTTP attempts per 60-second window, retries included (per process). |
max_state_chars |
24000 |
The state budget of points 1 and 3–6, in bytes of serialized state. A larger value does not raise the 48,000-byte cap: a state over it is never sent (invalid). |
backoff_seconds |
60 |
How long the client stops calling after the endpoint fails (see Failure behaviour). |
prewarm |
true |
At boot, one GET /health on the endpoint's host opens the TLS connection (HTTP/2 where the server offers it), so the first decision skips the handshake; no credentials are sent and a failure is ignored (global section only). scripts/bench/jev-first-call.sh measures the first-call difference. |
supervisor_every_steps |
4 |
The supervisor's loop check runs once every this many settled depth-0 steps. |
supervisor_max_continuations |
1 |
How many times the supervisor may block a stop with a continuation per human prompt; 0 never blocks. A continuation after the first is asked about only when an edit, write, apply_patch or bash call ran since the last one. |
permissions, output_view, supervisor, triage, tools, relevance, visibility |
shadow once enabled |
The per-point modes (the project section may set them). permission and tool_tiers are accepted for permissions and tools. |
thresholds |
the table above | Global section only. |
What is sent
A state is named fields, never a concatenated prompt, and the questions refer to them by backticked path; nothing from the state is spliced into a question.
| Point | State fields |
|---|---|
permission |
task (your latest prompt, at most 4,000 bytes), tool, arguments.command, and arguments.workdir when the call names a working directory. Never the model-written description of the command. |
output_view |
step_intent (the last 600 chars of the assistant text before the call), tool, output.blocks[] (id, lines, text ≤ 2,000 chars), trimmed to fit 48,000 bytes. |
supervisor |
goal (≤ 1,500 chars), step_count, open_todos, last_steps[≤ 6] (tool, args ≤ 200 chars, result ≤ 300 chars — an error's message for a failed call —, exit for a call that reports one such as bash, and failed: true for a call that ended in an error; both keys only when present), final_text (the last 600 chars). |
triage |
command (≤ 500 chars), exit_code, step_intent, output (head and tail, at most half the budget — 12 KB at the default). |
tool_tiers |
prompt (≤ 2,000 chars). |
relevance |
prompt (≤ 2,000 chars), in one request; skill bodies are never sent. |
visibility |
goal (≤ 1,500 chars), open_todos[≤ 20], parts[≤ 255] (id, tool, args, excerpt ≤ 400 chars of the old tool result), trimmed to fit 48,000 bytes. |
- Sticky keep-sets (points 5 and 6,
actonly). The tool list and the skill block are decided at a session's first prompt and kept. A later prompt that shares vocabulary with the previous one reuses both and sends nothing: it has fewer than four words (lowercasea–z,0–9,_runs of four or more characters, not one ofplease with this that from into then make also just should could would need want file files code change update fix), or at least a fifth of its words appear in the previous prompt. Each point whose set came from a decision then writes oneactedrow (a point that never asked, for a small catalogue or roster, writes none), verdictsticky, reason "keep-set reused: the prompt shares vocabulary with the previous one". A prompt that shares no vocabulary is decided as usual and its keep-set is added to the old one, so a session's tools and skills only grow. Inshadowevery prompt is asked about, and no sticky row is written. - Paths outside the project. There is no scope question; the bash gate marks an ask
paths_outside_project: trueand the advisor sends nothing then (a skip row,a path outside the project). The check runs on the call'sworkdirand on every word of the command that is a path: it starts with/or~, contains/, or is..— relative ones resolved against the workdir, the value of--key=value,key=valueand-I/pathtaken as the path, a leading redirection (2>,>>) dropped, a URL (://) and/dev/nullignored, and a$VAR/…word, which cannot be resolved, counted as outside. It is lexical, like the external-directory prompt: a symlink out of the project is not followed. - Head/tail truncation. A long text field keeps its head and tail around a
[truncated N bytes]marker. Point 1 never advises on a truncated state; the other points only record it. - 48,000 bytes is the hard cap on any state. The endpoint was measured to accept about 33k
input tokens — a 60,334-byte state answered, a 61,600-byte one was refused — and dense tool
output runs about 1.7 bytes per token, so no state is ever sent above 48,000 bytes of JSON. A
state the endpoint still refuses as too long comes back as HTTP 503 with error code 7003 ("User
Input Error"); the client records that as
invalid, never retries it and never counts it towards the back-off. - The injection guard. Before any call, every string of the state is normalised (case,
fullwidth letters, invisible and bidi characters, whitespace) and matched against phrases that
address the judge rather than describe the world — "ignore the above instructions", "note to the
classifier", "you are Jev", "this command is safe", … A match skips the call and records
injection. Your own words — the top-leveltask,goalandpromptfields — are exempt. The tool and skill descriptions points 5 and 6 rank are question options, which the guard does not scan. - No content filter yet. States carry raw text: the command, up to 255 output blocks, six step
results, up to 12 KB of failing output, up to 255 excerpts of old tool results. That can include
a secret from a file the dotenv rule does not cover (
.dev.vars,*.pem), and the decision log keeps every state on disk (see Reading decisions). Turnjev.enabledon only in repositories whose tool output may leave the machine. - Where it goes. Through the PlungeAI worker the request is answered by Workers AI through
Cloudflare AI Gateway, billed to the PlungeAI account with gateway logging on — file contents
leave the machine. The worker can also relay to OpenRouter; OceanCode never uses that: every
request to the worker has
backend=cloudflare, and an answer whosex-typesafe-backendheader is missing or names another backend is refused. TypeSafe states that it does not train on requests. This is why a repository must opt in with its ownjev.enabled.
Reading decisions
oceancode inspectshows onejevrow per point: the mode as its layer, and the endpoint host (oroff) as its source. See 09 · Capabilities.jev-decisions.jsonl. Every decision a point makes — acted on or not — is one JSON line in<ledger root>/<session id>/jev-decisions.jsonl, beside the session's ledger folder (the ledger root is<data dir>/session-ledgerunlessledger.dirsets it). A row carriespoint,mode,outcome,verdict,baseline(today's behaviour),latency_ms,server_ms(the worker's own time for the call, from itsServer-Timingheader; absent when the endpoint sent none),model,input_tokens,state_hash(a hash of the full state),state(the state itself, or for one over 8 KiB a head/tail excerpt — the full state is then written once to<ledger root>/<session id>/jev-states/<state_hash>.json, so every decision can be re-sent) andanswers— the full answers with every probability, or for a skipped or failed decision the reason as a string. It never carries the key, the endpoint or a header value, and a URL in an error message is replaced by<url>. Writing is best effort: a full disk never fails a decision (the first failure is logged once).- Outcomes.
acted(the verdict changed something),shadow(recorded only),fallback(the decision kept today's behaviour: a low-confidence answer, a skip such as a command over 4,000 chars, or an answer that could not be applied),held(point 1's deterministic hold kept an allow answer at ask),unavailable(deadline, transport, HTTP error, rate cap or back-off window),invalid(an answer missing a key, of the wrong type or with bad probabilities; a state over its budget; the 7003 refusal),injection(the guard skipped the call) andoff(the decider behind the point was off). - The triage re-run note. When point 4 re-runs a command, its decision row is written first,
with the state Jev was asked about, and a second row follows with outcome
actedand the reasonrerun exit=<code>orrerun refused: <why>as itsanswersstring. Only rows whoseanswersis an object are decisions; count and label those. A missing-binary or missing-module failure (theenv_missingsignals in the point 4 row above) is written as one row with the reasondeterministic env_missingas itsanswersstring and no request:acted/hintonce the hint was appended,fallback/passfor a failing install command,shadow/hintinshadow. The hint line is "jev triage:<name>is not available (exit); use what the project already has —<alternative>is available and do not install anything the task did not ask for" (the alternative clause only when one is onPATH; "a command" in place of the name when only the exit code said so, and for anenv_missinganswer Jev gave itself). - Labels. When you answer a permission prompt that Jev advised on, a label row is appended to
the same file —
allowfor once or always,denyfor reject — and joined to its decision. Only a person's answer is a label: the replies headlessoceancode rungives by itself (--auto's once, a Jev-advised reject, the reject without--auto) and the Ocean pager's own answers (its YOLO auto-approve, the queue it approves when you switch YOLO on, a cancelled or failed prompt) write none.oceancode jev labelsexports every labelled decision. - Labelling by hand. Points 2–7 have no prompt to answer, so their rows come unlabelled.
oceancode jev labels --unlabelled --out to-label.json(optionally--point <p>) writes them with"label": nulland"source": "unlabelled". An annotator fills eachlabelin the grammar ofbench/jev/README.md, setssourcetoannotator-<n>(a second annotator addssecond_label), and the rows are appended tobench/jev/labels.jsonby hand, for examplejq -s add bench/jev/labels.json to-label.json > merged.json && mv merged.json bench/jev/labels.json.oceancode jev labelskeeps every row already in that file and adds only decisions whose point and state hash it does not carry, so a later export never duplicates an annotated row. An unlabelled row may carry"evidence": {"spill_grepped": true}: its turn latergrepped orreada file undertool-output/, so the full output was wanted after all. It is a hint for you, never a label. - Implicit labels.
oceancode jev labels --implicit --out bench/jev/labels.jsonlabels the rows nobody labelled from what the agent did next in the session's transcript: the firstskillit loaded after a relevance pick (skill:<name>, sourceimplicit-skill-load), the tools it fetched withtool_searchafter a tier pick (wide;extra:<tool>ornarrow;extra:<tool>,implicit-tool-search), and whether it made anedit/write/apply_patch/bashcall after a supervisor continuation (continue, elsefinish,implicit-continuation). Rows follow the same merge as any export. These are the agent's behaviour, not your verdict, so spot-check them before a run counts on them; the rules are inbench/jev/README.md.
The oceancode jev subcommand
Every verb reads the same two sections and environment that boot reads, from the current
directory. Each ends with its sentinel on the last stdout line — except the two --json verbs
and verdicts, which keep stdout to JSON and print one JEV_STATUS_EMITTED points=7,
JEV_QUESTIONS_EMITTED points=7 or JEV_VERDICTS_EMITTED lines=<n> line on stderr, as
oceancode inspect --json does. A failure exits 1.
| Command | What it does |
|---|---|
oceancode jev status [--json] |
One point mode endpoint-host line per point (- when no client is configured), then JEV_STATUS_OK points=7. --json prints {points, endpoint, thresholds} — the modes and the resolved thresholds the bench lanes read before they spend. Makes no call. |
oceancode jev probe |
Live, one call. Asks one fixed destructive noul about git status --short at the first point that is not off; prints model=… latency_ms=… input_tokens=… destructive=… and JEV_PROBE_OK, or JEV_PROBE_FAIL <error> and exits 1. With every point off it refuses without a call, naming why — JEV_PROBE_FAIL jev is off: <reason>, where the reason is the one that turned the layer off: a missing or disabled section, OC_JEV=off, OC_TYPESAFE_API_KEY absent for the direct API, a refused plain-http endpoint, an endpoint that is not a URL, or every point's mode set to off. |
oceancode jev probe --record --point <p> --case <id> --request <req.json> --out <fixture.json> [--expect <json>] |
Live, one call. The alias of scripts/fixtures/jev-record.mjs: sends the request file's {state, questions} through the configured decider — its credential scoping, back-off and backend pin — and merges {state, questions, response, expect} into cases.<id> of the fixture, keeping every other case. Prints JEV_RECORD_OK point=<p> case=<id> cases=<n>, or JEV_RECORD_FAIL reason=<reason> <message> and exits 1 (a fixture that holds another point, or does not parse, is refused and left as it is). |
oceancode jev labels [--root <ledger dir>] [--out <file>] |
Every decision row with a label, from every <root>/<session>/jev-decisions.jsonl (default root: the configured ledger.dir, else <data dir>/session-ledger — where boot writes the log), appended to --out (default bench/jev/labels.json). The file's rows are kept exactly as read — their order, their own duplicates and any key you added by hand; a decision is added only when the file does not yet carry its point and state hash. A state over 8 KiB is exported whole from its jev-states/ file. Prints JEV_LABELS_OK added=<n> total=<m>; a label file that does not parse is refused (JEV_LABELS_FAIL) and left as it is. |
oceancode jev labels --implicit [--root <ledger dir>] [--out <file>] |
The same merge for labels read off the session transcripts (<root>/<session>/transcript/): a relevance row gets the skill the turn loaded, a tool-tier row the tools it fetched with tool_search, an acted supervisor continue row continue or finish by whether a mutating call followed (see Reading decisions). Rows a person labelled are left to labels. Prints JEV_LABELS_OK added=<n> total=<m> implicit=<k>, k being the rows it derived. |
oceancode jev labels --unlabelled [--point <p>] [--root <ledger dir>] --out <file> |
The same export for the decision rows nobody labelled ("label": null, "source": "unlabelled"), for annotators (see Labelling by hand), each with an evidence object where the transcript shows what the agent did next. --out is required, so unlabelled rows never land in the label set by default. |
oceancode jev questions --json |
Each point's question set from the seams' own builders, in the order the seams send them. A choice whose options come from the request is empty and names its source in options_from (state.output.blocks[].id, state.parts[].id, roster); the tool-tier catalogue is the built-in tools'. The bench generates its question file from this. |
oceancode jev verdicts --in <cases.jsonl> |
For each input line {point, state, response}, one output line {point, verdict} from that point's own policy at the global section's thresholds: permission "allow" / "ask" (the hold included; its line also carries held, true when the hold fixes the verdict at ask — the bench leaves such a row out of accuracy), output view {keep}, supervisor {stop, steer}, triage "pass" / "rerun" / "hint", tool tiers {wide, extra} (null when the answer is incomplete), relevance {keep}, visibility {protect}. Makes no call. |
Money. probe and probe --record are the only verbs that call Jev. So does every decision
in shadow or act: at the rate-card price of jev-1.13.0 ($0.042 per million input tokens,
output free) a permission question is about 550 input tokens and the largest output-view or
visibility state about 30k, so a long session costs cents. Through the PlungeAI worker the calls
are billed to the PlungeAI account. The live bench lanes refuse to run without OC_LIVE_CONFIRM=1.
Tool tiers and provider routes
With jev.tools = act a prompt's request advertises only the core tools, the tools Jev picked and
tool_search. A tool the model learns through tool_search is called in the same prompt,
before the next human prompt advertises it — every tool stays registered, so the engine runs the
call. Pins survive a reused keep-set: a tool tool_search returned is added to the sticky list
at the next prompt, exactly as it is added to a freshly decided one. A provider route that refuses
a call to a tool absent from that request's tool list must keep jev.tools = off.
No route has been checked yet: the check spends frontier-model money and is run by the owner, once
per wired route, before point 5 is promoted. With jev.tools = "act" in the project config (and,
in a default build, OC_JEV_ENDPOINT set to the worker URL above unless you hold a TypeSafe key),
confirm oceancode jev status --json reports tool_tiers as act — otherwise the check runs
with Jev off and proves nothing — then run
oceancode run -m <provider/model> "Use tool_search to look up webfetch, then fetch https://example.com and print its title".
The route passes when the run calls tool_search, then webfetch, and finishes without a provider
4xx; a route that fails is listed here as one that keeps jev.tools = off.
Environment variables
| Variable | Meaning | Default |
|---|---|---|
OC_JEV |
off, shadow or act: overrides every Jev decision point's mode (off turns the layer off whatever the config says). It never turns the layer on by itself — jev.enabled must still be set in both the global and the project config. |
unset → the config |
OC_JEV_ENDPOINT |
Overrides jev.endpoint (the Jev POST URL). The only way to use plain http, and only to a loopback host (127.0.0.1, localhost, ::1). |
unset → jev.endpoint, else the build default |
OC_TYPESAFE_API_KEY |
The TypeSafe API key, sent only to api.typesafe.ai (never to another host); never in config; stripped from child processes like every *KEY* variable. The direct API without it keeps the layer off. |
unset |
The same rows are in 02 · Configuration, with every other variable.
Failure behaviour
A Jev failure is never a turn failure: the point falls back to today's behaviour and writes one
row. A shadow call runs in the background (see Modes), so its row — an answer or a
failure — is written when the call ends, under the id it was given when it started; closing the
session waits for the calls still open, at most 2.5 seconds, and a process that is killed
before then loses their rows.
| Failure | Behaviour |
|---|---|
| Endpoint unreachable, DNS, TLS | Fallback; unavailable. Counts toward the back-off: the second consecutive deadline or transport failure opens the jev.backoff_seconds window, during which no point calls. |
Deadline exceeded (jev.deadline_ms, the response body included) |
Fallback; unavailable; the request is dropped, the turn goes on. One deadline alone never opens the back-off window; a second in a row does. The deadline of a large output-view or visibility state (points 2 and 7, up to 48,000 bytes) is never counted toward it, so a slow large state cannot silence the other points. |
| HTTP 401 / 403 | Fallback; one warning naming the credential source (OC_TYPESAFE_API_KEY or ocean.headers); no further requests until restart. |
| HTTP 422 | Fallback; the row carries the server's message. |
| HTTP 408 / 429 / 5xx | Up to 2 retries, 500 ms → 5 s with 25 % jitter, honouring retry-after up to 60 s, and only when the delay fits what is left of the deadline; then fallback, and a 429 or 5xx that still fails opens the back-off window at once. |
| HTTP 503 with error code 7003 (state too long) | Fallback; invalid; no retry and no back-off. |
| A redirect | Never followed (the headers would follow it off the worker); fallback as an HTTP error. |
Client rate cap (jev.max_rpm HTTP attempts per 60-second window, retries included, per process) |
The decision is skipped this time; unavailable. The cap does not bound the shared gateway, whose own 429 and the back-off window are the real protection. |
| Answer missing a key, of the wrong type, a choice outside its probabilities, or probabilities outside 0–1 or not summing to 1 ± 0.02 | Fallback; invalid. |
A 200 from the worker whose x-typesafe-backend header is missing or not cloudflare |
Fallback; invalid; one warning; no further requests until restart. |
| State over its budget | Each field is cut head/tail and output blocks are trimmed to fit (24,000 bytes by default for points 1 and 3–6, 48,000 for points 2 and 7, never above 48,000); a state that still does not fit is not sent (invalid). Point 1 never advises on a truncated command. |
| The injection guard matches | No call; injection. Point 1 keeps the ask, and under --auto in act rejects it. |
A served model other than jev.model |
Answered as usual; one warning per process (the thresholds were tuned on the configured id). |
See also
- 02 · Configuration — the
jevkey and the environment variables. - 07 · Headless and server —
oceancode jevamong the management subcommands. - 08 · Bounding a long run — the bounds the supervisor never changes.
- 12 · Permissions and sandbox — the rules and the floor that run before the permission advisor.
Platform mode (PlungeAI connectivity)
oceancode ships in two builds. The default build is the standalone coding agent and contains
no platform code at all. The platform build is produced with
cargo build --release -p ocean-cli --features platform
and adds the three PlungeAI couplings: the OceanTunnel daemon (crates/ocean-app/src/oceantunnel.rs),
the enrich resolver (crates/ocean-agent/src/systemcontext/remote.rs) and the per-step remote
capability packs (crates/ocean-agent/src/systemcontext/capability.rs). Everything the ocean
config section describes below is inert in the default build.
The platform cargo feature is declared on three crates — ocean-agent, ocean-app and
ocean-cli — and is off by default. The OceanInfo schema itself (every ocean.* key)
stays in the leaf crate ocean-config in both builds, so an oceancode.json with an ocean
section parses either way; only the code that acts on it is gated.
Which build do I have?
node scripts/gates/check-platform-boundary.mjs --no-build scans the two release binaries for three
byte-literal markers after a release build and reports markers_local=0 markers_platform=3 when the
boundary holds; a routine ./check.sh proves the same boundary on debug builds (--debug, which
builds target/debug/oceancode under both feature sets and scans between the builds — no release
build needed). At runtime, oceancode inspect reports a platform row: compiled in on a platform
build, not compiled in on the default build.
The "platform: not compiled in" notice
A default build that finds ocean.mode = "platform" or a non-empty ocean.endpoint in
oceancode.json prints once at boot and in oceancode inspect:
platform: not compiled in (ocean.mode or ocean.endpoint is set; rebuild with --features platform)
It is a single warning, not an error: the agent still runs, exactly as it does for any user with no
ocean section. Rebuild with --features platform to make the section take effect. A config without
an ocean section prints nothing.
Config keys
The ocean.* keys are serde-only and present in both builds; they take effect only in the platform
build (crates/ocean-config/src/ocean.rs:15-88).
| Key | Meaning | Default |
|---|---|---|
mode |
local or platform. The only switch that selects platform behaviour. |
local |
endpoint |
Enrich URL (GET ?agent=<name>&directory=<dir> → {baseline, version}). |
empty |
headers |
Headers sent on every platform request (auth lives here). | none |
ttl_seconds |
How often a session re-checks the enrich endpoint (0 → 300). | 300 |
agents |
Platform-resolved agent names; empty defaults to ocean-build, ocean-plan. |
ocean-build, ocean-plan |
mcp_url |
Auto-registers the platform MCP gateway as a remote MCP server named ocean. |
empty |
capability_endpoint |
Per-step capability pack endpoint. | empty |
gateway_base_url |
The inference gateway a provider baseURL should point at: the bare host or its /v1 root, both work. |
https://api.plungeai.com/v1 in platform mode |
tunnel_url |
OceanTunnel dial-out URL; with a token, runs the tunnel daemon. | empty |
tunnel_token |
Inline OceanTunnel session token. | empty |
tunnel_token_file |
File holding the tunnel token (~/ expands against $HOME). |
empty |
tunnel_agent |
Local agent name registered with the tunnel. | oceancode |
The switch is config-only. No environment variable selects platform mode; ocean.mode in
oceancode.json is the only switch — there is no OCEANCODE_MODE / OC_MODE. Two platform-related
environment variables the binary reads never turn platform mode on by themselves:
OC_TUNNEL_TOKEN— the tunnel session token, read at the highest priority (above the deprecatedOCEAN_TUNNEL_TOKEN, thentunnel_token_file, then the inlinetunnel_token).OCEAN_TUNNEL_TOKENis still read as a legacy fallback and logs one deprecation warning per process.OC_SSO_TOKEN— a per-request gateway SSO session token. When set, oceancode attaches it as theX-Ocean-SSO-Tokenheader (noBearerprefix) on requests to the configured https gateway host only — never a direct-provider host, never over plaintext — and never onAuthorization(the wallet bearer, yourozk_key, stays intact). Read at runtime, never persisted. Server-side reader is a pending platform handoff: until the inference gateway readsX-Ocean-SSO-Token, the header is ignored (harmless).
Every variable also has a row in 02 · Configuration.
Per-step capabilities
In platform mode the local <available_skills> catalog is replaced by a capability pack sized to
the plan row the model is working. At each step boundary the resolver asks the capability endpoint for
a pack (eager skill bodies within a 24 KiB budget, a deferred index, and a version); 304 when the
pack is unchanged, so the retained prefix — and the provider's prompt cache — never changes because a
skill changed. Deferred bodies cost nothing until pulled:
load_capabilitytool — a platform-build tool that pulls a deferred capability body by id (load_capability {id}) into the transcript as a normal tool result, never a file on disk. It is registered only in the platform build and refuses any id the platform never offered.capabilitypermission — the permission family that governsload_capability. It asks by default in the read-only agent families (ask,explore,security-review) and is allowed for thebuild,planandgeneralfamilies.- Registry skills — platform mode can also discover skills straight from the registry with no local
files; they surface with the provenance
platform:<id>(tierplatform, bound tool idskill_platform_<id>) and load with no base-directory line.
Model routing and the wallet
Signing in. On a platform build, oceancode login --provider ocean (alias plungeai) signs you in
through the browser and stores your account key. It opens the dashboard's
https://dashboard.plungeai.com/api/cli/authorize page with a loopback port, you sign in with any method
the dashboard offers, the dashboard hands a one-time code back to http://127.0.0.1:<port>/callback, and
oceancode trades that code for your ozk_ account key and stores it in auth.json under ocean — the
entry oceancode key ocean writes and platform mode uses first. The key never appears in a browser URL.
--no-browser prints the link without opening it; the sign-in gives up after three minutes;
oceancode logout ocean removes the key.
With an ozk_ platform key stored — oceancode key ocean ozk_… (it lands in auth.json), or a credential
under the ocean integration — platform mode synthesizes an ocean provider whose baseURL is the
gateway's /v1 root (https://api.plungeai.com/v1 unless gateway_base_url says otherwise), so every model
call routes through the inference gateway. A legacy sk-ocean- key is found the same way; when both are
stored the ozk_ key wins. A model of auto is resolved once per session from the gateway's
/v1/discovery/recommend (falling back to anthropic/claude-sonnet-4-5 if the recommend call fails).
The run --format json result envelope then carries two extra objects for a platform-routed run:
platform: { fallback_steps } (how many steps failed open to the local route) and
wallet: { balance, spent } (present only when the gateway meters). A local run omits both.
The ledger in platform mode
Two engine-owned sections in SESSION.md are fed by platform-mode durable events:
## Plan changes by user— regenerated fromtodo.updatedevents, one summary line per event that carried user-authored plan rows (the model's own edits contribute nothing).## Capabilities used— regenerated frompack.manifestevents, one| step | ids | version | hash |row per step; a fallback step lists its row with an empty id cell.
Both are bounded to the newest 20 rows and regenerated on every write, so they never crowd out model-owned sections. Plan rows also carry cross-session history: when a row that completed or was cancelled in an earlier session reappears, its projection shows how often it was seen and how often it failed.
Testing and gates
Everything in oceancode is verified by one script — ./check.sh — and a handful of opt-in
lanes on it. This page covers what those lanes are, and then the one test surface a terminal
UI needs that unit tests cannot give it: the interface probe, which drives the real TUI
over a pseudo-terminal and proves the interface never freezes.
The lanes
export PATH="$HOME/.cargo/bin:$PATH"
./check.sh # fmt · clippy -D warnings · build · nextest · goldens · doc + gate checks
E2E=1 ./check.sh # + real-engine PTY tests, chaos, crash-recovery drills, the interface probe
PERF=1 ./check.sh # + divan benches and the budget gates
COVERAGE=1 ./check.sh # + llvm-cov with the zero-covered-handler tripwire
MUTANTS=1 ./check.sh # + cargo-mutants over the risk-ranked modules
The default lane dials no model provider and touches no network. Every checker prints a
sentinel as its last line and exits non-zero on failure; the full roster is in
scripts/README.md (apps/ocean-coding-agent-rust/scripts/README.md). No lane spends money — the only live-provider
scripts (scripts/probes/live-*.sh) are run by hand, never by check.sh.
The interface probe
A TUI can pass every unit test and still lock up the instant a slash command awaits the whole
turn on the event loop — the exact freeze that shipped and was fixed on 2026-09-15. Unit tests
run the update function in-process; they cannot catch a real event loop that has stopped
turning keystrokes into frames. The interface probe can, because it is black-box: it spawns the
real oceancode binary behind a pseudo-terminal, renders its screen with pyte, feeds it real
keys, and asserts on what a person would see.
It costs nothing to run. The binary is wired to a fake Anthropic provider with a fake key,
an isolated HOME, and a loopback --listen port; the owner's real provider keys never enter a
probe run.
bash scripts/probe/run-probe.sh # every scenario
bash scripts/probe/run-probe.sh -k slash # only scenarios whose id (or file) contains "slash"
bash scripts/probe/run-probe.sh -k baseline # one file's scenarios (-k matches the module name too)
bash scripts/probe/run-probe.sh -j 4 # run 4 at a time, each on its own ports
bash scripts/probe/run-probe.sh --list # module:id of every discovered scenario
bash scripts/probe/run-probe.sh --keep # keep each scenario's temp HOME to debug a failure
The last line is the sentinel:
INTERFACE_PROBE_OK scenarios=10 passed=10 xfail=0 # exit 0
INTERFACE_PROBE_FAIL scenarios=10 failed=1 mcp_roundtrip # exit 1
run-probe.sh bootstraps its own Python venv at target/probe-venv (pexpect + pyte,
idempotent and offline-tolerant), resolves the binary from $OCEANCODE_BIN — building
target/debug/oceancode with cargo build -p ocean-cli when it is unset — and runs the
harness. It is wired into the E2E=1 lane after the crash-recovery drills, so a freeze is a
red build.
The liveness oracle
Every scenario ends on the same universal check, t.assert_live(), which the harness calls for
you after the scenario body returns:
type a unique nonce into the composer, assert the composer echoes it within 1.5 s, then erase it.
A frozen event loop cannot echo a keystroke, so that one assertion is the proof the interface is still alive — no matter what the scenario did before it. Because of this, a scenario only has to assert its own behavior and leave the composer on screen; the liveness check is free.
Driving the TUI interactively (for agents)
The same probe also runs as a persistent session, so an agent can drive the app step by step
from separate Bash calls — open it, look, type, look again — instead of only inside a batch
scenario. scripts/probe/session.py holds one PTY and pyte screen in a daemon; a client speaks
to it over a unix socket:
session.py open <name> --app oceancode|pager|cli|cmd [--cols 120 --rows 40] [--keep] [-- cmd args…]
session.py <name> screen | styled <row> | history | send <text> | key <k> [k…] | paste <text>
session.py <name> wait <needle> [timeout] | wait_re <regex> [timeout] | resize <c> <r> | alive | requests | evidence <file> | close
oceancode / pager boot the real binary on the fake provider in a fresh HOME (the scenarios'
isolation, but colours kept so styled can read them); cli runs the cli/ REPL against deployed
Studio. Wait for the app's ready marker before driving — ❯ for the pager (oceancode boots
Ocean pager too since the native TUI was removed on 2026-10-05), Ask anything for the REPL — and wait for a marker, never sleep
(wait polls the screen every 150 ms).
S="target/probe-venv/bin/python scripts/probe/session.py"
$S open pager --app pager # fresh HOME, fake provider, PTY 120×40
$S pager wait '❯' 40 # block on the ready marker
$S pager send 'hello'; $S pager key enter
$S pager wait 'ok: hello' 40
$S pager styled 38 # one row as (fg, bg, bold) runs — colour/bold asserts
$S pager close # kill app + fixture, remove the temp HOME
End every interactive check the way a scenario does: type a unique nonce and confirm it echoes in
the composer — the liveness oracle, by hand. bash scripts/probe/session-selftest.sh drives all
three surfaces from separate processes and prints PROBE_SESSION_OK apps=<n> as its last line;
--no-cli runs the hermetic surfaces only (the pager), which is how the
E2E=1 lane wires it in.
Adding a scenario
Drop a function named scenario_<name>(t, fx) into any file under scripts/probe/scenarios/.
New files and functions are discovered automatically — you never register anything. t is a
live Tui, fx a Fixture on the fake provider. Raise AssertionError (or call t.fail(msg))
to fail; a screen excerpt is attached automatically.
# scripts/probe/scenarios/my_feature.py
def scenario_models_open_and_esc(t, fx):
"""/models opens the model picker and Esc returns to a live composer."""
t.send("/models")
t.key("enter", 0.6)
if "MODEL" not in t.text():
t.fail("/models did not open the model picker")
t.key("esc", 0.4)
if t.PLACEHOLDER not in t.text():
t.fail("Esc did not return to the composer")
# the harness calls t.assert_live() after this returns
Driving the screen (t):
| Call | What it does |
|---|---|
t.send(text) |
type text into whatever has focus |
t.key(name) |
one key: enter, esc, tab, backtab, backspace, up/down/left/right, home/end, pgup/pgdn, ctrl-c, ctrl-d, ctrl-u, shift-enter, f1–f12 |
t.paste(text) |
bracketed paste |
t.resize(cols, rows) |
resize the terminal (delivers SIGWINCH) |
t.wait_for(needle, timeout) |
wait until needle (a substring or a compiled regex) is on screen; returns True/False |
t.text() / t.line(n) |
the whole screen / one row, as text |
t.alive() / t.pid |
is the process still running / its pid |
t.snapshot(label) |
record a labelled screen for later |
t.fail(msg) |
raise with a screen excerpt attached |
t.assert_live() |
the liveness oracle (also usable mid-scenario) |
t.assert_version_bar() |
assert a vX.Y.Z build stamp is in the status bar |
t.PLACEHOLDER |
the composer's empty-prompt placeholder text |
Steering the provider (fx): the fake provider picks its reply from MAGIC WORDS in the last
user message — put one in the prompt text your scenario sends:
| Word | Reply |
|---|---|
| (none) | one text block ok: <first 40 chars of the prompt> |
SLOW |
the same text paced over 30 deltas (a visibly streaming turn) |
ERR500 |
HTTP 500 with an Anthropic error body |
HANG |
an open stream that never answers (abort it from the TUI) |
TOOLUSE |
a tool_use for the bash tool on the first turn (the permission dialog appears), text on the follow-up |
WORKFLOW |
a tool_use for the workflow tool launching the built-in two-step pipeline (its sub-agents get default replies and the run completes) — the pager's Workflows-dock regression guard |
GOAL |
a tool_use launching the built-in goal supervisor; its planner/skeptic output_schema requests are answered with valid JSON so the run converges (Plan → Work → Verify) |
MD |
a markdown-heavy reply (heading, table, fenced code, wide CJK + emoji) |
Read what the binary actually sent with fx.requests(), fx.last_request(), fx.user_texts()
(the last-user text of every request), and fx.base_url / fx.port.
Each scenario is fully isolated: its own HOME, its own fixture project directory carrying its
own .claude/commands/{long,short}.md, its own two free ports, and its own fresh fixture
process. Scenarios never share state, so -j N runs them in parallel safely.
xfail policy
A scenario for a known, tracked defect is decorated @xfail("REASON-ID"):
from scenarios.xfail import xfail
@xfail("OCA-1234-slash-refusal-toast")
def scenario_unknown_slash_is_refused_visibly(t, fx):
...
It is reported as XFAIL <REASON-ID> and does not fail the run; a scenario that is
xfail-marked but unexpectedly passes is reported XPASS and counts as a pass. Use @xfail
only for a defect that has a tracking id — never to silence a flaky or half-finished scenario.
The moment the defect is fixed, remove the decorator so a regression turns the scenario red
again.
Distribution and updates
oceancode ships as two self-contained binaries: the engine oceancode and the terminal UI
ocean-pager it launches, installed side by side. This page covers installing it, keeping it
current, verifying a download, and removing it. Every download is checksummed, and every install
channel agrees on one set of release-asset names.
Installing
| Channel | Command |
|---|---|
| macOS / Linux (script) | curl -fsSL https://raw.githubusercontent.com/OWNER/ocean-coding-agent/main/packaging/install/install.sh | sh |
| Windows (PowerShell) | irm https://raw.githubusercontent.com/OWNER/ocean-coding-agent/main/packaging/install/install.ps1 | iex |
| Homebrew (macOS / Linux) | brew install OWNER/oceancode/oceancode |
| Cargo (Rust-native, npm-free) | cargo install --locked --path crates/ocean-cli (from a clone today; cargo install --locked oceancode once published) |
The script installer downloads the release asset for your platform, verifies it (see
Verifying a download), and installs oceancode and ocean-pager into
${OC_INSTALL_DIR:-$HOME/.local/bin} (Windows %USERPROFILE%\.oceancode\bin). If that directory
is not on your PATH, the installer prints the line to add. See
02 · Configuration for OC_INSTALL_DIR and the other install knobs.
cargo install is npm-free by design — the whole toolchain is Rust; no Node package is pulled at
build or install time. See packaging/homebrew/cargo-install.md.
Keeping current — oceancode update
oceancode update # update to the newest release for your channel
oceancode update --self-test # prove the swap + rollback offline (no network)
update fetches the newest release for the configured channel, never downgrades, verifies the
release signatures, swaps oceancode and ocean-pager atomically, and rolls both back if either
new binary fails a health check — so a failed update leaves the working pair exactly as it was.
--self-test prints UPDATE_SELF_TEST_OK binaries=2 as its last line.
Channels and opt-out:
| Setting | Where | Effect |
|---|---|---|
update.channel |
config (oceancode.json) |
stable (default) or beta (sees pre-releases). |
OC_UPDATE_CHANNEL |
env | Overrides update.channel for one run. |
update.auto_check |
config | true (default) checks for a newer release on boot. |
OC_NO_UPDATE_CHECK |
env | Any value suppresses the boot-time check. |
update.endpoint |
config | Overrides the release-metadata source (default: GitHub Releases). |
Telemetry-free first run. The boot-time update check is always skipped on the very first run
(and whenever update.auto_check is false or OC_NO_UPDATE_CHECK is set): a fresh install never
phones home before you ask it to.
Verifying a download
Every release ships a single SHA256SUMS manifest and a detached minisign signature
(<asset>.minisig) per asset. Verify a download before trusting it — the same check on macOS,
Linux and Windows, with no external tool:
oceancode verify-release oceancode-macos-aarch64.tar.gz \
--sha256sums SHA256SUMS
It prints VERIFY_RELEASE_OK file=<name> on success and refuses (non-zero exit) on any mismatch.
--pubkey <base64> overrides the embedded release public key.
Signing tiers. Releases are signed with minisign on every platform. When an Apple Developer ID
(macOS) or an Authenticode certificate (Windows) is available, the release also carries OS-native
codesigning / notarization. When no such certificate is present, the release ships unsigned at the
OS level but with SHA256SUMS + minisign — the portable trust root above still applies, and the
installer stays fail-closed (it refuses to install an unverifiable artifact unless you pass
--insecure-skip-verify / OCEANCODE_INSECURE_SKIP_VERIFY=1).
Test key notice. The public key embedded in
oceancode verify-release(verify_release::EMBEDDED_PUBKEY) and in the installers is a deterministic test key. The release owner must replace it with the real minisign public key before shipping a signed release.
Uninstalling
oceancode uninstall # remove the binary only
oceancode uninstall --purge # also remove ~/.config/oceancode and the data dir
oceancode uninstall --dry-run # print the plan, remove nothing
uninstall removes only what the installer placed. --purge additionally removes the app's config
and data directories (the data dir holds auth.json, TUI state and the default global session
store) — never your per-project .oceancode/oceancode.db. It requires a typed oceancode
confirmation unless --yes, never follows a symlink out of its own tree, and prints
UNINSTALL_OK removed=<n>.
Windows notes
On Windows the bash tool spawns the shell selected by the shell.kind config key (default
auto → PowerShell). Tool output is normalized for cross-OS stability: \r\n and stray \r
become \n, and backslash paths are shown POSIX-style (C:/Users/...) for display only. ConPTY is
the PTY backend, and the console has ANSI/VT processing enabled so escape sequences render.
Release-asset naming contract
Every channel — the script installer, Homebrew, oceancode update, oceancode verify-release, the
download page and the release CI job — agrees on one naming scheme. This is the single source of
truth; a producer or consumer that disagrees is a bug to reconcile against it.
Per target, the release publishes BOTH:
oceancode-<os>-<arch>.tar.gz— a gzipped tar containing exactlyoceancodeandocean-pager. The script installer (macOS/Linux) and Homebrew download this and install both side by side.oceancode-<os>-<arch>— the bare binary (Windows:oceancode-<os>-<arch>.exe).oceancode updatedownloads this and stages it directly asoceancode.newfor the atomic swap, so it must NOT be an archive. On Windows the PowerShell installer also consumes this bare.exe.ocean-pager-<os>-<arch>— the bare TUI binary;oceancode updateswaps it together withoceancode(the engine runs inside the pager process, so the pair must match). If either fails its--versionhealth check after the swap, both are rolled back to their<name>.bak. A release without this asset updatesoceancodealone and warns that the TUI was not updated.
Plus, for every asset (the tarball and both bare binaries):
<asset>.minisig— a detached minisign signature, and- one
SHA256SUMS— a single manifest hashing every asset (no per-target manifest).
No version appears in any filename — the version lives in the release tag path and the download
manifest's version field.
rustc triple ↔ <os>-<arch> token
The <os>-<arch> token is oceancode update's platform_suffix() (std::env::consts::OS +
std::env::consts::ARCH), matched as a substring of the asset name — so the Linux -musl suffix
is tolerated. The installer's detect_asset (packaging/install/install.sh) names match the tarball
column.
| rustc triple | <os>-<arch> token |
tarball (installer / Homebrew) | bare binary (oceancode update) |
bare TUI (oceancode update) |
|---|---|---|---|---|
aarch64-apple-darwin |
macos-aarch64 |
oceancode-macos-aarch64.tar.gz |
oceancode-macos-aarch64 |
ocean-pager-macos-aarch64 |
x86_64-apple-darwin |
macos-x86_64 |
oceancode-macos-x86_64.tar.gz |
oceancode-macos-x86_64 |
ocean-pager-macos-x86_64 |
x86_64-unknown-linux-musl |
linux-x86_64 |
oceancode-linux-x86_64-musl.tar.gz |
oceancode-linux-x86_64-musl |
ocean-pager-linux-x86_64-musl |
aarch64-unknown-linux-musl |
linux-aarch64 |
oceancode-linux-aarch64-musl.tar.gz |
oceancode-linux-aarch64-musl |
ocean-pager-linux-aarch64-musl |
x86_64-pc-windows-msvc |
windows-x86_64 |
oceancode-windows-x86_64.tar.gz |
oceancode-windows-x86_64.exe |
ocean-pager-windows-x86_64.exe |
Because the .tar.gz and the bare binaries carry the same <os>-<arch> token, the update path
must select the bare binaries specifically (skip .tar.gz) and tells them apart by the
ocean-pager- prefix; the installer/Homebrew select the tarball.
Platform download-page contract
When update.endpoint is set, oceancode update fetches release metadata from the platform's
release endpoint (Track P / S11-T03; the canonical schema lives in
docs/platform-download-contract.md). The endpoint returns JSON of this shape — the platform and
the binary agree on the artifact list here:
{
"version": "2.6.0",
"channel": "stable",
"artifacts": [
{
"target": "aarch64-apple-darwin",
"url": "https://…/oceancode-macos-aarch64",
"sha256": "…",
"minisig": "https://…/oceancode-macos-aarch64.minisig"
}
]
}
version and channel are strings; artifacts is one object per shipped target. url is the
bare binary oceancode update downloads and stages as oceancode.new (not the .tar.gz — that
is the installer's), sha256 matches its SHA256SUMS entry, and minisig is its detached
signature. This page documents the shape only; the endpoint itself is the platform's (Track P,
docs/platform-download-contract.md), not the binary's.
Release checklist
An ordered, mostly-automated release. Agents never publish or tag — the owner does the final GitHub Release and tap steps.
- Bump the version.
bash scripts/release/bump-version.sh <X.Y.Z>— moves the## UnreleasedCHANGELOG section under the new dated heading and updatesVERSION. This release is a minor (new subcommands + platforms):2.5.3→2.6.0, bumped 2026-09-15. Since then everyscripts/release/build-release.shrun bumps the patch on its own (2.6.01,2.6.02, …): a build number is never reused, and the check.sh lanes build withNO_BUMP=1. - CHANGELOG dated. Confirm the moved section reads correctly under
## 2.6.0 — <date>. - User-guide tables current.
node scripts/gates/check-user-guide.mjs→USER_GUIDE_OK … missing=0(everyOC_*var and subcommand has a row) andnode scripts/gates/check-changelog.mjs→CHANGELOG_OK. - Whole-tree green on three OSes.
./check.sh→ALL CHECKS PASSEDon macOS, Linux and Windows in CI. - Build the five targets.
bash scripts/release/build-all.sh(or the CIreleasejob's matrix) →BUILD_RELEASE_OK targets=5. Per target publish both the bareoceancode-<os>-<arch>(Windows.exe) and theoceancode-<os>-<arch>.tar.gz, using the<os>-<arch>tokens in the mapping table above — no version in any filename. - Per-artifact budgets.
RELEASE=1 ./check.sh(or the CI native legs) runssize-gate.shper artifact (≤ 26 MiB →SIZE_GATE_OK) andboot-bench.sh(< 36 ms →BOOT_BENCH_OK). - Sign.
bash scripts/sign/sign-artifacts.sh(minisign →SIGN_OK artifacts=<n>); where certificates exist,scripts/sign/codesign-macos.sh(Apple notarization) andscripts/sign/authenticode-windows.ps1(Windows Authenticode). - Checksums.
bash scripts/release/gen-sha256sums.sh dist→SHA256SUMS_OK count=<n>over the packaged assets. - Draft the GitHub Release (owner): upload the assets,
SHA256SUMSand each.minisig. - Homebrew tap (owner): mirror
packaging/homebrew/oceancode.rbto the tap with the realsha256s / bottle digests.
Owner-only steps (agents never do these)
- Replace
verify_release::EMBEDDED_PUBKEYand the installers' embedded minisign pubkey with the real release key. - Provision the CI secret
MINISIGN_SECRET_KEYand installminisignon the release runners. - Relocate
docs/plans/ci/oceancode.yml→.github/workflows/oceancode.yml. git tag/ publish the GitHub Release; push the Homebrew tap;cargo publish.
Release-notes template
# oceancode v<X.Y.Z>
## Highlights
- <the one or two changes users will notice first>
## Install
- macOS/Linux: `curl -fsSL <install.sh> | sh`
- Windows: `irm <install.ps1> | iex`
- Homebrew: `brew install OWNER/oceancode/oceancode`
- Cargo: `cargo install --locked oceancode`
## Verify
Download the asset for your platform and run:
`oceancode verify-release <asset> --sha256sums SHA256SUMS`
## Changed
- <user-visible changes from the CHANGELOG's dated section>
## Fixed
- <fixes from the CHANGELOG>
## Checksums
See `SHA256SUMS` and the per-asset `<asset>.minisig` attached to this release.
See also
- 01 · Getting started — the install one-liners and first run.
- 02 · Configuration —
update.*,shell.kind,OC_UPDATE_CHANNEL,OC_NO_UPDATE_CHECK,OC_INSTALL_DIR. - 07 · Headless and server —
update/verify-release/uninstallas management subcommands.
Operations
Running oceancode for a long time — days, not minutes — is a first-class case. This page
covers the operational surface that keeps a long run healthy: a metrics scrape, database and
ledger retention, the storage doctor, crash recovery, the event-bus drop counter, and log
levels. Everything here is off or conservative by default; you opt in.
The backup, restore and crash-recovery procedures live in the
storage-and-recovery runbook (apps/ocean-coding-agent-rust/docs/RUNBOOK-storage-and-recovery.md).
Metrics (OC_METRICS_ADDR)
Set OC_METRICS_ADDR=host:port to serve runtime gauges for a Prometheus scrape (uptime,
worker and live-task counts, injection-queue depth, and — on Linux — oceancode_rss_kb, the
process resident set from /proc/self/statm). Nothing listens by default. The endpoint is a
profiling aid, not a control surface: it exposes gauges, never accepts commands.
The HTTP server also answers GET /global/health, which carries dropped_events — see
Event-bus drops below.
A fresh headless server's resident set is held under the 45 MB SLO by the PERF=1 lane:
scripts/gates/rss-fresh.sh boots one release-binary serve on an ephemeral loopback port and
samples its RSS (RSS_FRESH_KB), which assert-budgets.mjs gates against the ceiling.
Retention and vacuum
Two independent stores grow over a long run — the session database oceancode.db and the
Session Ledger directory — and each has its own retention key. Both sweep hourly with the
first tick at boot, and both are off by default so an upgrade never deletes a user's
history on first launch.
| Key | Default | Effect |
|---|---|---|
database.retention_days |
0 (off) |
Top-level sessions whose whole tree has been idle longer than this many days are deleted hourly, exactly as a user delete would (messages, event log, share URL go with them). 0 disables the sweep. 30 is the ledger-symmetric opt-in. |
database.vacuum |
true |
Run VACUUM after a sweep that removed something, so the file shrinks on disk instead of only freeing pages internally. |
ledger.retention_days |
30 |
Session-ledger directories idle longer than this are pruned hourly (parent-aware). |
Set retention in oceancode.jsonc:
{
"database": { "retention_days": 30, "vacuum": true },
"ledger": { "retention_days": 30 }
}
A runtime config write re-arms the sweeps at the next start.
oceancode doctor --storage
oceancode doctor reports orphaned sessions and captured panic reports (read-only; it boots
nothing). Add --storage for a storage report:
$ oceancode doctor --storage
…orphan / panic report…
storage:
oceancode.db 4194304 bytes (wal 32768 bytes)
sessions=128
ledger 2097152 bytes, oldest 12 days
restore is offline-only: stop oceancode before restoring a backup
--storage is a doctor flag, not a separate subcommand. The report is best-effort: it
renders zeros for a path that does not exist yet.
Backup and restore
Backups are consistent even under concurrent writes (VACUUM INTO, not a raw file copy), and
restore verifies the backup's PRAGMA integrity_check before it swaps anything into place.
Restore is offline-only — it replaces the live database file, so stop oceancode first.
The step-by-step commands are in the runbook (apps/ocean-coding-agent-rust/docs/RUNBOOK-storage-and-recovery.md).
Crash recovery
A panic writes a timestamped report under the data dir's crash/, and an active-sessions
registry records a session whose process died without a clean exit. The next boot prints a
one-line stderr notice when it finds an orphan; oceancode doctor lists both. A crashed
session resumes with oceancode run --continue — the Session Ledger is intact byte-for-byte
across a kill -9. Full drill in the runbook (apps/ocean-coding-agent-rust/docs/RUNBOOK-storage-and-recovery.md).
Event-bus drops (dropped_events)
Each event subscriber has a bounded queue. When a subscriber falls behind, the bus's default
behaviour is to evict the oldest queued event and count the drop rather than block the
producer — so a slow consumer never stalls the streaming hot path. The running total is
visible as the dropped_events field on GET /global/health.
Two events config keys tune this:
| Key | Default | Effect |
|---|---|---|
events.sub_capacity |
1024 |
Per-subscriber event-queue depth. A deeper queue tolerates a slower subscriber before it drops. 0 falls back to the built-in default. |
events.mode |
drop_oldest |
Overflow behaviour. drop_oldest evicts the oldest event and counts the drop (the streaming default). block_until briefly parks the publisher (up to one second) for a free slot before falling back to drop-oldest — bounded back-pressure for a deployment that would rather slow a producer than lose events. |
The bus is sized from the project-tier events config at boot.
Log levels (OC_LOG)
The engine logs through a tracing subscriber that redacts secret-shaped values before any
write (Anthropic / OpenAI / AWS / PEM / bearer / assignment shapes), so a token never lands
in a log file. Control it with OC_LOG or the obs config block.
| Setting | Default | Effect |
|---|---|---|
OC_LOG (env) |
unset → obs.log_level |
Level: trace, debug, info, warn, error. Overrides obs.log_level. An unrecognized value logs a one-line notice and falls back to info. |
obs.log_level |
info |
The configured level when OC_LOG is unset. |
obs.log_rotate_bytes |
0 → 10 MiB |
Byte ceiling before the live log file rotates to <file>.1, so logs never grow unbounded on a multi-day run. |
obs.log_file |
unset → stderr | Log sink path for the headless modes (oceancode serve / run). Absent logs to stderr. See the sink-precedence note below. |
The level (OC_LOG → obs.log_level → info) applies to every mode. The obs config is read
from the run's project directory (the --dir a serve/run was given, else the current
directory), the same way boot reads project config (ocean_config::load, honouring
$OC_CONFIG_DIR) — so oceancode run --dir /path/to/project uses that project's obs settings,
not the shell's cwd. A malformed or absent config falls back to defaults and never blocks startup.
Sink precedence. obs.log_file (bounded by obs.log_rotate_bytes) redirects the default
stderr diagnostic sink of the headless oceancode serve / run modes to a size-bounded
rotating file, so headless logs never grow unbounded on a multi-day run. The interactive shape is
Ocean pager, which owns the terminal and keeps its own state under its seeded $OC_PAGER_HOME (see
19 · Ocean pager); tui.log is no longer written. So the jsonc example below
applies to serve / run:
{ "obs": { "log_level": "debug", "log_file": "/var/log/oceancode.log", "log_rotate_bytes": 10485760 } }
Endurance soak (operators)
A deterministic soak drives 1000 metering-fixture steps (no live model) and grades per-step
p99 latency and RSS growth against pinned budgets. Write the soak record to its own path so a
short marathon-fixture run cannot clobber it (soak-metrics.mjs grades any record under 1000
steps as SOAK_RED reason=short):
STEPS=1000 TURNS_OUT=docs/results/soak/turns.jsonl bash scripts/bench/marathon-fixture.sh --steps 1000
node scripts/bench/soak-metrics.mjs docs/results/soak/turns.jsonl # → SOAK_OK / SOAK_RED
OC_SQLITE_CACHE_KB and OC_READ_POOL_SIZE bound per-connection SQLite memory and the read
pool for a measurement run; leave both unset in normal operation. See
02 · Configuration.
Ocean pager — the terminal UI
Ocean pager is oceancode's terminal UI. A plain oceancode execs Ocean pager (ocean-pager); the pager and the Ocean engine run in one process over typed ACP channels — no HTTP between UI and engine. --db, --directory, --listen and --verbose are passed through.
The first-party ratatui TUI was removed on 2026-10-05 (its guide pages are in archive/native-tui/docs/). For compatibility --tui pager and OC_TUI=pager are accepted and change nothing (--tui grok is refused as unknown); --tui ocean and OC_TUI=ocean fail with "the native TUI was removed; oceancode starts Ocean pager". The pager's own slash commands, keys and settings are in Ocean pager reference.
Binary
- Build the pager:
bash scripts/release/build-pager.sh(orcd ocean-pager && cargo build --release -p ocean-pager-bin --locked) →target/release/ocean-pager, the binaryoceancodelaunches. - A plain debug build lands in
target/debug/ocean-pagerand is never launched. To run a pager from elsewhere, setOC_PAGER_BIN.
Discovery order: OC_PAGER_BIN → ocean-pager beside oceancode (and beside the binary a symlinked oceancode resolves to) → ocean-pager on PATH.
oceancode sets $OC_PAGER_HOME to <data>/oceancode/pager ($XDG_DATA_HOME, else ~/.local/share) — the directory a bare ocean-pager uses too — so the pager never shares ~/.grok. Auto-update, remote fetch, feedback and voice are off. Every pager variable: Environment.
Environment
Read by ocean-pager, not by the engine. Every pager variable is OC_PAGER_*; the old GROK_* names are not read.
| Variable | Effect |
|---|---|
OC_PAGER_HOME |
The pager's state directory (config.toml, sessions, debug logs, the extracted ripgrep under bin/). oceancode sets it; unset → $XDG_DATA_HOME/oceancode/pager, else ~/.local/share/oceancode/pager. GROK_HOME is ignored. |
OC_PAGER_LOG_FILE |
Write the pager's debug log to exactly this path (honors RUST_LOG). |
OC_PAGER_DEBUG_LOG |
A path, or 1/true for <pager home>/debug/tui-<pid>.txt: turns the debug log on. |
OC_PAGER_CONFIG |
Inline JSON config overlay, merged above config.toml. |
OC_PAGER_CONFIG_PATH |
An extra JSON or TOML config-overlay file (read by extension), merged above config.toml. |
OC_PAGER_MANAGED_CONFIG_FAIL_CLOSED |
Overrides the requirements fail-closed setting (applies to requirements.toml only). |
OC_PAGER_SHELL |
Shell the pager spawns: an executable path (bash, zsh, …); on Windows pwsh, powershell or cmd. |
OC_PAGER_THEME |
Force a theme name, or auto, without editing the config. |
LC_OC_PAGER_THEME |
Same as OC_PAGER_THEME; the LC_ form survives SSH SendEnv LC_*. |
OC_PAGER_APPEARANCE |
dark or light: the terminal background auto themes assume. |
LC_OC_PAGER_APPEARANCE |
Same as OC_PAGER_APPEARANCE, SSH-forwardable. |
OC_PAGER_TERMINAL_THEME |
Follow the terminal's own palette (on/off; a requirement pin wins). |
OC_PAGER_NERD_FONTS |
Nerd Font glyphs in the git status (on/off). |
OC_PAGER_FORCE_LEGACY_CONSOLE |
Windows: ASCII-safe glyphs for the legacy console. |
OC_PAGER_SCREEN_MODE |
Internal: the screen mode a /fullscreen / /minimal relaunch hands to the new process (read and removed at startup). |
OC_PAGER_SCREEN_MODE_SWITCH |
exec forces the legacy re-exec switch between screen modes. |
OC_PAGER_SCROLL_SPEED |
Mouse-wheel scroll speed; applied on first load only (the settings modal persists it). |
OC_PAGER_SCROLL_MODE |
Mouse-wheel scroll mode; first load only. |
OC_PAGER_SCROLL_LINES |
Lines per wheel notch; first load only. |
OC_PAGER_INVERT_SCROLL |
Invert the wheel direction; first load only. |
OC_PAGER_SCROLL_CADENCE_MS |
Redraw cadence while scrolling (ms). |
OC_PAGER_MIN_DRAW_MS |
Minimum interval between frames (ms). |
OC_PAGER_DISPLAY_REFRESH_PROBE_ENABLED |
Probe the display refresh rate at startup (on/off). |
OC_PAGER_DISPLAY_REFRESH_AUTO_CADENCE |
Derive the draw cadence from the probed refresh rate (on/off). |
OC_PAGER_SCROLL_DEBUG |
Show the scroll debug HUD. |
OC_PAGER_SCROLL_LOG |
Log scroll input events (unset or 0 = off). |
OC_PAGER_FPS |
Show the frames-per-second HUD. |
OC_PAGER_MEMTRACE |
Periodic memory trace in the debug log; 0/false/off disables. |
OC_PAGER_MEMTRACE_INTERVAL_SECS |
Memory-trace interval (seconds). |
OC_PAGER_MEMTRACE_THRESHOLD_MB |
Memory trace logs only above this resident size (MB). |
OC_PAGER_MOUSE_REPORTING_TOGGLE |
Enable the mouse-reporting toggle (on/off). |
OC_PAGER_SHOW_THINKING_BLOCKS |
Show model thinking blocks (on/off; overrides the setting). |
OC_PAGER_GROUP_TOOL_VERBS |
Group consecutive tool calls by verb (on/off; overrides the setting). |
OC_PAGER_COLLAPSED_EDIT_BLOCKS |
Start edit blocks collapsed (on/off; overrides the setting). |
OC_PAGER_CONTEXTUAL_HINTS |
Contextual hints on/off (overrides [ui.contextual_hints]). |
OC_PAGER_TIPS_OVERRIDE |
Debug builds only: ` |
OC_PAGER_SLASH_COMMAND_TAGS |
JSON map of tags shown beside slash commands in the / menu. |
OC_PAGER_DEFAULT_SELECTED_PERMISSION |
Which permission-card option the cursor starts on. |
OC_PAGER_ESC_DOUBLE_PRESS_MS |
Window for the double-Esc gesture (ms). |
OC_PAGER_DOCK |
Dock layout on/off (a requirement pin wins). |
OC_PAGER_DOCK_V2 |
Second dock layout on/off (same precedence as OC_PAGER_DOCK). |
OC_PAGER_AGENT_DASHBOARD |
0 turns /dashboard off. |
OC_PAGER_WORKSPACE_DASHBOARD |
Use the workspace (v2) dashboard layout instead of the persisted v1 layout. |
OC_PAGER_SESSION_PICKER_GROUPED |
1/0: group the session picker (overrides [cli] session_picker_grouped, default on). |
OC_PAGER_HUNK_TRACKER |
Hunk-tracker mode (overrides [ui] hunk_tracker_mode). |
OC_PAGER_SUGGESTIONS |
Shell-command ghost suggestions (off by default). They request ocean/suggest, which the Ocean engine does not serve, so nothing shows. |
OC_PAGER_SUGGESTIONS_AI |
AI variant of OC_PAGER_SUGGESTIONS (same limitation). |
OC_PAGER_SUGGESTIONS_AI_MODEL |
Model id for OC_PAGER_SUGGESTIONS_AI. |
OC_PAGER_MCP_PUSH_SERVER_STATUS |
Push MCP server status updates into the UI (default on). |
OC_PAGER_ENVRC_TIMEOUT_SECS |
Timeout for evaluating a directory's .envrc (seconds; 0 disables). |
OC_PAGER_CONNECT_UI_TIMEOUT_SECS |
Seconds before startup shows the connect-failure report (6 s floor). |
OC_PAGER_PROMPT_ACK_TIMEOUT_SECS |
Prompt acknowledgement timeout (seconds). |
OC_PAGER_EXIT_TIMEOUT_SECS |
Force-exit this many seconds after a quit if teardown hangs (default 20; 0 disables). |
OC_PAGER_COPY_FILE |
Backup file every copy is also written to (default <pager home>/last-copy.txt). |
OC_PAGER_OSC52_SINK |
Emit clipboard writes as OSC 52 for a wrapping terminal. |
LC_OC_PAGER_OSC52_SINK |
Same as OC_PAGER_OSC52_SINK, SSH-forwardable. |
OC_PAGER_CLIPBOARD_NO_OSC52 |
Never emit OSC 52 (for hosts that paint it as garbage). |
OC_PAGER_CLIPBOARD_NO_NATIVE_READ |
macOS: don't read the native pasteboard. |
OC_PAGER_CLIPBOARD_NO_DATA_CONTROL |
Linux: skip the Wayland data-control clipboard protocol. |
OC_PAGER_SQLITE_JOURNAL_MODE |
SQLite journal mode for the pager's local databases (the WAL kill switch). |
OC_PAGER_RESET_CHILD_OOM |
Linux, internal: set on child processes so they reset the inherited OOM score. |
OC_PAGER_EVENT |
Set for notification hook commands: the event name. |
OC_PAGER_MESSAGE |
Set for notification hook commands: the message text. |
OC_PAGER_SESSION_ID |
Set for notification hook commands: the session id. |
OC_PAGER_BUNDLE_RG_PATH |
Build time: embed this ripgrep binary instead of downloading the release tarball. |
OC_PAGER_RG_VER |
Build-internal: embedded ripgrep version, set by ocean-pager-ripgrep/build.rs. |
OC_PAGER_RG_TARGET |
Build-internal: embedded ripgrep target triple. |
OC_PAGER_RG_SHA256 |
Build-internal: SHA-256 the extracted ripgrep is verified against. |
OC_PAGER_CHANGELOG_OFFLINE |
Tests: never fetch the changelog. |
OC_PAGER_MDM_NO_EXPAND_TEST |
Tests: macOS managed-preferences layer. |
OC_PAGER_TEST_OPEN_URL_FILE |
Tests: write opened URLs to this file instead of launching a browser. |
OC_PAGER_TEST_FRAME_WRITE_DELAY_MS |
Tests: delay each frame write (ms). |
OC_PAGER_TEST_HOLD_TEARDOWN_SECS |
Tests: hold teardown to exercise the exit timeout. |
What is wired to the Ocean engine
/, /mcps, /skills, /plugins, /hooks, /workflows, /model, and /effort use the pager chrome and Ocean Services. MCP add/toggle, plugin enable/disable, hook reload, and tool permission cards go through the same ACP methods Grok Build uses. The /workflows picker lists built-in and discovered project/user workflows over ocean/workflows/list (empty when the workflows section is disabled); launching, pausing, resuming and stopping a run go through the workflow tool, not an ext request.
Skills complete in the composer. Typing / and a prefix lists your enabled, user-invocable skills alongside the pager's own commands — a plugin skill under Claude Code's plugin-qualified name (/superpowers:writing-plans, so /super finds every superpowers skill), a project or user skill under its bare name (/formatcode); the prefix narrows the list, Tab/Enter inserts /<skill> into the message box so you can keep typing, and sending dispatches it — the engine expands the skill body into the turn (a typed bare /writing-plans still runs the plugin skill too). The candidates come from ocean/skills/list (fetched at session start; the Ocean engine does not advertise skills as ACP commands), so a disabled skill is never offered. A Space toggle on the Skills tab of the capabilities picker updates the popup live, and the <available_skills> block and skill_* tools shrink on the next step (09 · Capabilities).
The capabilities picker. Each tab (/skills, /plugins, /hooks, /mcps, /workflows, Marketplace) lists only its own kind. On the Skills tab, Space toggles the skill under the cursor and the change sticks (it writes ~/.oceancode/skill-state.json); / opens a search that filters the rows by the skill's name, its slash id, its description and its author, case-insensitively, and a query that matches nothing reads No matches. a / d enable / disable every skill behind a confirm prompt. Each row shows its source — project, user or plugin: <name> — grouped by source.
Group and master toggles. On the Skills, Plugins, Hooks and MCP tabs Space also works above the single row; every change applies on the next step and costs one cache rewrite (09 · Capabilities):
| Row / key | Action |
|---|---|
Everything (first row) + Space |
Master switch: off removes skills, MCP tools, hooks and plugin rules from the agent's context, built-in tools stay; asks first |
All skills / All plugins / All MCP / All hooks (second row) + Space |
The whole kind on or off; asks first |
a / d (Skills tab) |
Same as All skills on / off |
A Plugin: <name> header (Skills, MCP) or a hook source header + Space |
Every row in the group; asks first above 10 rows; an all-off group reads [off] |
A row + Space |
That one skill, plugin, hook or MCP server |
The picker's last row shows the live counts (skills 22/259 · plugins 2/21 · mcp 1/10 · hooks 8/8 · applies on the next step (one cache rewrite)), prefixed EVERYTHING OFF while the master switch is off. The Plugins tab also lists Claude Code's cached plugins under User (Claude). The Marketplace tab reads ~/.oceancode/marketplaces/** only when you open it (opening /skills or /plugins no longer touches the marketplace), and marketplace content never reaches the prompt or the tool list.
Pasted images
Paste an image from the clipboard (Ctrl+V / Cmd+V) or drag an image file onto the composer; it shows as [Image #1] in your text and goes to the model with the prompt ("what's wrong with this layout? [Image #1]"). Every route carries it — Anthropic, OpenAI chat and Responses (xAI, Groq, OpenRouter, …), Gemini. With several images, each is labelled [Image #N] in the request, so the model matches the name in your text to the right picture.
Limits: at most 20 images per prompt and 3.75 MiB per image (about 5 MB once base64-encoded — Anthropic's limit, the strictest of the routes). The engine sends PNG, JPEG, GIF and WebP — the types every route accepts, decided by the image's bytes, not its declared type. The pager re-encodes a TIFF (what a macOS screenshot usually is on the clipboard), BMP, ICO or GIF as PNG before it sends, so those paste and drag in like any other; PNG, JPEG and WebP go as they are. An image over the cap after that, a type the pager cannot decode, or an http(s) source refuses the whole prompt with a message naming the image; nothing is resized or dropped silently.
Text-only models. When the catalog says the current model reads no images (its models.dev entry lists input modalities without image), the text still goes and the image does not; a session note says so: Image #1 not sent: <model> doesn't accept images. A model the catalog cannot describe — an offline seed entry or one you declared under providers.*.models — gets the image. Switching to a text-only model after pasting drops the media from the request on the next turn — and an image a tool returns (read on a PNG) is replaced by a one-line note on such a model — so the provider does not answer 400.
The image is stored inline on the user row of oceancode.db ("files":[{"uri":"data:image/png;base64,…); a reopened session replays that row as text only, and the headless and server prompt bodies (07 · Headless and server) carry text only for now.
Goal mode (orchestrate: always)
Set orchestrate: "always" in the workflows section of oceancode.json and a plain prompt is
no longer a single turn — it launches the built-in goal workflow, which plans the objective
into verifiable deliverables, works in rounds with one resumed worker, and converges only when a
panel of adversarial skeptics fails to refute completion (identical gaps two rounds running, or a
spent budget, pause instead of looping). The pager's Workflows dock shows the run advancing
through its Plan → Work → Verify phases, with the sub-agents of each round beneath it. The
Claude-settings ultracode flag does not turn this on — only the explicit orchestrate
opt-in does. Headless, the same goal run is oceancode goal "<objective>" (see
07-headless-and-server.md).
What the pager does not do
Login, billing, worktrees, /imagine and Grok memory flush are unsupported (-32601).
Connecting a provider is in the pager: pick a gray /model row, or /connect <provider>. OpenCode-style auth methods:
- API key — most providers (Google, Groq, Perplexity, …).
- ChatGPT Plus/Pro — OpenAI, via OpenCode’s JSON deviceauth (
auth.openai.com/codex/device), or paste an API key. - Claude Pro/Max — Anthropic PKCE (paste the browser code), or paste an API key.
- GitHub Copilot — GitHub device flow (
github.com/login/device). - claude-code —
oceancode claude(Claude CLI login). - Bedrock / Vertex — environment/config, not a prompt.
Already-connected providers open a manage overlay: switch API key ↔ subscription, or d to disconnect (removes auth.json; env-backed providers stay connected until the env var is unset). Credentials go to the same auth.json as oceancode key / oceancode login.
Expired subscription
Ocean runs on the Claude subscription by consuming the active slot Claude Code keeps in
~/.claude/.credentials.json (claudeAiOauth); it never rotates that slot's refresh token. On
macOS, once that file's token is within five minutes of expiry, Ocean also reads the Keychain item
Claude Code refreshes itself into (Claude Code-credentials, read only) and uses whichever carries
the later expiry, so a self-refresh by Claude Code no longer strands Ocean an hour later; no file
still means no login. When the slot's access token has expired and carries no refresh token, a
prompt cannot be sent, and the
turn fails in the transcript with a terminal, actionable message —
Claude subscription token has expired: Run oceancode login to reconnect your Claude account. —
and no retry countdown or network banner (the failure is not a transport error and is never
retried). Run oceancode login, complete the Claude Pro/Max browser flow, and send the prompt
again. (Headless, oceancode run prints the same message to stderr and exits non-zero.)
The same holds when the slot expires mid-session: it was valid when the turn started, so the
request reached Anthropic and the server rejected the now-dead token with an HTTP 401. On the
subscription route that 401 (and a 403) is the same terminal run oceancode login failure — it is
never retried — whereas a 401 on an API-key provider stays a generic error (its fix is a new key,
not a re-login).
Coming from the native TUI (removed 2026-10-05)
The pager renames some surfaces of the removed native TUI and has no ctrl+x leader — it uses
direct chords. The pager equivalents:
| Native TUI | Ocean pager | Notes |
|---|---|---|
/copy |
/toggle-mouse-reporting |
Toggle terminal mouse capture so text selection/copy works. |
/mcp |
/mcps |
The MCP-servers picker. |
/models |
/model |
The model picker (also gates provider /connect). |
/cockpit |
status bar + /context |
No unified cockpit panel; the status bar plus /context show the same figures. |
ctrl+x leader chords |
direct chords | The pager has no ctrl+x prefix. Use ctrl+p (command palette), ctrl+\ (Agent Dashboard), ctrl+t (todos pane), ctrl+m (multiline / model picker off the prompt). |
Shift+Tab ring and permission modes
Shift+Tab cycles Build → Auto → Plan → Ask → Debug → Build. Build, Plan, Ask and
Debug are the engine's agents (the curated ring in crates/ocean-acp/src/modes.rs);
Auto is the one permission stop, Claude Code's "accept edits": the agent stays Build,
the prompt border shows auto in yellow, and the engine's permission mode is
acceptEdits — edits run without a card, bash and outside-directory asks still
prompt (or go to Jev, see 13), and the question tool always
asks. Cycling on from Auto drops back to ask before the switch to Plan. The stop is
absent while always-approve is on, and when the auto feature is gated off
([auto_mode] enabled = false in the pager config).
The same mode is reachable from /auto, /always-approve (ctrl+o; the engine's
bypassPermissions, every card auto-answered by the pager) and the permission_mode
row of the settings modal. Each change is persisted to the pager config and sent to
the engine as ocean/yolo_mode_changed, so the engine's mode and the border flag
agree (see 12 · Permissions and sandbox).
A subagent's ask surfaces as its own card, labelled with the subagent.
Search counter and tideline tiers
The top-right status row shows a search N · 0.3s chip once the session has run a
grep or glob tool call: N is the number of calls, the time is the last call's
latency (green ≤ 1 s, yellow ≤ 5 s, orange above). Click it for the breakdown —
calls by tool, last / average / total latency, bytes and files scanned, and how
many recursive grep -r / find the model ran through bash (and how many of
those were rewritten to rg). The figures are folded from each tool result's
metadata (elapsed_ms, files, bytes, search_redirect) in oceancode.db,
so they survive a restart; nothing is counted in memory.
The Ocean context row (ctx … N% full · tideline …) is tiered against the
tideline limit: green under 80 % of it, yellow from 80 %, orange at or over it
(the gauge, the percentage and the tideline clause share the tone). With the
ledger off the same tiers apply against the context window. tideline ×12 @30% …
carries the number of turns past the tideline (/tideline prints the same
figure); no ×N while it is zero.
The bash tool rewrites a plain recursive grep -r … to the bundled ripgrep
(POSIX shells; every flag must have an rg equivalent, patterns are literal-safe)
and prepends [oceancode] ran as: rg … to the result; any other recursive
grep or a find … -name runs as typed under a one-line hint naming the grep /
glob tools. A leading cd <dir> &&/; prefix, a trailing 2>/dev/null,
| grep -v … / | head followers, and BRE \| alternation are carried or
translated verbatim, so the common slow pipelines still rewrite. The ripgrep directory leads the child PATH, so rg is available
in bash on every OS where a binary resolves (OC_RG_BIN overrides; see 02).
Ocean session commands
These act on the Ocean engine over _ocean/* / ocean/* ext methods (Ocean backend only):
| Command | What it does |
|---|---|
/tideline |
Bare shows the ledger state; /tideline now cuts the ledger; <n>%, <tokens>, or default set the tideline (the floor and the presets were removed — a second number or a preset word is refused with the usage line). The old /shelve still works as a hidden alias. |
/budget |
Bare shows the spend cap; $5 (or 5) caps USD, off clears it, subscription switches to list-price mode. |
/limits |
Bare shows the per-turn bounds; steps <n> caps steps, time <90m|1h30m> bounds wall-clock. |
/subagent |
Bare reports the effective sub-agent model + source; <provider/model> pins one, default / off / clear removes the override. |
/plan add|done|cancel|edit|remove |
Edit the live todo table (1-based rows); bare /plan still enters plan mode. |
/redo |
Re-apply the turn a /undo (/rewind) reverted. |
/share |
Share this session via a public URL (Ocean backend). |
/unshare |
Revoke the current session's share link. |
/notify |
Toggle desktop notifications; /notify on|off sets it explicitly. |
Xcode and other ACP editors — oceancode acp
oceancode acp runs the Ocean engine as an Agent Client Protocol (ACP) agent on the process's
stdin/stdout. An ACP editor — Xcode 26.6 and later, Zed, JetBrains, Neovim — starts the binary itself
as a subprocess and talks newline-delimited JSON-RPC over the pipe. There is no URL, no port and no API
key for the editor: the editor is the client, the subprocess is the agent, and the editor asks you for
consent the first time it launches it.
It is the same engine as the pager: the same model and credentials, the same permissions, the same
agents (build · plan · ask · debug), the same ledger and session store. Only the host differs
(crates/ocean-cli/src/acp.rs:22, crates/ocean-acp/src/stdio.rs:38).
| Flag | Default | Meaning |
|---|---|---|
--db <path> |
oceancode.db |
the session store |
--directory <dir> |
cwd | the working directory the engine starts in; the editor's session/new cwd wins per session |
--verbose, -v |
off | trace every provider request/response to stderr (credentials masked) |
stdout is the wire. Every byte the process writes to stdout is one JSON-RPC message; the banner, diagnostics and tracing go to stderr. The process exits 0 when the editor closes stdin.
Xcode
Source: Apple, Setting up coding intelligence — "To enable an agent that doesn't appear in Intelligence settings and supports the Agent Client Protocol (ACP), click Add an Agent under Agents, enter information in the next sheet, and click Add."
Find the binary's absolute path:
which oceancode(for example/Users/you/.local/bin/oceancode). Xcode needs the absolute path — no~, no relative path, and it validates nothing until first launch.Xcode › Settings › Intelligence, under Agents click Add an Agent… and fill the sheet:
Field Value Name Ocean CodeExecutable the path from step 1 Interpreter leave blank ( oceancodeis a native binary, not a script)Arguments acp— one argument per rowEnvironment Variables none needed (see Credentials) Click Add. Ocean Code now appears under Agents.
In the Intelligence sidebar click New Conversation and pick Ocean Code. The first time, Xcode asks whether to allow the agent to run — click Allow.
Do not confuse the Agents sheet with Chat › Add a Chat Provider: that door takes a URL (or a local
port) serving /v1/models and /v1/chat/completions plus an API key and gives a chat box without
tools. The coding agent lives behind the Agents sheet.
Credentials
Xcode starts the agent through your login shell (observed on Xcode 27: the agent's PATH carries
~/.local/bin, Homebrew and Xcode's Developer/usr/bin), so MCP servers started with npx or uvx
spawn fine — but do not rely on a shell export for the credential. Log in once from a terminal and
the stored credential is used by every Xcode session:
oceancode auth # API key or Claude Pro/Max subscription → auth.json in the data directory
(01 · Getting started lists the locations.) A key in oceancode.json works
too. The sheet's Environment Variables rows also work, but Xcode writes their values into a plist under
~/Library/Developer/Xcode/CodingAssistant/ACP/ — prefer the stored login.
Xcode runs the agent with ~/Library/Developer/Xcode/CodingAssistant/<agent-id>/ as its working
directory, so that is where oceancode.db — the transcripts of your Xcode conversations — lands;
each session's own cwd is the project folder Xcode hands over in session/new.
Xcode's tools
When Xcode starts a session it hands the agent its own tools — build, test, run, SwiftUI previews,
documentation search, diagnostics — as an MCP server (xcode-tools) in session/new. The engine
registers every server the editor supplies before the session is created
(crates/ocean-acp/src/mcp_servers.rs:19, crates/ocean-acp/src/agent.rs:158), so the first step
already offers them; the tools are named xcode-tools_<Tool> in the registry. Nothing to configure:
"build the project" in an Xcode conversation uses Xcode's build, and Xcode asks before the first
build/test tool runs, as it does for every agent.
For a session you start outside Xcode — the pager, oceancode run — the same tools come from
Xcode's MCP bridge: in Intelligence settings turn on Allow external agents to use Xcode tools, then
oceancode mcp add xcode --command xcrun --arg mcpbridge
with the project open in Xcode (11 · MCP; Apple: Giving external agents access to Xcode).
Permissions and questions
A tool call that needs approval becomes an ACP session/request_permission with three options —
allow once, allow always, reject (crates/ocean-acp/src/stdio.rs:136); Xcode shows it as its own
approval prompt. Cancelling the prompt rejects the tool (crates/ocean-acp/src/stdio.rs:178). The
engine's question tool has no equivalent in base ACP, so it is rejected and the model asks in text
instead. Always-approve and the Shift+Tab agent ring are reachable through the editor's mode picker
where it has one; Xcode exposes the session modes it is given (build · plan · ask · debug).
Verify by hand
Two requests on stdin, one reply or notification per line on stdout, exit 0 at EOF — no Xcode needed. Hold stdin open for a moment: the host returns as soon as the pipe closes, and a request still in flight at that instant is dropped (an editor keeps the pipe open for the life of the conversation).
{ printf '%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":1}}' \
'{"jsonrpc":"2.0","id":2,"method":"session/new","params":{"cwd":"'"$PWD"'","mcpServers":[]}}'
sleep 3
} | oceancode acp 2>/dev/null
The first line answers with agentCapabilities (loadSession, mcpCapabilities.http/sse), the last
with a sessionId plus the model and mode lists; between them an _ocean/sessions/changed
notification — an Ocean extension the pager uses and a plain ACP client ignores. Add a
session/prompt line with that id to see the turn stream as session/update notifications. This is
what crates/ocean-cli/tests/acp_mode.rs pins on every build.
See also
- 19 · Ocean pager — the terminal UI on the same engine.
- 07 · Headless and server —
serve,run,goal,workflow. - 12 · Permissions and sandbox — the rules behind every approval prompt.
CLI reference
oceancode is one binary: oceancode [subcommand] [flags] [args]. With no subcommand it starts Ocean
pager. acp runs it as an editor agent, and serve, run, goal and workflow run it headless.
01 · Getting started explains the three boot shapes. This page lists every
subcommand, its flags and its exit codes.
Flag syntax
Most engine commands parse flags with a port of Go's flag package: the default shape, serve, acp,
migrate, run, workflow, goal, ledger, export, login, logout, key and auth. Go's rules
apply:
-nameand--nameare the same flag. A value is written--name valueor--name=value. Single-letter flags cannot be bundled:-vcis one flag namedvc.- Parsing stops at the first argument that is not a flag, or at a bare
--, which is dropped. Everything after that point is positional, even if it looks like a flag. - A boolean flag never takes the next argument; turn one off with
--name=false. Integer flags accept Go's base prefixes (0x1000,0o10000,0b…) and_separators. -h/--helpprintsUsage of <name>:and the flag list to stderr, then exits 0. An unknown flag, a missing value or a bad value prints the error and the usage to stderr, then exits 2.
The capability and maintenance commands (plugin, mcp, skill, trust, hooks, jev, inspect,
doctor, update, verify-release, uninstall, claude) parse their arguments by hand. Write their
flags with two dashes, exactly as shown. Most of them reject --help with a usage error (exit 1).
update ignores --help and runs a real update, so never try oceancode update --help.
Global flags
--version must be the first argument. --data-dir and --fake-llm-sse are removed from the whole
command line before the subcommand is chosen, so a run prompt that contains the words --data-dir x
loses them.
| Flag | Type | Default | Meaning |
|---|---|---|---|
--version |
— | — | Print oceancode <version> (<build stamp>) and exit 0. Needs no database, config or terminal. -v is not an alias: it means --verbose. |
--data-dir <dir> |
string | the app data dir | Where auth.json and the other per-install state live, for commands that boot the engine. Also --data-dir=<dir>. |
--fake-llm-sse <file> |
string, repeatable | none | Test hook: replay a recorded provider SSE body for each turn, in order, repeating the last one. Also --fake-llm-sse=<file>. |
OC_DETERMINISTIC=1 is read at the same point: it pins the clock and seed that ids are minted from, so
two runs produce identical id sequences. Every invocation except --version checks for orphaned sessions
left by a crashed run. When it finds any, it prints one line to stderr: note: <n> orphaned session(s) from a previous run — run `oceancode doctor` for details.
Common engine flags
--db, --directory and --verbose are not global; each command declares them. They mean the same
thing everywhere, so the per-command tables below omit them.
| Flag | Type | Default | Meaning | Taken by |
|---|---|---|---|---|
--db |
string | oceancode.db |
Path to the session database. | the default shape, run, serve, acp, workflow run/pause/resume/stop, goal, ledger, export, migrate |
--directory |
string | current directory | Working directory. For workflow runs it is the project to filter by. |
the same, minus migrate, plus workflow runs |
--verbose, -v |
bool | off | Trace every outbound LLM provider request/response to stderr, with credentials masked. | the default shape, run, serve, acp, workflow run and its control verbs, goal |
Exit codes
| Code | Meaning | Where |
|---|---|---|
| 0 | Success, or -h/--help on a Go-flag command. |
all |
| 1 | The command failed. The message is printed to stderr as error: <message>. |
all |
| 2 | Flag parse error (unknown flag, missing or bad value). | Go-flag commands |
oceancode run exits with a typed code once its turn has started. A failure before that (boot, flag
validation, an empty message) is exit 1.
| Code | Meaning | Stop reason(s) |
|---|---|---|
| 0 | finished | finished |
| 2 | permission rejected | permission_rejected |
| 3 | budget | budget |
| 4 | time | time |
| 5 | no progress | no_progress, max_steps, doom_loop |
| 6 | error | error, hook_blocked; also a --json-schema that cannot be satisfied |
| 130 | interrupt | user_interrupt |
oceancode workflow run and oceancode goal exit by the run's final status:
| Code | Status |
|---|---|
| 0 | complete (and a --validate-only check) |
| 2 | failed, cancelled, interrupted, budget_limited, or any unrecognised status |
| 3 | a pause: user_paused, back_off_paused, no_progress_paused, infra_paused, blocked |
Exit 2 from run can be a permission rejection or a flag error; the stderr text tells them apart.
oceancode (no subcommand)
oceancode [--db <path>] [--directory <dir>] [--listen <host:port>] [--tui pager] [-v]
Execs Ocean pager, which runs the engine inside its own process. stdout must be a terminal; otherwise it
fails with stdout is not a terminal — use `oceancode serve` for headless serving. The pager binary is
looked up in this order: OC_PAGER_BIN, then next to oceancode (also next to its target if oceancode
is a symlink), then PATH. Here --verbose becomes the pager's --debug.
| Flag | Type | Default | Meaning |
|---|---|---|---|
--listen |
string | 127.0.0.1:4096 |
HTTP bind address handed to the pager. A non-loopback address needs OC_SERVER_PASSWORD. |
--tui |
string | empty | Kept for old scripts: empty or pager is accepted, ocean is refused (the native TUI was removed), and any other value is an error. Falls back to OC_TUI. |
See 19 · Ocean pager.
oceancode run
oceancode run [flags] <message…>
Runs one non-interactive turn and exits with the typed code above. Flags must come before the
message, because a flag after it is sent to the model as text; a bound flag placed after the message
also prints a warning that the run is not bounded by it. When stdin is not a terminal, run reads it to
EOF and appends it to the message, so close stdin (</dev/null) when the prompt is only an argument. An
empty message fails with you must provide a message.
| Flag | Type | Default | Meaning |
|---|---|---|---|
--agent |
string | empty | Agent to use. |
--model, -m |
string | empty | Model to use, provider/model. |
--variant |
string | empty | Model variant (provider-specific reasoning effort, e.g. high, max). |
--title |
string | truncated prompt | Session title. |
--continue, -c |
bool | off | Continue the last session. |
--session, -s |
string | empty | Session id to continue. |
--resume-title, -r |
string | empty | Resume the most recent session whose title matches this text. |
--fork |
bool | off | Fork the session before continuing. An error unless --continue or --session is also given. |
--auto |
bool | off | Auto-approve permissions that are not explicitly denied (dangerous!). |
--output-format |
string | text |
stream-json (NDJSON per event), json (one buffered result object), or text. Takes precedence over --format. |
--format |
string | default |
Deprecated. default = text; json = the stream-json framing. |
--tools |
string | all | Restrict the advertised tools to these comma-separated ids. An unknown id is an error. |
--disallowed-tools |
string | empty | Remove these comma-separated tool ids from the advertised set. |
--allow |
string | empty | Append allow permission rules, e.g. "Bash(git *),Edit(src/**)". |
--deny |
string | empty | Append deny permission rules (last match wins, so a --deny beats an overlapping --allow). |
--append-system-prompt |
string | empty | Append this text to the run's system context. |
--json-schema |
string | empty | Validate the model's final JSON against this JSON Schema (a file path or inline JSON). On a mismatch the model is re-prompted with the validation errors, up to a retry limit. Prints the validated object; exits 6 if the schema cannot be satisfied. |
--max-usd |
string | unset | USD bound for this session: a number, off, or subscription (no bound; spend shows the list-price estimate). |
--max-minutes |
string | unset | Wall-clock bound: minutes, 90m, 8h, 2d, or off. |
--max-steps |
string | unset | Step bound per turn, 1..=1000, or default to clear a stored override. |
--tideline, --shelve |
string | unset | The Session Ledger tideline: <pct>% (a share of the model's context), <limit>, or default. --shelve is the older spelling. |
--window |
string | — | Removed. Any value fails with --window was renamed: use --tideline. |
--subagent |
string | unset | The model this session's sub-agents run, provider/model. |
--atif |
string | empty | When the turn ends, write the session's ATIF-v1.8 trajectory (JSON) to this path. |
See 07 · Headless and server and 08 · Bounding a long run.
oceancode serve
oceancode serve [--port <n>] [--host <ip>] [--db <path>] [--directory <dir>] [-v]
Starts the engine with its HTTP surface only and no UI. The banner goes to stdout and diagnostics to
stderr. A non-loopback --host without OC_SERVER_PASSWORD is refused before anything boots.
| Flag | Type | Default | Meaning |
|---|---|---|---|
--port |
int | 4096 |
TCP port to listen on. |
--host |
string | 127.0.0.1 |
Interface to bind. A non-loopback host requires OC_SERVER_PASSWORD. |
oceancode acp
oceancode acp [--db <path>] [--directory <dir>] [-v]
Serves the engine as an Agent Client Protocol agent over stdin/stdout, which is how Xcode and other ACP editors launch it. stdout carries only newline-delimited JSON-RPC; the banner, diagnostics and tracing go to stderr. There is no HTTP surface, and the command returns when the editor closes stdin. It takes only the common engine flags. See 20 · Xcode and other ACP editors.
oceancode workflow
oceancode workflow run <name|path> [--args <json>] [--budget <n>] [--validate-only] [-m <provider/model>]
oceancode workflow runs [--all] [--directory <dir>]
oceancode workflow pause|resume|stop <run_id> [--budget <n>]
run starts a host-run Rhai workflow and prints each workflow_updated body to stdout as one JSON line.
The last line is the terminating body, and the command exits with the workflow/goal code above. Put the
name or path first, then the flags. runs lists the persisted runs for the current project, newest
first; it takes no --db or --verbose. pause, resume and stop (run id first) only act on a run
that is live in the engine that same command boots.
| Flag | Sub-subcommands | Type | Default | Meaning |
|---|---|---|---|---|
--args |
run |
string | {} |
JSON object bound to the workflow's args global. |
--budget |
run, control verbs |
string | from config | run: absolute bound on cumulative child-agent calls, 1..=1024. resume: a higher call bound that lifts a budget-limited run. |
--validate-only |
run |
bool | off | Metadata and compile check only; launches no agent. |
--model, -m |
run |
string | empty | provider/model for steps that name no model. There is no default provider. |
--all |
runs |
bool | off | List runs from every project. |
oceancode goal
oceancode goal "<objective>" [--budget <n>] [-m <provider/model>] [--db <path>] [--directory <dir>] [-v]
Launches the built-in goal workflow (Plan → Work → Verify) and prints each goal_updated body to stdout
as one JSON line. It exits with the workflow/goal code above. The objective must be the first
argument, with the flags after it. A missing objective, or a flag in first place, fails with
missing objective: usage: oceancode goal "<objective>" [--budget <n>] [--model <provider/model>].
| Flag | Type | Default | Meaning |
|---|---|---|---|
--budget |
string | from config | Absolute bound on cumulative child-agent calls for the goal, 1..=1024. |
--model, -m |
string | empty | Default model for every goal agent that names none. There is no default provider. |
oceancode ledger
oceancode ledger path [--db <path>] [--directory <dir>] <session-id>
oceancode ledger rebuild [--merge] [--yes] [--db <path>] [--directory <dir>] <session-id>
path prints the absolute ledger directory of the session. rebuild rebuilds the ledger mechanically and
prints rebuilt <n> turn(s) in <dir>. --merge then prints the number of merge chunks and asks
fold forward with model calls? [y/N] on stderr before folding them forward, which costs model calls.
The session id is read after the flags, so write the flags first; a flag placed after the id is not
applied. With the ledger disabled (ledger.enabled = false), rebuild fails.
| Flag | Type | Default | Meaning |
|---|---|---|---|
--merge |
bool | off | rebuild only: fold the merge backlog forward after the rebuild (model calls). |
--yes |
bool | off | rebuild only: skip the y/N confirmation for --merge. |
See 06 · Session ledger and export.
oceancode export
oceancode export (--md | --atif) [--out <path>|-] [--db <path>] [--directory <dir>] <session-id>
Writes a whole session as one Markdown document or as an ATIF-v1.8 trajectory (JSON). Pass exactly one of
--md or --atif. As with ledger, the session id is read after the flags, so write the flags first.
| Flag | Type | Default | Meaning |
|---|---|---|---|
--md |
bool | off | Write the session as one Markdown document. |
--atif |
bool | off | Write the session as an ATIF-v1.8 trajectory (JSON). |
--out |
string | - |
Output path; - writes to stdout. |
See 06 · Session ledger and export.
oceancode login
oceancode login [--provider claude|copilot|codex] [--no-browser]
Logs in to a provider and stores the credential in the data dir's auth.json. claude (the default;
also anthropic, claude-pro, claude-max) runs the Claude Pro/Max OAuth flow: it prints the authorize
URL and reads the pasted code from stdin. copilot (github-copilot) and codex (openai) run the
OAuth 2.0 device-code flow and store the credential under github-copilot or openai.
| Flag | Type | Default | Meaning |
|---|---|---|---|
--provider |
string | claude |
Provider to log in to: claude, copilot or codex. |
--no-browser |
bool | off | Print the authorize URL only; do not try to open a browser. |
A --features platform build adds --provider ocean (dashboard sign-in that stores the Ocean account
key) and --sso-token <token> (stores a gateway SSO session token). See
15 · Platform mode.
oceancode logout
oceancode logout [anthropic|zai-coding-plan|ocean]
Removes one provider's stored credential from auth.json; the default is anthropic. Accepted names:
anthropic/claude, zai-coding-plan/zai/glm/zai-coding/glmcoding, and ocean/plungeai. If
nothing is stored, it prints No stored <provider> credential. and exits 0. Takes no flags.
oceancode key
oceancode key [<provider>] [<api-key>]
Stores an API key in auth.json (type api). The provider defaults to anthropic and takes the same
names as logout. With no key on the command line, the key is read from stdin when stdin or stdout is
not a terminal; in a terminal, a non-Anthropic provider prompts for it. With no key at all, the command
prints the credential status for anthropic and zai-coding-plan. A key without the expected sk-ant-
prefix triggers a warning but is still stored. Takes no flags.
oceancode auth
oceancode auth
An interactive picker: 1 Anthropic API key, 2 Claude subscription (runs login), 3 GLM coding plan
(runs key zai-coding-plan). Needs both stdin and stdout to be a terminal and fails otherwise. Takes no
flags.
oceancode claude
oceancode claude
Checks the claude-code provider before you use it. It reports the claude binary's path (or claude.bin
from config), its --version, and whether a Claude Code login is present. Advisory only: it exits 0
whatever it finds. Any argument fails with usage: oceancode claude.
oceancode inspect
oceancode inspect [--json]
Prints where every config key, skill, agent, command, MCP server and instruction file comes from, for the
current directory (there are no --dir/--home flags). Each human row is
kind id winning-layer source, and the last line is INSPECT_OK sources=<n>. With --json the JSON
array goes to stdout and the sentinel to stderr. Boots nothing. See
09 · Capabilities and plugins.
oceancode plugin
oceancode plugin [--root <home>] <subcommand>
--root (also --root=<home>) overrides the home that the stores live under
(<home>/.oceancode/plugins and <home>/.oceancode/marketplaces).
| Sub-subcommand | Prints |
|---|---|
marketplace add <spec> / update <n> / remove <n> |
PLUGIN marketplace-added|marketplace-updated|marketplace-removed <name> |
marketplace list |
<name> plugins=<n> per marketplace |
install <name-or-path> [--trust] [--marketplace <m>] |
PLUGIN installed <name> <version>, plus PLUGIN trusted <name> with --trust |
list |
<name> <version> enabled=<bool> trusted=<bool> per plugin |
uninstall <n> / update <n> |
PLUGIN uninstalled <n> / PLUGIN updated <n> <version> |
enable <n> / disable <n> |
PLUGIN enabled|disabled <n> |
enable-all / disable-all |
PLUGIN enabled-all|disabled-all count=<n> |
trust <n> [--off] |
PLUGIN trusted|untrusted <n> |
validate <n> |
PLUGIN validated <n> ok=<bool>, then one - <problem> line per problem |
import --from claude-code [--trust] [--no-repo-trust] [--dry-run] [--home <h>] [--dir <d>] |
Ends with IMPORT_OK plugins=<n> marketplaces=<m> repo_trust=<bool> (IMPORT_DRY_RUN_OK … under --dry-run). import takes --home/--dir, not --root. |
See 09 · Capabilities and plugins.
oceancode mcp
oceancode mcp [--root <dir> | --dir <dir>] [--home <h>] <subcommand>
--root (or its alias --dir) selects the project directory where .mcp.json lives; the default is the
current directory. --home selects the home tier.
| Sub-subcommand | Behaviour |
|---|---|
add <name> --command <cmd> [--arg <a>]… [--env K=V]… |
Writes a stdio server to <dir>/.mcp.json under mcpServers.<name>. Prints MCP added <name> stdio. |
add <name> --url <url> [--header K=V]… |
Writes a remote (type: http) server. Prints MCP added <name> remote. Pass exactly one of --command or --url. |
remove <name> |
Deletes the entry from .mcp.json. Prints MCP removed <name>. |
enable <name> / disable <name> |
Clears or sets the server's held-disabled bit in the trust store. Prints MCP_ENABLED/MCP_DISABLED <name>. |
list |
One row per discovered server: name stdio|remote tier trusted|untrusted enabled|disabled|shadowed source. |
login <name> / logout <name> |
login prints the OAuth callback URL and contacts no network. logout clears the stored OAuth entry. |
doctor |
Connects to each server with a 2-second budget and prints one status row per server (connected, needs-auth, failed, disabled, untrusted). |
See 11 · MCP servers.
oceancode skill
oceancode skill [--root <dir>] [--dir <d>] [--home <h>] <subcommand>
| Sub-subcommand | Behaviour |
|---|---|
create <name> [--description <text>] |
Writes <root>/.oceancode/skills/<name>/SKILL.md (--root defaults to the current directory) and prints SKILL created <path>. Refuses to overwrite an existing skill. |
list [--json] |
One row per discovered skill: id tier trusted|untrusted state description. state is enabled or the reason the skill is disabled. |
enable <id> / disable <id> |
Flips one skill's user-tier enable bit. <id> is a stable id, or a bare name that matches exactly one skill. Prints SKILL enabled|disabled <id>. |
enable-all / disable-all |
Prints SKILL enabled-all / SKILL disabled-all count=<n>. |
See 09 · Capabilities and plugins.
oceancode trust
oceancode trust show | repo on|off | mcp <name> on|off|clear [--dir <d>] [--home <h>]
Reads and sets the per-project trust record in <home>/.oceancode/trust.json. show prints
repo=<bool>, then one mcp <name>=<bool> line per override and one disabled <name> line per held
server. Every subcommand ends with TRUST_OK. repo grants or revokes repo-tier execution (repo hooks
and .mcp.json servers). mcp sets a per-server override that takes precedence over repo, or clears
it. See 11 · MCP servers.
oceancode hooks
oceancode hooks [--dir <d>] [--home <h>] list [--json]
oceancode hooks add <Event> --command <cmd> [--matcher <re>] [--timeout <s>] [--scope user|project|local]
oceancode hooks remove <Event> --command <cmd> [--matcher <re>] [--scope user|project|local]
list prints every hook source file the boot loader reads: one line per source with its tier, its trust,
whether it is loaded (or held, and why) and its path, followed by its handlers. The output ends with
HOOKS_OK sources=<n> handlers=<m> loaded=<k>. --json prints {"repo_trusted", "sources"} and no
sentinel. add and remove edit Claude Code's settings.json for the chosen scope (default project)
and print HOOK_ADDED <path> or HOOK_REMOVED <path>. Removing a handler that does not exist fails with
hooks remove: no such handler. See 10 · Hooks.
oceancode jev
oceancode jev status [--json]
oceancode jev probe [--record --point <p> --case <id> --request <req.json> --out <fixture.json> [--expect <json>]]
oceancode jev labels [--implicit | --unlabelled [--point <p>]] [--root <ledger dir>] [--out <file>]
oceancode jev questions --json
oceancode jev verdicts --in <cases.jsonl>
Inspects the Jev decision layer, read from the current directory and the environment (no --dir/--home
flags). status prints one point mode endpoint-host line per decision point; --json prints a document
instead, with JEV_STATUS_EMITTED on stderr. probe makes one live call to Jev and prints JEV_PROBE_OK
or JEV_PROBE_FAIL <why>; probe --record records a fixture. labels exports labelled decisions
(JEV_LABELS_OK added=<n> total=<m>); --unlabelled requires --out. questions --json and
verdicts --in print the decision points' question sets and policy verdicts for the bench. Only probe
calls Jev. See 13 · Jev decisions.
oceancode doctor
oceancode doctor [--storage]
Reports orphaned sessions (registry entries whose process died without a clean exit) and captured panic
reports found under the data dir. --storage adds the storage report: the sizes of oceancode.db and its
WAL, the session count, and the ledger directory's size and oldest session age. Read-only; any other
argument is ignored. See 18 · Operations.
oceancode migrate
oceancode migrate [--db <path>]
Opens the database named by --db (default oceancode.db), migrates it to the current schema, prints
opened and migrated <path>, and exits.
oceancode update
oceancode update [--self-test]
Updates itself to the newest release on the configured channel; OC_UPDATE_CHANNEL (stable/beta)
overrides update.channel. Both oceancode and ocean-pager are verified first, swapped atomically, and
rolled back together if a health check fails. Prints UPDATE_APPLIED from=<old> to=<new>, or
oceancode <v> is already the newest <channel> release. --self-test exercises the swap and rollback
offline on stub binaries and prints UPDATE_OK from=<old> to=<new> rollback_tested=1, then
UPDATE_SELF_TEST_OK binaries=2. No other argument is recognised. See
17 · Distribution and updates.
oceancode verify-release
oceancode verify-release <artifact> [--pubkey <base64>] [--sha256sums <path>]
Verifies the artifact's minisign <artifact>.minisig signature without any external tool. --pubkey
replaces the embedded release key; --sha256sums also checks the artifact's entry in that SHA256SUMS
file. Prints VERIFY_RELEASE_OK file=<name>. See 17 · Distribution and updates.
oceancode uninstall
oceancode uninstall [--purge] [--yes] [--dry-run]
Removes the installed oceancode binary from OC_INSTALL_DIR (default ~/.local/bin). --purge also
removes the app's config and data dirs, but never a project's .oceancode/oceancode.db. Unless --yes is
given, it lists every path and asks you to type oceancode to confirm. --dry-run prints
would remove: <path> lines and removes nothing. The last line is UNINSTALL_OK removed=<n>. See
17 · Distribution and updates.
See also
- 01 · Getting started — the three boot shapes.
- 02 · Configuration — every
OC_*variable these commands read. - 07 · Headless and server —
run,serve,workflow,goalin depth. - 09 · Capabilities and plugins, 10 · Hooks, 11 · MCP servers — the capability commands.
- 17 · Distribution and updates —
update,verify-release,uninstall.
Ocean pager reference
This page lists Ocean pager's slash commands, keyboard shortcuts, mode ring, config.toml settings, themes and
notification options. For how the pager starts, its environment variables and what is wired to the Ocean engine,
see Ocean pager.
Slash commands
Type / in the composer to open the menu. A prefix narrows the list, and Tab or Enter inserts the command.
Enabled skills appear in the same menu (19 · Ocean pager).
In the Needs column, session means an active session, fullscreen or minimal means that screen mode only (in the other mode the command tells you to switch), and dashboard means the Agent Dashboard. args means the command needs an argument.
Session and conversation
| Command | Aliases | What it does | Needs |
|---|---|---|---|
/new |
/clear |
Start a new session | — |
/resume |
— | Resume a previous session | — |
/fork [--worktree|--no-worktree] [directive] |
— | Branch the current session into a peer agent | session |
/rename <title> | --auto |
/title |
Rename the current session; --auto returns to the automatic title |
session, args |
/delete |
— | Delete this session | session |
/session-info |
— | Show session info | session |
/btw <question> |
— | Ask a side question without interrupting the turn | session, args |
/recap |
/summarize |
Summarize the session so far; listed only when session recap is available | session |
/rewind |
/undo |
Rewind to a previous turn | session |
/redo |
— | Re-apply the turn a rewind reverted (ocean/restore_code) |
session |
/compact [instructions] |
— | Compact the conversation history (ocean/compact_conversation) |
session |
/queue |
— | List the prompts queued behind the running turn | session |
/history |
— | Search prompt history | session |
/loop [interval] <prompt> |
— | Run a prompt on a recurring interval | args |
/plan [description] |
— | Enter plan mode; add/done/cancel/edit/remove edit the live todo table (_ocean/todos/set) |
session |
/view-plan |
/show-plan, /plan-view |
View the current plan | session |
/share |
— | Share this session via a public URL (ocean/share_session); hidden from the menu until enabled, but you can still type it |
session |
/unshare |
— | Revoke this session's share link (ocean/unshare_session) |
session |
Ocean engine controls
These commands use _ocean/* methods. For their argument forms, see
19 · Ocean session commands.
| Command | Aliases | What it does | Needs |
|---|---|---|---|
/tideline [now | <n>% | <tokens> | default] |
/shelve (hidden) |
Show or set the Session Ledger tideline (_ocean/ledger/status) |
session |
/budget [<usd> | off | subscription] |
— | Show or set this session's spend bound (_ocean/session/bounds) |
session |
/limits [steps <n|default> | time <dur|off>] |
— | Show or set the per-turn step and time bounds (_ocean/session/bounds) |
session |
/subagent [<provider/model> | default | off | clear] |
— | Show or set the sub-agent model (_ocean/subagent_model) |
session |
Model, context and output
| Command | Aliases | What it does | Needs |
|---|---|---|---|
/model <name> [effort] |
/m |
Switch the active model | session, args |
/effort <level> |
— | Set reasoning effort for the current model | session, args |
/connect <provider> |
— | Connect a model provider (API key or account login) | args |
/context |
— | View context usage | session |
/usage |
/cost |
View usage | — |
/copy [N] [file] |
— | Copy the last response (or the Nth) to the clipboard or a file | session |
/export [filename] |
— | Export the current conversation to a file or the clipboard | session |
/transcript |
/log |
View the conversation transcript in $PAGER |
session |
/find [text] |
— | Search the conversation scrollback | session, fullscreen |
/jump |
— | Jump to a turn in the conversation | session, fullscreen |
/expand |
— | Re-print the last collapsed block, fully expanded | session, minimal |
/edit-prompt |
— | Open an external editor for an empty prompt (to keep a draft, use the command palette) | session |
Extensions and workflows
| Command | Aliases | What it does | Needs |
|---|---|---|---|
/skills |
— | View skills | — |
/plugins |
/plugin |
View plugins | — |
/hooks |
— | View hooks | — |
/mcps |
— | Show MCP server status | — |
/marketplace |
— | View the marketplace | — |
/workflows |
— | Browse installed workflows | — |
/workflow |
— | Launch a saved workflow, list runs, or manage a run (pause, resume, stop, save) | session |
Settings and display
| Command | Aliases | What it does | Needs |
|---|---|---|---|
/settings |
/config, /preferences, /prefs |
Open the settings modal | — |
/theme [name] |
/t |
Switch the color theme | fullscreen |
/auto |
— | Toggle auto mode (a classifier approves safe tools); listed only when auto mode is available | — |
/always-approve |
— | Toggle always-approve (skips all permission prompts) | — |
/vim-mode |
— | Toggle vim-style scrollback keys | — |
/multiline |
/ml |
Toggle multiline input (swaps Enter and Shift+Enter) |
— |
/compact-mode |
— | Toggle compact UI (less padding) | — |
/timestamps |
— | Toggle message timestamps | — |
/timeline |
— | Toggle the timeline sidebar | fullscreen |
/notify [on | off] |
— | Toggle desktop notifications (Notifications) | — |
/toggle-mouse-reporting |
— | Toggle terminal mouse reporting, so the terminal's own click-drag copy works | — |
/minimal |
— | Switch this session to minimal (scrollback-native) mode | session, fullscreen |
/fullscreen |
/full |
Switch this session to fullscreen mode | session, minimal |
Navigation, help and maintenance
| Command | Aliases | What it does | Needs |
|---|---|---|---|
/dashboard |
/agents-dashboard, /sessions |
Open the Agent Dashboard; listed only when the dashboard is enabled | fullscreen |
/cd [path] |
— | Change the working directory for new agents | dashboard |
/home |
/welcome |
Return to the welcome screen | — |
/help |
— | Browse commands and keyboard shortcuts | — |
/docs [web|title] |
/howto, /guides |
Open the in-TUI how-to guides, or (web) the published developer guide, https://code.plungeai.com/docs, in the browser |
— |
/release-notes |
/changelog |
View release notes for the current version | — |
/doctor [fix [FIX]] |
/terminal-setup, /terminal-check, /terminal-info |
Check this session and show the fixes available | session |
/quit |
/exit |
Quit Ocean Code (asks first) | — |
/debug [scroll|fps|log] |
— | Toggle debug overlays; listed only on debug builds | — |
/scroll-debug |
— | Toggle the scroll-diagnostics HUD; never listed | — |
Keyboard
The defaults live in a single registry. Keys apply where the Where column says: prompt when the composer has
focus, scrollback when the conversation has focus (Tab switches between them), agent anywhere on the agent
screen, and global everywhere. Ctrl+. (or Ctrl+X) opens the full cheatsheet.
Essentials
| Keys | Where | Action |
|---|---|---|
Enter |
prompt | Send |
Ctrl+Enter (alt Ctrl+I) |
prompt | Send now while a turn runs: cancels the current turn and sends the message next. In Apple Terminal it is Ctrl+O; in VS Code-family terminals, Ctrl+L |
Tab |
prompt / scrollback | Focus the scrollback / focus the prompt (i or Space also focus the prompt) |
Ctrl+C |
agent | Cancel the turn |
Shift+Tab |
prompt | Cycle the mode ring (Modes) |
Ctrl+O |
agent | Toggle always-approve |
Ctrl+P (alt ?) |
agent | Command palette: search every action and slash command by name |
Ctrl+. (alt Ctrl+X) |
agent | Keyboard shortcuts. Where Ctrl+. is unreliable (Windows, WSL, some terminals) the two keys swap |
Ctrl+M |
agent | Pick a model (when the prompt has focus, Ctrl+M toggles multiline instead) |
F2 (alt Ctrl+,, Cmd+,) |
agent | Open the settings modal |
Ctrl+N |
global | New session |
Ctrl+Q (alt Ctrl+D) |
global | Quit; press twice in quick succession. In VS Code's terminal, Ctrl+D only |
Prompt
| Keys | Where | Action |
|---|---|---|
Ctrl+M |
prompt | Toggle multiline |
Ctrl+S (alt Alt+S) |
prompt | Stash the draft; on an empty composer, pop it back. A new stash replaces the old one |
! |
prompt | Shell mode (type it on an empty prompt) |
Up |
prompt | Browse prompt history (empty prompt). /history opens the search panel |
Esc Esc |
prompt | With text and no turn running: clear the prompt (Ctrl+Z restores it) |
Esc Esc |
prompt / scrollback | With an empty prompt and at least one turn: open the rewind picker |
Esc |
while a turn runs | Never cancels; a toast names the cancel key |
The double-press window is 800 ms (OC_PAGER_ESC_DOUBLE_PRESS_MS overrides it).
Panes and sessions
| Keys | Where | Action |
|---|---|---|
Ctrl+T |
agent | Toggle the todo pane |
Ctrl+G |
agent | Toggle the tasks pane. In minimal mode, edit the prompt in $VISUAL / $EDITOR (vi if neither is set) |
Ctrl+B |
agent | Send the running task to the background |
Ctrl+; (alt Ctrl+') |
agent | Toggle the prompt queue. In VS Code-family terminals on a local Mac, Ctrl+4 |
Ctrl+R |
agent | Open sessions |
Ctrl+L |
agent | Open extensions (unbound in VS Code-family terminals; use /plugins) |
Ctrl+\ (alt Ctrl+4) |
global | Open the Agent Dashboard |
Scrollback
Bare-letter keys in this table work only when vim_mode is on (/vim-mode). Arrows,
PgUp/PgDn, Enter, Tab, ? and every Ctrl chord always work.
| Keys | Action |
|---|---|
j / k (Down / Up) |
Select the next / previous entry |
L / H (Shift+Right / Shift+Left) |
Next / previous turn |
J / K |
Jump to the next / previous turn at the top of the viewport |
g / G |
Go to the top / bottom |
Ctrl+K / Ctrl+J |
Scroll up / down one line |
Ctrl+U / Ctrl+D |
Scroll up / down half a page (in VS Code, D scrolls down) |
PgUp / PgDn |
Scroll one page |
h / l (Left / Right) |
Collapse / expand the selected entry |
e / E |
Expand or collapse the entry / every entry |
Ctrl+E |
Toggle all thinking blocks |
r |
Toggle raw markdown |
y / Y |
Copy the content / the command or path |
Enter (alt Ctrl+F) |
Open the entry in the viewer |
o / O |
Next / previous link |
x |
Kill the selected background task |
Ctrl+R |
Toggle mouse reporting, only when [ui] mouse_reporting_toggle is on (it then replaces Open sessions while the scrollback has focus) |
Agent Dashboard
| Keys | Action |
|---|---|
Down / Up (j / k with vim mode) |
Select the next / previous row |
Shift+Down / Shift+Up |
Move the agent down / up |
Shift+Tab |
Cycle the dispatch mode |
Ctrl+T |
Pin / unpin the agent |
Ctrl+R |
Rename the agent |
Ctrl+X |
Stop / delete the agent |
Ctrl+O |
Toggle always-approve for the selected agent |
Ctrl+G |
Toggle row grouping |
Ctrl+L |
Change the working directory for new agents |
Ctrl+W |
Toggle worktree mode for new agents |
Ctrl+. (or ?) |
Show the shortcuts overlay |
Esc |
Close the dashboard |
Ctrl+\ |
(inside an attached session) Back to the dashboard |
Ctrl+[ / Ctrl+] |
(inside an attached session) Previous / next session |
Ctrl+X |
(inside an attached session) Stop the agent and go back to the dashboard |
Mouse
Click a pane to focus it. The wheel and trackpad scroll the pane (see the scroll_* settings), and you can click or
drag the scrollbar. In-app selection and double-click follow keep_text_selection; hold
Shift while dragging to use the terminal's own selection. Queued prompts have [Interject], [edit] and [cancel]
buttons. Double-clicking a [Pasted: N lines] chip expands it into editable text.
Modes (Shift+Tab ring)
Shift+Tab cycles Build → Auto → Plan → Ask → Debug → Build. Build, Plan, Ask and Debug are the engine's agents;
Auto is a permission stop that keeps the Build agent and sets the auto permission mode. The prompt border shows the
current mode in its color:
| Mode | Border |
|---|---|
build |
blue |
auto |
yellow |
plan |
violet |
ask |
green |
debug |
orange |
always-approve |
red |
The ring skips the Auto stop while always-approve is on, or when auto mode is not available. Leaving Auto writes
permission_mode = "ask" back. What each permission mode allows is in
19 · Shift+Tab ring and permission modes.
Settings (config.toml)
The settings modal (/settings, F2) edits these settings. Unless the Key column gives another table, each is
written to the [ui] table of <pager home>/config.toml ($OC_PAGER_HOME). Restart means the change takes effect
on the next start. Rows in the modal's Hidden in minimal set still apply to the full TUI.
Appearance
| Key | Type | Default | Meaning |
|---|---|---|---|
theme |
enum | oceannight |
Color theme (Themes) |
auto_dark_theme |
enum | oceannight |
Theme used in system dark mode (only with theme = "auto") |
auto_light_theme |
enum | oceanday |
Theme used in system light mode (only with theme = "auto") |
screen_mode |
fullscreen | minimal |
fullscreen |
How plain ocean-pager opens next time. Restart. /minimal and /fullscreen switch only the current session |
compact_mode |
bool | false |
Less padding around messages. Turns on automatically at 20 terminal rows or fewer |
show_timestamps |
bool | true |
Clock time next to user messages and agent responses |
show_timeline |
bool | false |
A per-turn tick rail in place of the scrollbar; hover to preview, click to jump |
page_flip_on_send |
bool | true |
Scroll a sent prompt to the top of the screen so the response starts on a fresh page |
simple_mode |
bool | true |
Plain readline-style input in the prompt instead of vim keys (vim input is experimental) |
vim_mode |
bool | false |
Vim keys for navigating the scrollback; the prompt is unaffected |
render_mermaid |
auto | on | off |
auto |
auto/on show a clickable row that opens the rendered diagram; off shows the raw source |
max_thoughts_width |
int 40–500 | 120 |
Column width of the thoughts panel |
show_thinking_blocks |
bool | true |
Show thinking blocks while streaming |
group_tool_verbs |
bool | true |
Fold consecutive read/search/list tool calls and subagent rows into one summary row |
collapsed_edit_blocks |
bool | false |
Show edits as one-line +N/-M summaries and merge back-to-back edits to the same file |
[ui.display_refresh] auto_cadence_enabled |
bool | true |
Match the display's refresh rate when streaming and scrolling; off keeps about 60 Hz. Restart |
respect_manual_folds |
bool | false |
Keep manually folded blocks as they are while streaming (experimental). Written to pager.toml under [scrollback.scroll], not config.toml |
Mouse
| Key | Type | Default | Meaning |
|---|---|---|---|
scroll_speed |
int 1–100 | 50 |
Wheel and trackpad speed multiplier |
scroll_mode |
auto | wheel | trackpad |
auto |
Force wheel or trackpad behavior when detection misreads your device |
scroll_lines |
int 1–10 | 3 |
Lines per scroll tick; until you set it, each terminal's own profile applies |
invert_scroll |
bool | false |
Reverse the vertical scroll direction |
keep_text_selection |
flash | hold | word_select |
flash |
flash clears the selection after mouse-up, hold keeps it until Esc, a click or a scroll, and word_select makes a double-click select a word and a triple-click a paragraph |
Editor, agent and models
| Key | Type | Default | Meaning |
|---|---|---|---|
combine_queued_prompts |
bool | false |
Merge consecutive plain follow-ups into one model turn |
confirm_before_rewind |
bool | true |
Ask before rewinding the conversation |
permission_mode |
default | ask | auto | always-approve |
ask |
The permission mode; the Shift+Tab ring and Ctrl+O write it too |
remember_tool_approvals |
bool | true |
Offer "Always allow" options on permission prompts. Restart |
default_selected_permission |
always_allow_all_sessions | allow_command_always | allow_once | reject |
always_allow_all_sessions |
The option the cursor starts on in a permission prompt |
[toolset.ask_user_question] timeout_enabled |
bool | true |
The ask_user_question tool times out instead of waiting forever. Restart |
[models] default |
model | (none) | The model for new sessions; changing it also switches the active session |
fork_secondary_model |
model | (none) | The model for the secondary agent when you fork; clearing it restores the built-in default |
hunk_tracker_mode |
off | agent_only | all_dirty |
off |
Which file changes are tracked as hunks; off also turns off line-count stats. Restart |
[cli] show_tips |
bool | true |
The tip-of-the-day banner on startup. Restart |
[cli] auto_update |
bool | true |
Download pager updates on startup. Restart. When oceancode launches the pager, auto-update is always off (19) |
[ui.contextual_hints] holds the per-hint switches. All default to true, and the modal shows them as one
Show contextual hints group: undo (Ctrl+Z restores a cleared prompt), plan_mode (suggests plan mode for a
planning request), image_input (offers to paste a clipboard image), send_now (empty-prompt Enter sends the top
queued item), small_screen (suggests /compact-mode), word_select, export_copy (points out /copy and
/export) and ssh_wrap (a /doctor tip over SSH).
Two modal rows are not saved. multiline_mode (default false) resets each session, and plan_mode (off/on,
default off) is per session.
Keys outside the modal
| Key | Type | Default | Meaning |
|---|---|---|---|
[ui] require_plan_approval |
bool | false |
The plan viewer always asks before leaving plan mode |
[ui] mouse_reporting_toggle |
bool | false |
Bind Ctrl+R in the scrollback to toggle mouse reporting (OC_PAGER_MOUSE_REPORTING_TOGGLE overrides it) |
[cli] session_picker_grouped |
bool | on | Group the session picker |
[dashboard] enabled |
bool | true |
Enable /dashboard (OC_PAGER_AGENT_DASHBOARD=0 turns it off) |
Themes
| Name | Display name | Notes |
|---|---|---|
oceannight |
Ocean Night | Default. A neutral dark theme with a magenta accent. Aliases ocean-night, dark |
oceanday |
Ocean Day | A light theme. Aliases ocean-day, light, day |
tokyonight |
Tokyo Night | Needs truecolor. Aliases tokyo-night, tokyo |
rosepine-moon |
Rose Pine Moon | Needs truecolor. Aliases rosepine, rose-pine, rose-pine-moon |
oscura-midnight |
Oscura Midnight | Needs truecolor. Alias oscura |
terminal |
Terminal | Uses the terminal's own background and text colors. Offered only when the terminal theme is enabled (OC_PAGER_TERMINAL_THEME). Aliases terminal-default, transparent, native |
auto |
Auto | Follows the system's dark/light appearance and picks auto_dark_theme or auto_light_theme. Alias system |
Names are case-insensitive. On a terminal without truecolor, the themes that need it are not offered. The pre-rename
keys groknight, grok-night, grokday and grok-day still load, for one release. OC_PAGER_THEME forces a theme
without editing the config.
Notifications
Desktop notifications, the tab title and the progress indicator are set in [ui.notifications]:
| Key | Default | Meaning |
|---|---|---|
method |
auto |
auto, osc9, osc99, osc777, bel or none |
condition |
unfocused |
unfocused (only once the terminal has lost focus), always or never |
idle_threshold_secs |
3 |
How many seconds the terminal must be unfocused before a notification fires |
events |
["turn_complete", "approval_required"] |
Also available: session_ready, task_complete, agent_error |
sleep_prevention |
true |
Keep the machine from idle-sleeping during a turn (macOS IOKit assertion, Linux systemd-inhibit) |
progress_bar |
true |
Show a progress indicator in the tab (OSC 9;4) |
session_recap |
true |
Show an automatic recap when you come back after being away |
session_recap_threshold_secs |
30 |
How many seconds unfocused count as "away" for the automatic recap |
[ui.notifications.title] enabled |
true |
Set the terminal/tab title to reflect the agent's state |
[ui.notifications.title] items |
["action-required", "spinner", "activity", "session-name", "ocean"] |
Also available: cwd, model, turn-timer |
Protocol selection (method = "auto"). The pager picks the protocol from the terminal:
| Terminal | Protocol |
|---|---|
| iTerm2, WezTerm, Warp | OSC 9 |
| Kitty | OSC 99 |
| Ghostty, VTE, Terminator, foot | OSC 777 (titled "Ocean Code") |
| Zellij (any terminal), and every other terminal | BEL |
Under tmux the sequence is wrapped in DCS passthrough, so it reaches the outer terminal. Control characters are stripped from the text first. The progress indicator is drawn only on Ghostty, WezTerm and iTerm2 3.6 or later.
Custom commands. Each [[ui.notifications.hooks]] entry runs command through sh -c for its events. The
command gets OC_PAGER_EVENT, OC_PAGER_MESSAGE and OC_PAGER_SESSION_ID. only_unfocused defaults to true, and
timeout_secs defaults to 10 (minimum 1); a hook that runs past its timeout is killed.
[[ui.notifications.hooks]]
command = "terminal-notifier -title 'Ocean Code' -message \"$OC_PAGER_MESSAGE\""
events = ["turn_complete", "approval_required"]
/notify. /notify toggles desktop notifications, and /notify on or /notify off sets them. Off sets
method to none for the running pager. On restores the method you had before, or auto. It does not write
config.toml.
See also
- Ocean pager: the binary, the environment variables, and what is wired to the engine
- Permissions and sandbox: what each permission mode allows
- Session ledger and export: the tideline behind
/tideline - Plan mode and todos:
/planand the todo pane - Bounding a long run:
/budgetand/limits
HTTP API reference
oceancode serve runs the engine as a long-lived HTTP server, by default on http://127.0.0.1:4096 (--port and --host change it; the default pager shape takes --listen <host:port>). Every route below is relative to that base URL. HTTP basic auth is off until OC_SERVER_PASSWORD is set; then it covers every route, SSE included, with the username from OC_SERVER_USERNAME (default oceancode). A non-loopback bind refuses to start without the password. Events stream over SSE: /event serves the v1 frames, /api/event and /global/event the v2 payload-wrapped envelope. Flags and variables: Headless and server.
curl -u "oceancode:$OC_SERVER_PASSWORD" http://127.0.0.1:4096/api/health
v2 — /api
| Method | Path | Operation | What it does |
|---|---|---|---|
| GET | /api/agent |
v2.agent.list |
built-in + config agents. |
| GET | /api/command |
v2.command.list |
— |
| PATCH | /api/credential/{credentialID} |
v2.credential.update |
(relabel). The contract declares 204. |
| DELETE | /api/credential/{credentialID} |
v2.credential.remove |
The contract declares 204. |
| GET | /api/event |
— | The v2 GlobalEvent stream shared by GET /api/event (v2.event.subscribe) and GET /global/event (global.event). |
| GET | /api/fs/find |
v2.fs.find |
ranked recursive search. |
| GET | /api/fs/list |
v2.fs.list |
the direct children of a directory. |
| GET | /api/fs/read/{*path} |
v2.fs.read |
The wildcard path is the file to read; the body is the raw bytes under the detected mime. |
| GET | /api/health |
v2.health.get |
— |
| GET | /api/integration |
v2.integration.list |
— |
| GET | /api/integration/attempt/{attemptID} |
v2.integration.attempt.status |
An unknown attempt is a 404 NotFoundError. |
| DELETE | /api/integration/attempt/{attemptID} |
v2.integration.attempt.cancel |
The contract declares 204. |
| POST | /api/integration/attempt/{attemptID}/complete |
v2.integration.attempt.complete |
The contract declares 204. |
| GET | /api/integration/{integrationID} |
v2.integration.get |
— |
| POST | /api/integration/{integrationID}/connect/key |
v2.integration.connect.key |
The contract declares 204. |
| POST | /api/integration/{integrationID}/connect/oauth |
v2.integration.connect.oauth |
— |
| GET | /api/location |
v2.location.get |
The LocationInfo schema requires a resolved {directory, project} (project carries id + directory), so it is built via the shared helper rather than a bare {directory}. |
| GET | /api/model |
v2.model.list |
— |
| GET | /api/permission/request |
— | the pending requests wrapped in {location, data}. |
| GET | /api/permission/saved |
— | the always-allow rules as PermissionSavedInfos wrapped in {data}. |
| DELETE | /api/permission/saved/{id} |
— | drops a saved rule addressed by the composite "<permission>:<pattern>" id. |
| GET | /api/provider |
v2.provider.list |
— |
| GET | /api/provider/{providerID} |
v2.provider.get |
— |
| GET | /api/pty |
v2.pty.list |
— |
| POST | /api/pty |
v2.pty.create |
— |
| GET | /api/pty/{ptyID} |
v2.pty.get |
— |
| PUT | /api/pty/{ptyID} |
v2.pty.update |
— |
| DELETE | /api/pty/{ptyID} |
v2.pty.remove |
The v2 contract is 204. |
| GET | /api/pty/{ptyID}/connect |
v2.pty.connect |
(WebSocket upgrade). |
| POST | /api/pty/{ptyID}/connect-token |
v2.pty.connectToken |
— |
| GET | /api/question/request |
— | the {location, data} envelope over the request list. |
| GET | /api/reference |
v2.reference.list |
— |
| GET | /api/session |
v2.session.list |
— |
| POST | /api/session |
v2.session.create |
— |
| GET | /api/session/active |
v2.session.active |
The sessions with an in-flight coordinator drain, keyed by id. |
| GET | /api/session/{sessionID} |
v2.session.get |
— |
| POST | /api/session/{sessionID}/agent |
v2.session.switchAgent |
Publishes the durable agent-switched event the projector already handles. |
| POST | /api/session/{sessionID}/compact |
v2.session.compact |
Triggers a compaction turn via Runner::compact (the same summarize-then-replace path the runner's own overflow recovery uses). 204 on success; errors map through the shared session-error writer. |
| GET | /api/session/{sessionID}/context |
v2.session.context |
The active post-compaction context window, wrapped in {data:[…]}. |
| POST | /api/session/{sessionID}/interrupt |
v2.session.interrupt |
the v2 alias of the v1 session.abort semantics. |
| GET | /api/session/{sessionID}/message |
v2.session.messages |
— |
| GET | /api/session/{sessionID}/message/{messageID} |
v2.session.message |
— |
| POST | /api/session/{sessionID}/model |
v2.session.switchModel |
— |
| GET | /api/session/{sessionID}/permission |
— | the pending requests scoped to one session. |
| POST | /api/session/{sessionID}/permission |
— | evaluates the requested action/resources against the session's ruleset synchronously (no blocking wait) and returns {id, effect}, mirroring the v2 contract's non-interactive evaluation shape. |
| GET | /api/session/{sessionID}/permission/{requestID} |
— | a single pending request. |
| POST | /api/session/{sessionID}/permission/{requestID}/reply |
— | — |
| GET | /api/session/{sessionID}/permission/mode |
— | the session's mode + lockout. |
| POST | /api/session/{sessionID}/permission/mode |
— | sets the mode from { "mode": "acceptEdits" }; 400 on an unknown mode, 409 when bypass is locked. |
| POST | /api/session/{sessionID}/prompt |
v2.session.prompt |
Admits durable user input (the session.next.prompted event) then drives the coordinator to run the turn. The drain runs Runner::run, which streams the provider turn and projects the assistant message. |
| GET | /api/session/{sessionID}/question |
— | — |
| POST | /api/session/{sessionID}/question/{requestID}/reject |
— | TS validates the path params BEFORE the handler resolves the session, so a malformed requestID is a 400 even for a session that does not exist. |
| POST | /api/session/{sessionID}/question/{requestID}/reply |
— | — |
| POST | /api/session/{sessionID}/revert/clear |
— | the v2 alias of Store::unrevert. |
| POST | /api/session/{sessionID}/revert/commit |
— | The ported store has no durable "committed" state distinct from "staged" (unlike upstream's stage/commit split, which also touches the working tree), so commit is a no-op success once a bookmark is confirmed to exist — staging already persisted the revert. |
| POST | /api/session/{sessionID}/revert/stage |
— | Faithful to the v1 revert semantics (Store::revert stages a bookmark at messageID); files (a diff/file-list request) is accepted but the ported RevertState carries no diff/files projection, so those fields are omitted rather than fabricated. |
| POST | /api/session/{sessionID}/wait |
v2.session.wait |
TS's V2Session.wait is a stub: after confirming the session exists it unconditionally fails with Session.OperationUnavailableError, mapped to a 503 ServiceUnavailableError. Match the status and message exactly so a client cannot tell the servers apart — the existence check still produces 404 for an unknown id first. |
| POST | /api/session/{sessionID}/subagent/{childID}/message |
— | Steers (delivery:"steer") or queues ("queue", the default) text into the child's live turn, or resumes it when it has settled. Returns the delivery status. A missing child is a 404. |
| POST | /api/session/{sessionID}/subagent/{childID}/resume |
— | Re-enters a completed child with text on a fresh step budget and returns its new result. A missing child is a 404. |
| GET | /api/skill |
v2.skill.list |
— |
v1 — auth / catalog / config
| Method | Path | Operation | What it does |
|---|---|---|---|
| GET | /agent |
app.agents |
the v1 shape. |
| PUT | /auth/{providerID} |
auth.set |
persist a credential to auth.json. |
| DELETE | /auth/{providerID} |
auth.remove |
drop the stored credential. |
| GET | /command |
command.list |
the v1 shape. |
| GET | /config |
config.get |
the merged config. |
| PATCH | /config |
config.update |
Deep-merges the body over the project config document and persists it. The merged value is written back into Services so the cached read path (config.get, formatter.status, agent/command discovery) reflects the change. TS ConfigHttpApi.update returns ctx.payload — the SUBMITTED patch, not the merged config — so the request body is echoed. |
| GET | /config/providers |
config.providers |
TS config.providers returns providerSvc.list() — the RESOLVED, connected providers (those with a usable credential via auth.json, a stored DB credential, or a declared env key) — plus a per-provider default-model map. It is NOT the bare oceancode.json provider override block: the TUI's connect gate iterates this list, and an override-only answer left the gate permanently false. |
| GET | /event |
event.subscribe |
the v1 SSE stream ({id,type,properties} frames). Mirrors EventSubscribe. |
experimental
| Method | Path | Operation | What it does |
|---|---|---|---|
| GET | /experimental/capabilities |
experimental.capabilities.get |
Mirrors experimental.ts capabilities(): {backgroundSubagents: …}: on unless OC_EXPERIMENTAL_BACKGROUND_SUBAGENTS is 0/false — the same rule the task tool's background mode reads. |
| GET | /experimental/console |
experimental.console.get |
Mirrors experimental.ts getConsole(): {consoleManagedProviders, activeOrgName?, switchableOrgCount}. With no cloud accounts registered account.orgsByAccount() returns [] (account.ts:329 over an empty repo.list()), so switchableOrgCount is 0; consoleManagedProviders is [] (config.ts:589); activeOrgName is omitted. No 500 — the empty result is a success. |
| GET | /experimental/console/orgs |
experimental.console.listOrgs |
Mirrors experimental.ts listConsoleOrgs(): {orgs: [...]}. With no accounts, orgsByAccount() is [] so the flattened list is empty (experimental.ts:56). |
| POST | /experimental/console/switch |
experimental.console.switchOrg |
Mirrors experimental.ts switchConsole(): account.use(accountID, orgID) wrapped in a catch that maps any failure to a 400 BadRequest (experimental.ts:84). With no account registered use() fails, so this is the faithful 400. |
| POST | /experimental/control-plane/move-session |
experimental.controlPlane.moveSession |
Mirrors control-plane.ts moveSession(): 204 on success; failures map to MoveSessionError. MoveSession first loads the session (move-session.ts:77) — a missing one yields "Session not found: {id}" (control-plane.ts:33). The move itself is a local operation and is a no-op success when the destination is the session's current directory (move-session.ts:79). |
| POST | /experimental/project/{projectID}/copy |
v2.projectCopy.create |
— |
| DELETE | /experimental/project/{projectID}/copy |
v2.projectCopy.remove |
The contract declares 204. |
| POST | /experimental/project/{projectID}/copy/generate-name |
experimental.projectCopy.generateName |
The LLM naming path is not ported; a deterministic slug is returned, matching the {name} shape. |
| POST | /experimental/project/{projectID}/copy/refresh |
v2.projectCopy.refresh |
The contract declares 204. |
| GET | /experimental/resource |
experimental.resource.list |
Mirrors experimental.ts resource(): mcp.resources() -> Record<id, Resource>. The MCP manager bridges tools but does not enumerate resources, so a fresh instance returns the empty map (experimental.ts:169). |
| GET | /experimental/session |
experimental.session.list |
Mirrors experimental.ts session(): sessions.listGlobal(...) -> GlobalSession[]. The global cross-project aggregate (slug/summary/cost/tokens/project) is not ported; a fresh self-hosted instance's listGlobal yields [] (experimental.ts:134). |
| POST | /experimental/session/{sessionID}/background |
experimental.session.background |
Mirrors experimental.ts sessionBackground(): returns false immediately when the experimentalBackgroundSubagents runtime flag is off (experimental.ts:156). That flag is off by default. |
| GET | /experimental/tool |
tool.list |
every registered tool's id/description/parameters. |
| GET | /experimental/tool/ids |
tool.ids |
— |
| GET | /experimental/workspace |
experimental.workspace.list |
— |
| POST | /experimental/workspace |
experimental.workspace.create |
— |
| GET | /experimental/workspace/adapter |
experimental.workspace.adapter.list |
Mirrors workspace.ts adapters() -> listAdapters(projectID) (workspace.ts:16). The only built-in is the worktree adapter (control-plane/adapters/index.ts BUILTIN), reported with its name/description (adapters/worktree.ts:29-30). |
| GET | /experimental/workspace/status |
experimental.workspace.status |
Mirrors workspace.ts status(): [...connections.values()] filtered to the project's workspaces (workspace.ts:55, 833). With no active sync connections the map is empty. |
| POST | /experimental/workspace/sync-list |
experimental.workspace.syncList |
Mirrors workspace.ts syncList(): discover workspaces via the registered adapters and register any new ones, no body, 204 (workspace.ts:51, 743). The built-in worktree adapter discovers nothing without configured remotes, so this is a no-op 204. |
| POST | /experimental/workspace/warp |
experimental.workspace.warp |
Mirrors workspace.ts warp(): workspace.sessionWarp(...) where a missing workspace maps to a 404 NotFoundError (workspace.ts:81). With no synced workspaces the target is always absent. |
| DELETE | /experimental/workspace/{id} |
experimental.workspace.remove |
— |
| GET | /experimental/worktree |
worktree.list |
the copy worktree directories. |
| POST | /experimental/worktree |
worktree.create |
a linked worktree on a new branch under the repo of the request directory. |
| DELETE | /experimental/worktree |
worktree.remove |
INTENTIONAL, SAFER DIVERGENCE FROM TS, carried verbatim from Go: git worktree remove refuses to remove the repository's MAIN worktree and that refusal surfaces as a 400. The TS handler is permissive and reports 200 for the same request because its remove path does not guard the main worktree. The guard is kept — matching TS would mean performing (or claiming) a destructive removal of the primary checkout. The non-main path is identical (200) on all three servers. |
| POST | /experimental/worktree/reset |
worktree.reset |
hard-reset a linked worktree (the body's directory) back to the default branch of the repo identified by the request directory. |
files / search
| Method | Path | Operation | What it does |
|---|---|---|---|
| GET | /file |
file.list |
direct children as v1 FileNodes. |
| GET | /file/content |
file.read |
The TS v1 /file/content returns only {type, content} (no mimeType) and trims the decoded text; both are mirrored so the envelope matches byte-for-byte. |
| GET | /file/status |
file.status |
the working-tree git status mapped onto the v1 File shape. |
| GET | /find |
find.text |
ripgrep-style file-content search rooted at the request directory. pattern is the regexp; the results are the ripgrep match records the TS handler forwards verbatim. |
| GET | /find/file |
find.files |
ranked file-name search returning the matching paths. |
| GET | /find/symbol |
find.symbols |
The LSP client exposes hover/definition/diagnostics but not workspace/symbol, and no live client is retained, so symbol search has no backend; the endpoint returns an empty-but-valid array. |
| GET | /formatter |
formatter.status |
the per-formatter enabled/extensions status computed from config. |
global / instance
| Method | Path | Operation | What it does |
|---|---|---|---|
| GET | /global/config |
global.config.get |
— |
| PATCH | /global/config |
global.config.update |
TS GlobalHttpApi.configUpdate persists and returns only the submitted patch projection (an empty patch yields {} after provider/default stripping), not the full merged config. Echo the request body. |
| POST | /global/dispose |
global.dispose |
Mirrors global.ts dispose(): returns true after disposing. The server is a single long-lived process, so there is nothing to tear down per request (the OS process owns the lifecycle). |
| GET | /global/event |
— | The v2 GlobalEvent stream shared by GET /api/event (v2.event.subscribe) and GET /global/event (global.event). |
| GET | /global/health |
global.health |
Mirrors global.ts health(): {healthy: true, version}, plus the four fields this port adds so every running instance says what it is: model_format is always "markdown" — the only payload format the product renders since D9; the field stays because clients read it, but it is no longer configurable — ledger says whether the graph's Session Ledger hook is installed (ocean_app::Services::ledger) — leaf T48b / T52b — build is the binary's own build stamp (crate::server_build, leaf T65), and dropped_events is the bus's drop-oldest counter (ocean_events::Bus::dropped_events, long-runs plan Task 13 / L-12): live events evicted from a saturated subscriber queue since boot — a saturation signal, not an error count, and never reset. |
| POST | /global/upgrade |
global.upgrade |
Mirrors global.ts upgrade(): when the installation method is "unknown" it reports {success:false, error:"Unknown installation method"}. This build is not produced by a known installer. The contract models this as the failure variant of the 200 anyOf (the 400 is reserved for the BadRequest envelope), so it is returned with status 200. |
| POST | /instance/dispose |
instance.dispose |
Mirrors instance.ts dispose(): returns true. Single-process, so no per-request teardown. |
| POST | /log |
app.log |
write the structured log line then return true. |
lsp / mcp
| Method | Path | Operation | What it does |
|---|---|---|---|
| GET | /lsp |
lsp.status |
The registry resolves candidate servers but does not hold live clients (spawn is on demand), so there is no connected server to report; the endpoint returns an empty-but-valid array. |
| GET | /mcp |
mcp.status |
a map of server name -> status. |
| POST | /mcp |
mcp.add |
register a server at runtime and connect it, returning the full status snapshot. |
| POST | /mcp/{name}/auth |
mcp.auth.start |
begin the remote OAuth flow, returning the authorization URL the caller opens. |
| DELETE | /mcp/{name}/auth |
mcp.auth.remove |
The 200 body is {"success": true} (additionalProperties:false), not a bare boolean. |
| POST | /mcp/{name}/auth/authenticate |
mcp.auth.authenticate |
— |
| POST | /mcp/{name}/auth/callback |
mcp.auth.callback |
completes the remote OAuth flow with the code returned to the loopback callback and answers with the server's resulting status. |
| POST | /mcp/{name}/connect |
mcp.connect |
— |
| POST | /mcp/{name}/disconnect |
mcp.disconnect |
— |
| GET | /mcp/{name}/prompts |
— | the server's cached prompt list (P1-T38). |
| GET | /mcp/{name}/resources |
— | the server's advertised resources (P1-T38), via McpManager::list_resources → Client::list_resources. |
| GET | /mcp/{name}/tools |
— | the server's cached tool definitions (WP6d). |
| POST | /mcp/{name}/trust |
mcp.trust |
(GAP-06): persist the per-server trust decision to <home>/.oceancode/trust.json (keyed by the project dir) and apply it to the running manager, returning the refreshed GET /mcp map. |
| POST | /mcp/{name}/enable |
mcp.enable |
(GAP-06 parity): persist the enable/disable decision to <home>/.oceancode/trust.json's disabled_mcp (so the next boot honours it) and apply it live to the running manager, returning the refreshed GET /mcp map. An unknown server id is a 404. |
hooks (GAP-06)
| Method | Path | Operation | What it does |
|---|---|---|---|
| GET | /hooks |
— | the boot-time hook source report plus this project's repo trust bit (GAP-06). Read-only: the rows are whatever the boot loader (or the last reload) recorded. |
| POST | /hooks |
— | add a command hook to one of Claude Code's three writable settings files (GAP-06 parity), then reload the dispatcher and return the refreshed GET /hooks shape. An unknown event / empty command is a 400 (InvalidInput); a settings file that is not a JSON object is a 400 (InvalidData, never clobbered). The body is an ocean_app::hooks_boot::HookEdit. |
| POST | /hooks/reload |
— | re-run the boot hook loader (re-attaching the dispatcher) and return the refreshed report (GAP-06). |
| POST | /hooks/remove |
— | remove a hook from one of the three writable settings files (GAP-06 parity), then reload and return the refreshed GET /hooks shape. A path that is not one of the three writable scopes is a 400 (InvalidInput); a handler that is not present is a 404 (NotFound). The body is an ocean_app::hooks_boot::HookRemove. |
| POST | /trust/repo |
— | flip this project's repo-tier trust (GAP-06): persist the decision, release/hold the repo's .mcp.json servers and reload the hook dispatcher, then return the refreshed GET /hooks shape. A trust-store write error is a 400. |
the Claude Code settings mirror (2026-09-10)
| Method | Path | Operation | What it does |
|---|---|---|---|
| GET | /claude/settings |
— | the Claude Code settings mirror (model, effortLevel/modelSettings, ultracode, alwaysThinkingEnabled of the three tiered settings.json files) with the effective effort and the model it pins (ocean_app::claude_settings, 2026-09-10). Read-only: the rows are whatever boot or the last watcher reload recorded. |
path / permission
| Method | Path | Operation | What it does |
|---|---|---|---|
| GET | /path |
path.get |
The home/state/config values mirror the TS server's Global.Path.*, derived via the xdg-basedir port in ocean-config so they are byte-identical to the TS server on the same host/env (e.g. ~/.config/oceancode, not the macOS os.UserConfigDir() location). |
| GET | /permission |
— | the v1 alias, a bare array. |
| POST | /permission/{requestID}/reply |
— | the v1 alias, dispatched on requestID alone. |
plugin
| Method | Path | Operation | What it does |
|---|---|---|---|
| GET | /plugin |
— | the installed plugins with their enable/trust bits. |
| POST | /plugin/install |
— | install from source, returning {name, version, enabled, trusted}. |
| GET | /plugin/marketplace |
— | the added marketplaces with their plugin entries. |
| POST | /plugin/marketplace/add |
— | add a marketplace by spec. |
| DELETE | /plugin/marketplace/{name} |
— | remove a marketplace, returning the refreshed marketplace list (GAP-06 parity). A marketplace that is not present is a 404. |
| POST | /plugin/{name}/enable |
— | turn a plugin's loading on or off. |
| POST | /plugin/{name}/trust |
— | grant or revoke a plugin's execution trust. A missing/invalid body is a 400 (falsify_trust_route_requires_body), so trust is never granted by an empty request. |
| GET | /plugin/{name} |
— | the full detail of one installed plugin (GAP-06 parity). A name that is not installed is a 404. |
| DELETE | /plugin/{name} |
— | uninstall a plugin, returning the refreshed installed list (GAP-06 parity). A plugin that is not installed is a 404. |
project
| Method | Path | Operation | What it does |
|---|---|---|---|
| GET | /project |
project.list |
— |
| GET | /project/current |
project.current |
resolve the request directory. |
| POST | /project/git/init |
project.initGit |
initialize a repository in the request directory (idempotent). |
| PATCH | /project/{projectID} |
project.update |
— |
| GET | /project/{projectID}/directories |
project.directories |
— |
provider
| Method | Path | Operation | What it does |
|---|---|---|---|
| GET | /provider |
provider.list |
{all, default, connected}. |
| GET | /provider/auth |
provider.auth |
Mirrors provider.ts auth(): svc.methods() -> Record<providerID, Method[]>. No v1 auth-method plugins are ported, so this is the empty map a fresh instance returns. |
| POST | /provider/{providerID}/oauth/authorize |
provider.oauth.authorize |
Mirrors provider.ts authorize(): with no oauth method registered for the provider the TS service hits an undefined method lookup, which the handler maps to a generic BadRequest ProviderAuthError. The Claude Pro/Max subscription login is served elsewhere (the v2 integration endpoints and the oceancode login CLI), so a v1 client is never offered a method here. |
| POST | /provider/{providerID}/oauth/callback |
provider.oauth.callback |
Mirrors provider.ts callback(): with no pending oauth result the TS service returns OauthMissing. |
pty
| Method | Path | Operation | What it does |
|---|---|---|---|
| GET | /pty |
pty.list |
— |
| POST | /pty |
pty.create |
— |
| GET | /pty/shells |
pty.shells |
the available login shells. |
| GET | /pty/{ptyID} |
pty.get |
— |
| PUT | /pty/{ptyID} |
pty.update |
— |
| DELETE | /pty/{ptyID} |
pty.remove |
The v1 contract answers 200 true (unlike the v2 delete, which is 204). |
| GET | /pty/{ptyID}/connect |
pty.connect |
(WebSocket upgrade). |
| POST | /pty/{ptyID}/connect-token |
pty.connectToken |
— |
question
| Method | Path | Operation | What it does |
|---|---|---|---|
| GET | /question |
— | every pending question across sessions. |
| POST | /question/{requestID}/reject |
— | dismisses a pending question, failing the waiting Ask with a rejection. |
| POST | /question/{requestID}/reply |
— | resolves a pending question with the user's answers, unblocking the waiting Ask. |
session (v1)
| Method | Path | Operation | What it does |
|---|---|---|---|
| GET | /session |
session.list |
The v1 response is the bare array. |
| POST | /session |
session.create |
Maps the supported v1 body fields onto SessionStore::create; parentID, metadata and permission have no ported column and are dropped. |
| GET | /session/status |
— | the run status of every session, keyed by id. |
| GET | /session/{sessionID} |
session.get |
— |
| DELETE | /session/{sessionID} |
session.delete |
Removes the session, its children, message projection and event log. |
| PATCH | /session/{sessionID} |
session.update |
the v1 title patch. |
| POST | /session/{sessionID}/abort |
session.abort |
The TS handler is idempotent: it interrupts any running turn and returns true even for an unknown session id (no 404 pre-check). Mirrored. The interrupt goes through the runner, not the bare coordinator, so it also reaches the session's sub-agents — foreground through the token cascade, background through the runner's job index (long-runs plan Task 12, L-9). |
| GET | /session/{sessionID}/children |
— | the sub-sessions whose parentID is this session. The store has no parent filter, so this lists and filters in-process (the TS handler likewise scans). |
| POST | /session/{sessionID}/command |
— | resolves a registered command, substitutes its arguments, and runs it as a prompt turn. An unknown command maps to CommandNotFoundError (404). |
| GET | /session/{sessionID}/diff |
session.diff |
the structured diff for the session's working directory (git mode). |
| POST | /session/{sessionID}/fork |
— | copies the session into a new one (optionally truncated at messageID). |
| POST | /session/{sessionID}/init |
— | runs the project-analysis (/init) flow as a prompt turn. |
| GET | /session/{sessionID}/checkpoints |
— | the recorded checkpoints (id, reason, message bookmark, seq), oldest first. An unknown session lists nothing. |
| POST | /session/{sessionID}/undo |
— | restore the last pre-write state. With no earlier checkpoint it is a no-op with a nothing to undo body, never an error. |
| POST | /session/{sessionID}/redo |
— | re-apply the next checkpoint's files. Available only after an undo; otherwise a no-op with a nothing to redo body. |
| GET | /session/{sessionID}/checkpoints/{a}/diff/{b} |
— | a unified diff between two checkpoints (text/plain); the header names both short ids. An unknown session or checkpoint is a 404. |
| POST | /session/{sessionID}/checkpoints/revert-hunk |
— | restore one diff hunk of file to its pre-edit (checkpoint) bytes, leaving every other hunk. An unknown hunk id is a 404 (never a whole-file overwrite). |
| POST | /session/{sessionID}/checkpoints/apply-hunk |
— | re-apply one previously reverted hunk of file. |
| GET | /session/{sessionID}/message |
session.messages |
the v1 alias. |
| POST | /session/{sessionID}/message |
session.prompt |
the v1 prompt alias. |
| GET | /session/{sessionID}/message/{messageID} |
session.message |
— |
| DELETE | /session/{sessionID}/message/{messageID} |
session.deleteMessage |
— |
| DELETE | /session/{sessionID}/message/{messageID}/part/{partID} |
part.delete |
— |
| PATCH | /session/{sessionID}/message/{messageID}/part/{partID} |
part.update |
The body is a v1 Part (the full tagged-union shape). The flat ported model stores assistant content (text/reasoning/tool) inline rather than as addressable Part rows, so for those types the update is applied to the inline content; for every type the updated Part is returned with the path-derived ids, matching the v1 contract. |
| POST | /session/{sessionID}/permissions/{permissionID} |
— | the v1 session-scoped response. The {response} enum maps directly onto permission::Reply. |
| POST | /session/{sessionID}/prompt_async |
— | admits durably and returns immediately; the turn runs in the background. |
| POST | /session/{sessionID}/revert |
— | records a revert bookmark at the given message/part. |
| POST | /session/{sessionID}/share |
— | mints (or returns) the share row. |
| DELETE | /session/{sessionID}/share |
— | drops it. |
| POST | /session/{sessionID}/shell |
— | runs a shell command in the session's working directory, recording it as a durable shell message. |
| GET | /session/{sessionID}/subagent |
— | the session's effective sub-agent model (2026-09-16), so a remote TUI shows the identity row's sub-agents segment. The engine owns the whole precedence (ocean_app::Services::effective_subagent_model); this hands its answer back as {"model": ModelRef | null}. An unknown session is 404, like the sibling per-session GETs. |
| POST | /session/{sessionID}/summarize |
— | runs a summarization turn and reports whether a summary was produced. |
| GET | /session/{sessionID}/todo |
session.todo |
The ETag header is the table's entity tag; send it back as If-Match on PUT for a conditional edit. |
| PUT | /session/{sessionID}/todo |
— | the user's edit. Rows are matched by id or content; changed and new rows become user-authored; rows left out are removed. With If-Match set to a stale tag nothing is written and the answer is 412 carrying the current rows and tag. |
| POST | /session/{sessionID}/unrevert |
— | clears the revert bookmark. |
| GET | /skill |
app.skills |
the v1 bare array. |
sync
| Method | Path | Operation | What it does |
|---|---|---|---|
| POST | /sync/history |
sync.history.list |
Mirrors sync.ts history(): select every durable event from the event table, excluding the (aggregateID, seq) cursors in the payload, ordered by seq (sync.ts:72). The payload maps aggregateID -> lastSeq the caller already has. |
| POST | /sync/replay |
sync.replay |
Mirrors sync.ts replay(): replay the serialized events into the event store, return {sessionID: events[0].aggregateID} (sync.ts:34). TS's ReplayPayload declares directory: Schema.String and events: Schema.NonEmptyArray(ReplayEvent), so the HttpApi rejects a missing directory or an empty array with a 400 BEFORE the handler runs. |
| POST | /sync/start |
sync.start |
Mirrors sync.ts start(): fork startWorkspaceSyncing with Effect.ignore, return true (sync.ts:27). The fork is best-effort and its result is ignored, so the response is unconditionally true. |
| POST | /sync/steal |
sync.steal |
Mirrors sync.ts steal(): requires the instance workspaceID; without one it returns 400 BadRequest (sync.ts:62). A self-hosted instance has no workspace binding. The contract's 400 is the effect HttpApiError BadRequest ({_tag:"BadRequest"}), not the {name,data} envelope. |
tui
| Method | Path | Operation | What it does |
|---|---|---|---|
| POST | /tui/append-prompt |
tui.appendPrompt |
— |
| POST | /tui/clear-prompt |
tui.clearPrompt |
— |
| GET | /tui/control/next |
tui.control.next |
long-poll for the next queued TUI request (FIFO). Parks until a request is enqueued or the client disconnects. |
| POST | /tui/control/response |
tui.control.response |
enqueue the TUI's reply for whoever is waiting (fire-and-forget FIFO). |
| POST | /tui/execute-command |
tui.executeCommand |
— |
| POST | /tui/open-help |
tui.openHelp |
— |
| POST | /tui/open-models |
tui.openModels |
— |
| POST | /tui/open-sessions |
tui.openSessions |
— |
| POST | /tui/open-themes |
tui.openThemes |
— |
| POST | /tui/publish |
tui.publish |
dispatch a TUI event onto the request queue for a connected client to pull via control/next. |
| POST | /tui/select-session |
tui.selectSession |
— |
| POST | /tui/show-toast |
tui.showToast |
— |
| POST | /tui/submit-prompt |
tui.submitPrompt |
— |
vcs
| Method | Path | Operation | What it does |
|---|---|---|---|
| GET | /vcs |
vcs.get |
current/default branch info. |
| POST | /vcs/apply |
vcs.apply |
applies a unified patch to the working tree. The contract's 200 body is {applied: boolean}. |
| GET | /vcs/diff |
vcs.diff |
the structured per-file diff. |
| GET | /vcs/diff/raw |
vcs.diff.raw |
The contract's 200 body is a text/x-diff string (not JSON), so the diff is written verbatim with that content type. |
| GET | /vcs/status |
vcs.status |
the working-tree file status list. |
Generated from crates/ocean-server/src/router.rs — 218 operations.