> ## Documentation Index
> Fetch the complete documentation index at: https://www.plungeai.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

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


`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 → project file → global file → the managed file
([12](/oceancode/permissions-and-sandbox#managed-enterprise-config)) (`crates/ocean-app/src/services.rs:3000`). The global
file is layered over the project file, so a key set in both takes the global value; the `jev` section is the one
exception, read per file ([13](/oceancode/jev-decisions#modes)). 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`. |
| `effort` | string | none | Reasoning effort a new session's `model` runs at, as the model's own level name (`xhigh`, `high`, `on`, `off`, `minimal`, …; `/effort` lists the levels the current model has). Any level without whitespace loads; one the model does not list sends no effort field. Wins over `~/.claude/settings.json`'s effort when both name the same model. An empty value never overrides an earlier layer. |
| `default_agent` | string | `build` | Agent a new session starts in. |
| `mode` | string | `model` | The work mode: a diagram id (lowercase letters, digits and dashes, starting with a letter), such as `delegate` or `race`. `model` lets the model decide, as before work modes. An id no diagram has runs `model`, with a log line. A session's own pick (`/mode`, `F4`) and `agents.<id>.mode` come first. See [23 · Work modes](/oceancode/work-modes). |
| `test_command` | string | none | The project's test command, run by a work mode's `tests` gate that names no command (Race needs it). It runs through the `bash` permission rules. |
| `agents` | object | none | Per-agent overrides, keyed by agent name. Each agent may set `model`, `prompt`, `temperature`, `disable`, `tool_profile` (`default` / `codex` — `apply_patch` is the sole mutation tool / `hashline` — the content-anchored `hashline` tool replaces `edit`), and `effort`. `model` (`provider/model`) and `effort` (a level name) are the agent's defaults: when a session switches to the agent (Shift+Tab, or a new session started in it) it takes that model, when the catalog lists it and its provider is connected, and that effort, clamped to the levels the model offers (the highest offered level at or below it, else the lowest; none on a model without levels). An empty field keeps the session's own. An agent that sets both `model` and `effort` also makes a `task` call naming that model (`model: "provider/id"`) run at that effort, over a `modelSettings` row in `~/.claude/settings.json` for the same model. The pager edits the rows for `build`, `plan`, `ask` and `debug` under F2 › Agent defaults ([22](/oceancode/pager-reference)). |
| `agents.<id>.mode` | string | none | The work mode a session of that agent runs when the session picked none itself; the picker's `d` writes it. This is a diagram id, not the `mode:` line of an agent's markdown file (`primary` / `subagent` / `all`). See [23](/oceancode/work-modes). |
| `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](/oceancode/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](/oceancode/jev-decisions)). |
| `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](/oceancode/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](/oceancode/permissions-and-sandbox#the-shell-guard) and [17 · Distribution and updates](/oceancode/distribution). |
| `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](/oceancode/permissions-and-sandbox#child-process-environment). |
| `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](/oceancode/permissions-and-sandbox#the-sandbox). |
| `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](/oceancode/permissions-and-sandbox#reference-aliases). |
| `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](/oceancode/session-ledger-and-export). |
| `retention.days` | number | `7` (`0..=3650`) | How many days an idle session is kept: past it the session (with its whole sub-agent tree) is deleted from `oceancode.db` and its Session Ledger directory goes with it. `0` = off. One setting for the database and the ledger; nothing is deleted for 24 hours after the sweep is first armed. `/retention` reads and sets it. See [18 · Operations](/oceancode/operations#retention-and-vacuum). |
| `database.retention_days` | number | none | Legacy fallback: read only when no config layer has a `retention` section, and only when > 0. (`database.vacuum` is no longer read.) |
| `ledger.retention_days` | number | none | Ignored since `retention.days`; a config that still sets it loads with one warning at boot. |
| `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. Since 2026-10-21 a set `subagent.model` is the default ahead of the fleet router's pin. |
| `subagent.effort` | string | none | Reasoning effort sub-agents run at — the model's own level name, as for `effort`. Applies to `subagent.model`, or to the router's sub-agent model when `subagent.model` is unset; when both name the same model and this is unset, the `~/.claude/settings.json` effort is kept. |
| `subagent.force` | bool | `false` | `true` makes the `task` tool ignore a per-call `model:` and run the sub-agent default — the same as `CLAUDE_CODE_SUBAGENT_MODEL_FORCE=1` in `~/.claude/settings.json` (either one forces). |
| `subagent.max_concurrent` | number | none (`0..=64`) | Sub-agents that may run at once in a session's whole tree. Unset: the work mode's own limit, else none. A set value wins in every mode, Model included; `0` means no sub-agents. See [23](/oceancode/work-modes#limits). |
| `subagent.max_per_turn` | number | none (`1..=1000`) | Sub-agents one turn may start before you are asked whether to go on. Unset: the work mode's own limit, else none. |
| `subagent.max_depth` | number | `4` (`1..=4`) | How deep sub-agents may nest. Unset: the work mode's own limit, else 4. |
| `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. Since work modes, `always` with no `mode` set is read as `mode: "goal"` ([23](/oceancode/work-modes)); a set `mode` wins. |
| `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](/oceancode/capabilities#enabling-and-disabling-skills). |
| `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](/oceancode/capabilities#enabling-and-disabling-skills)) or a bare name (matching every copy of that name). |
| `skills.roster` | string | `names` | How the system prompt lists skills: `names` (one line of names, no descriptions — the default since 2026-10-21) or `full` (an `<available_skills>` element with name, description and location per skill — the default before). The skill tool loads any enabled skill by name either way. |
| `jev` | object | on with a key | The TypeSafe Jev decision layer, on whenever a TypeSafe key resolves (`oceancode key typesafe <key>`, else `OC_TYPESAFE_API_KEY`, else `TYPESAFE_API_KEY`): `enabled` (an opt-out — `false` in either the global or the project config turns it off), `endpoint` (default `https://api.typesafe.ai/v1/systemone`), `model` (versioned id), budgets, one mode per decision point (`permissions`, `output_view`, `supervisor`, `triage`, `visibility`, `check`, `guidance`, `snippets`, `effort`, `work_mode` = `off`/`shadow`/`act`; unset → the point's default: `act` for `check`, `guidance`, `snippets`, `effort`, `work_mode` (asked only under the Auto work mode), `off` for the other five), `supervisor_max_continuations`, `prewarm`, `thresholds` (including `check_pass`, `check_remaining`, `supervisor_no_progress`, `guidance_yes`, `guidance_no`, `effort_confidence`, `work_mode_confidence`). A project config may set only `enabled` and the modes. **Deprecated, accepted with one warning and never read:** `tools` (alias `tool_tiers`) and `relevance` (points retired 2026-10-21), `supervisor_every_steps`, and the thresholds `tools_confidence`, `tools_min_p`, `relevance_needs`, `relevance_min_p`, `relevance_rerank`, `permission_scope`, `supervisor_looping`, `supervisor_loop_progress`. See [13 · Jev decisions](/oceancode/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](/oceancode/distribution). |
| `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: false` | Whole-tree snapshots: `enabled` takes one before every `edit`/`write`/`apply_patch`/`bash` call for the HTTP `/session/{id}/undo`·`/redo` routes (about 1.8 s per call on a 40k-file project; the user's own `.git` is never touched). `/rewind` does not need it: the file tools keep a copy of each file before they change it. |
| `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](/oceancode/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](/oceancode/permissions-and-sandbox#managed-enterprise-config). | 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](/oceancode/distribution). | 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](/oceancode/pager#environment). | `<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 and return their last text with `stopped at the step cap; resume to continue`. | `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](/oceancode/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` | 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 answered by the `read` tool without starting a shell: line numbers, paging, truncation guards, the `.env` deny and freshness tracking apply exactly as for a direct `read`. `refuse` restores the old refusal (the error names the exact `read` call by absolute path); `off` runs pure reads in the shell as typed. Pipes, redirects, chains, globs, missing paths, `tail -f` and `sed -i` always run. POSIX shells only. | unset (serve) |
| `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](/oceancode/mcp). | 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](/oceancode/mcp). | `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](/oceancode/operations#log-levels-oc_log). | 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](/oceancode/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](/oceancode/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](/oceancode/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](/oceancode/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](/oceancode/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 supplies a key — without one the layer stays off. | 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 `https://api.typesafe.ai/v1/systemone` |
| `OC_TYPESAFE_API_KEY` | The TypeSafe API key when none is stored (`oceancode key typesafe` wins), 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 a key keeps the layer off. | unset |
| `TYPESAFE_API_KEY` | The same key under the name TypeSafe's own SDKs read; `OC_TYPESAFE_API_KEY` wins over it. | 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-test`s. 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_PAGER_DIALECT_COVERAGE_OK` | Not read from the environment: the sentinel line the shared ACP dialect's coverage test prints (`ocean-acp` `pager_dialect_coverage_ok` in `extensions/dialect.rs`: `methods=<n> implemented=<n> unmapped=0` — every `Implemented` ext method has a dispatch arm). Listed because the name has the `OC_` shape this table is checked against. | — |
| `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` | Anthropic cache diagnostics are on by default: every request opts in, first with a null previous ID, then with that session’s previous response ID. `0` disables them. The API reports structural divergence, pending or unavailable comparisons; token usage determines whether the cache hit. IDs reset on restart or session deletion. | `1` |
| `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](/oceancode/hooks#where-hooks-are-read-from--and-when-they-run). |

**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, except a session whose agent has its own `agents.<id>` row (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). A save of
`oceancode.json` is live too: the pager toasts `Settings applied: …` with each value that moved, and an
open session still on its agent row's old value follows the row ([19](/oceancode/pager#shifttab-ring-and-permission-modes)).

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

## Reasoning effort per provider

Set the effort in `oceancode.json` for every role, on any provider: `effort` (the main session),
`subagent.effort` (sub-agents) and `agents.<id>.effort` (the effort a session runs at after
Shift+Tab to that agent, clamped to the model's levels; with `agents.<id>.model` also set, a `task`
call naming that model runs at it too). Option+Tab (or F3) in the pager steps the current session's
effort through the model's levels; the next Shift+Tab loads the agent's default again.
A write to the user or the project `oceancode.json` applies without a restart: running sessions
keep their model and effort, new sessions and the next sub-agent get the new values.

Every model is offered its own reasoning levels, taken from models.dev `reasoning_options`: the
model's effort list as listed (e.g. `minimal`…`high` on Gemini 3 Flash, `low`/`high`/`max` on
GLM-5.3); `on`/`off` for a model that can only switch thinking on and off; `low`/`medium`/`high`
for a model that takes only a thinking budget (1024 / 8192 / 24576 tokens, clamped into the model's
range). A toggle model also offers `off`. Models without models.dev data (seed and config entries)
fall back to the built-in Claude and GPT-5 tiers. The level you pick is sent as named — never
mapped to a different one — and a level the model does not list sends nothing.

| Protocol / provider | Request field sent |
|---|---|
| Anthropic Messages | `thinking: {type: "adaptive"}` + `output_config.effort` (Claude 4.6+); budget-token `thinking` on older Claude |
| OpenAI Responses (`openai`) | `reasoning.effort` (+ `reasoning.summary: auto`) |
| OpenAI Chat, provider `openai` | `reasoning_effort` |
| OpenAI-compatible chat (xAI, Z.AI, DeepSeek, Groq, Cerebras, OpenRouter, …) | `reasoning_effort`; a toggle model also sends `thinking: {type: "enabled"}`, and `off` sends only `thinking: {type: "disabled"}` |
| Gemini (Google AI Studio) | `generationConfig.thinkingConfig.thinkingLevel` (`MINIMAL`/`LOW`/`MEDIUM`/`HIGH`, Gemini 3+) or `thinkingConfig.thinkingBudget` (Gemini 2.5; `off` = `0` where the model allows it) |
| Bedrock, Azure, GitHub Copilot, Cloudflare | nothing yet — no effort menu is shown |

## See also

- [01 · Getting started](/oceancode) — where these files live on disk.
- [07 · Headless and server](/oceancode/headless-and-server) — `OC_SERVER_USERNAME` / `OC_SERVER_PASSWORD`.
