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 reference

The Ocean coding agent: the oceancode engine (headless run, serve, goal and workflow; 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

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


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


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/.

The step list keeps working throughout — plan mode changes what the model may do, not how progress is tracked.

See also


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

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:

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


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:

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


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:

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


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:

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:

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:

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


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


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:

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


Permissions and sandbox

OceanCode decides what a tool may do in three layers, from softest to hardest:

  1. Permission rules and modes — an ask/allow/deny policy over each tool call.
  2. The shell and filesystem guards — a classifier that reasons about what a bash command or a file edit actually does, on top of the rules.
  3. 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).

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

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:

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.

See also


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

  1. 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 } }
    
  2. The project config — <project>/.oceancode/oceancode.jsonc or .json. The layer is on only when both sections say enabled: true, so a repository has to opt in:

    { "jev": { "enabled": true } }
    

    A project section may set only enabled and the seven mode keys. Its endpoint, 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.

  3. An endpoint the build can reach. The default build's endpoint is the direct TypeSafe API, https://api.typesafe.ai/v1/systemone, which needs OC_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 the ocean.headers map of the global config (the managed document or yours — never the project file's). Either build can use the other endpoint through jev.endpoint in the global config or OC_JEV_ENDPOINT. The key goes only to api.typesafe.ai, ocean.headers only to the worker, a redirect is never followed, and plain http is refused unless OC_JEV_ENDPOINT names a loopback host.

    The worker refuses a public-internet POST /systemone that 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's ocean.headers is ignored):

    { "ocean": { "headers": { "x-ocean-key": "<the worker key>" } } }
    

    Every request also names its decision point in an x-jev-point header (AI Gateway metadata on the worker's side) and the worker's own time comes back in Server-Timing.

  4. Check it. oceancode jev status should list the seven points in shadow with the endpoint host; oceancode jev probe makes one live call and prints JEV_PROBE_OK.

  5. Leave everything in shadow until the point has its own labels (see Promoting a point to act).

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:

  1. Run the point in shadow for at least a week of your own sessions.
  2. oceancode jev labels collects every decision you answered (see Reading decisions) into bench/jev/labels.json.
  3. The point's own run — node scripts/bench/jev-decisions.mjs --set bench/jev/labels.json --point <p> (live, OC_LIVE_CONFIRM=1) — must print JEV_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 for permission and 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 prints JEV_EVAL_RED reason=wilson.
  4. Then set that point's key to act in 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:

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.

Reading decisions

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


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 deprecated OCEAN_TUNNEL_TOKEN, then tunnel_token_file, then the inline tunnel_token). OCEAN_TUNNEL_TOKEN is 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 the X-Ocean-SSO-Token header (no Bearer prefix) on requests to the configured https gateway host only — never a direct-provider host, never over plaintext — and never on Authorization (the wallet bearer, your ozk_ key, stays intact). Read at runtime, never persisted. Server-side reader is a pending platform handoff: until the inference gateway reads X-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_capability tool — 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.
  • capability permission — the permission family that governs load_capability. It asks by default in the read-only agent families (ask, explore, security-review) and is allowed for the build, plan and general families.
  • Registry skills — platform mode can also discover skills straight from the registry with no local files; they surface with the provenance platform:<id> (tier platform, bound tool id skill_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 from todo.updated events, one summary line per event that carried user-authored plan rows (the model's own edits contribute nothing).
  • ## Capabilities used — regenerated from pack.manifest events, 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 exactly oceancode and ocean-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 update downloads this and stages it directly as oceancode.new for 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 update swaps it together with oceancode (the engine runs inside the pager process, so the pair must match). If either fails its --version health check after the swap, both are rolled back to their <name>.bak. A release without this asset updates oceancode alone 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.

  1. Bump the version. bash scripts/release/bump-version.sh <X.Y.Z> — moves the ## Unreleased CHANGELOG section under the new dated heading and updates VERSION. This release is a minor (new subcommands + platforms): 2.5.3 → 2.6.0, bumped 2026-09-15. Since then every scripts/release/build-release.sh run bumps the patch on its own (2.6.01, 2.6.02, …): a build number is never reused, and the check.sh lanes build with NO_BUMP=1.
  2. CHANGELOG dated. Confirm the moved section reads correctly under ## 2.6.0 — <date>.
  3. User-guide tables current. node scripts/gates/check-user-guide.mjs → USER_GUIDE_OK … missing=0 (every OC_* var and subcommand has a row) and node scripts/gates/check-changelog.mjs → CHANGELOG_OK.
  4. Whole-tree green on three OSes. ./check.sh → ALL CHECKS PASSED on macOS, Linux and Windows in CI.
  5. Build the five targets. bash scripts/release/build-all.sh (or the CI release job's matrix) → BUILD_RELEASE_OK targets=5. Per target publish both the bare oceancode-<os>-<arch> (Windows .exe) and the oceancode-<os>-<arch>.tar.gz, using the <os>-<arch> tokens in the mapping table above — no version in any filename.
  6. Per-artifact budgets. RELEASE=1 ./check.sh (or the CI native legs) runs size-gate.sh per artifact (≤ 26 MiB → SIZE_GATE_OK) and boot-bench.sh (< 36 ms → BOOT_BENCH_OK).
  7. Sign. bash scripts/sign/sign-artifacts.sh (minisign → SIGN_OK artifacts=<n>); where certificates exist, scripts/sign/codesign-macos.sh (Apple notarization) and scripts/sign/authenticode-windows.ps1 (Windows Authenticode).
  8. Checksums. bash scripts/release/gen-sha256sums.sh dist → SHA256SUMS_OK count=<n> over the packaged assets.
  9. Draft the GitHub Release (owner): upload the assets, SHA256SUMS and each .minisig.
  10. Homebrew tap (owner): mirror packaging/homebrew/oceancode.rb to the tap with the real sha256s / bottle digests.

Owner-only steps (agents never do these)

  • Replace verify_release::EMBEDDED_PUBKEY and the installers' embedded minisign pubkey with the real release key.
  • Provision the CI secret MINISIGN_SECRET_KEY and install minisign on 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


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

  1. Build the pager: bash scripts/release/build-pager.sh (or cd ocean-pager && cargo build --release -p ocean-pager-bin --locked) → target/release/ocean-pager, the binary oceancode launches.
  2. A plain debug build lands in target/debug/ocean-pager and is never launched. To run a pager from elsewhere, set OC_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."

  1. 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.

  2. Xcode › Settings › Intelligence, under Agents click Add an Agent… and fill the sheet:

    Field Value
    Name Ocean Code
    Executable the path from step 1
    Interpreter leave blank (oceancode is a native binary, not a script)
    Arguments acp — one argument per row
    Environment Variables none needed (see Credentials)

    Click Add. Ocean Code now appears under Agents.

  3. 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


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:

  • -name and --name are the same flag. A value is written --name value or --name=value. Single-letter flags cannot be bundled: -vc is one flag named vc.
  • 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/--help prints Usage 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.

See 07 · Headless and server.

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.

See 07 · Headless and server.

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.

See 07 · Headless and server.

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


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
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


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.
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.

Generated 2026-10-17 from apps/ocean-coding-agent-rust/docs/user-guide/ · code.plungeai.com/docs