OceanCode
Permissions and sandbox
OceanCode decides what a tool may do in three layers, from softest to hardest:
- Permission rules and modes — an ask/allow/deny policy over each tool call.
- The shell and filesystem guards — a classifier that reasons about what a
bashcommand or a file edit actually does, on top of the rules. - The kernel sandbox — OS-level isolation the process cannot talk its way out of, off by default.
An empty config is not an asking one: the base rule is * * allow, so a tool call
is allowed unless something fences it — the shell's dangerous floor, an
external_directory ask, a denied .env read. Secret-named environment variables
are stripped from child processes and .env files are never read into the model.
Add bash * ask under permission to make every shell command off the read-only
safe list ask; that is also the posture the Jev permission advisor works on. This
page is how to loosen or tighten each layer.
Permission modes
A mode is a coarse posture layered over your permission rules. Set the boot
default with permission_mode in the config (see
02 · Configuration), or switch it live over the HTTP API
(see Switching the mode live below).
| Mode | What it does |
|---|---|
(empty) / default | Evaluate the rules as written; with the base * * allow rule a call no rule fences is allowed. |
acceptEdits | Auto-allow edit; everything else is unchanged. |
dontAsk | Never block on an interactive ask (a residual ask resolves to deny in a headless posture). |
bypassPermissions | Auto-allow everything — except the dangerous floor below, which no mode can reopen. |
The Ocean pager's auto (the Shift+Tab Auto stop, /auto) is acceptEdits and its
always-approve (ctrl+o, /always-approve) is bypassPermissions; the pager sends every
change to the engine (see 19 · Ocean pager).
A mode is expressed as ordinary last-match-wins rules, so it can never silently
override the safety floor: bypassPermissions appends an allow-all rule, but the
dangerous-floor deny is evaluated first and wins regardless.
Jev advisor. With the opt-in Jev decision layer on (see
13 · Jev decisions), a residual ask on a bash command — one
that no allow, deny or specific-pattern ask rule settled — is first put to Jev as
four bounded questions (in act the prompt waits at most jev.deadline_ms, 1.5 s by
default). With the empty config's * * allow base there is no such ask, so the
advisor sees nothing until a rule such as bash * ask makes the shell guard ask.
In shadow, the default once the layer is enabled, the call runs in the background, its
answer is only recorded and you are asked as before, without waiting. With jev.permissions = "act" a command inside a bounded
safe class — not destructive, no network egress, easily undone, a single-file or
project blast radius — runs without the prompt. The dangerous floor and deny rules
run before the advisor and never reach it, dontAsk gets no advice, a command that
names a path outside the project (or runs in a workdir outside it) gets none
either — the shell guard checks that in code — and a fixed hold list (package
installs and publishes, git push, recursive rm, ssh, an uploading curl, an
interpreter run on a file, …) always asks. Under headless --auto, an act advice
that the command is destructive, sends data out, reaches the system or a third
party, or tripped the injection guard rejects the call instead of approving it, and
the rejection carries the reason and the Jev scores back to the model as feedback
(see 13). Your answer to an advised prompt is
recorded as that decision's label.
Rule syntax — Tool(pattern)
Individual permission entries and the shell.allow / shell.deny lists take a
Tool(pattern) form, e.g. Bash(git push:*), Edit(src/**), Read(*.md). The
tool name is matched case-insensitively; an empty pattern means *.
A tool whose last rule for its key is a * deny — bash * deny, or a read-only
agent's blanket deny — is not offered to the model at all: its schema is left out of
the request (write, apply_patch and hashline count as edit). A narrower last
rule keeps the tool, so the plan agent still sees edit for its plan file. Sub-agent
children are sent the built-in tools only, never MCP or skill tools.
MCP calls — the mcp key
Every MCP tool call passes the rules under the key mcp with the pattern
<server>/<tool>, e.g. "mcp": {"github/*": "allow", "github/delete_repo": "deny"}.
When no mcp rule decides, the server's origin does: a server declared in config
(the project's .oceancode/oceancode.json or your global oceancode.json, a user-tier
~/.claude.json, the project-local tier) or passed by the client (the servers Xcode
passes) runs without asking; a server from the project's .mcp.json, a plugin or a
runtime add (POST /mcp, the pager's /mcp add) asks, and "always" approves the whole
server. Config servers are not trust-gated, including those in a .oceancode/oceancode.json
committed to a checkout: read that file before you open a repository you don't trust. The server's own readOnlyHint is never
used: the server supplies it. A headless oceancode run without --auto cannot ask,
so it refuses the call and tells the model the rule that approves the server. Trust
is the separate gate that decides whether such a server runs at all
(11 · MCP servers).
Switching the mode live
The permission mode is switched at runtime over the HTTP API —
GET/POST /api/session/{id}/permission/mode (see
07 · Headless and server). A managed
(enterprise) requirements file can lock out bypassPermissions: once locked,
selecting it downgrades to default.
The shell guard
Before any bash command runs, a classifier splits it into segments, peels
wrappers (sudo, env, nice, timeout, xargs, bash -c "…", VAR=val …),
and decides allow / ask / deny per segment. The whole command runs only if
every segment is allowed (a chaining guard — git status && rm x still asks).
- Read-only safe list. Commands that only read (
git status,git log,ls,rg,grep,find, …) auto-approve with no rule. The list looks at flags:findwith-delete,-exec,-execdir,-ok,-okdir,-fprint,-fprint0,-fprintfor-fls;git branchthat deletes, renames, copies, forces or re-points (-d,-D,-m,-M,-c,-C,-f,-u,--delete,--set-upstream-to,--unset-upstream);git remotewithadd,remove,rm,rename,set-url,set-head,set-branches,pruneorupdate; andgit tagthat creates, signs, forces or deletes (a name aftertagcreates a tag unless-l/--listmakes it a pattern) are off the list and ask. Adding output redirection (> file) drops a command off the safe list. - Dangerous floor. A fixed set of destructive shapes is refused outright and
no permission mode can reopen it:
rm -rf /(and obfuscated variants),git push --force,dd of=/dev/*,chmod -R 777 /,mkfs*, the fork bomb, andcurl … | sh. The floor peels wrappers and unwrapsbash -c/command substitutions, so hiding the command behindsudo,/bin/rm, or$(…)does not evade it. shelloverrides.safe_list_extraadds read-only commands;allow/denylayerTool(pattern)rules onto the classifier. Neither can remove the dangerous floor.
Child-process environment
Every bash and PTY child gets a composed environment rather than a raw copy of
the parent's. By default, variables whose name contains KEY, SECRET, or
TOKEN (case-insensitive) are stripped, so a secret in your shell never
leaks into a spawned tool.
Tune it with child_env:
| Field | Meaning |
|---|---|
inherit | all (default), core (only PATH/HOME/USER/SHELL/LANG/TERM/TMPDIR), or none. |
include_only | When non-empty, keep only these variable names. |
set | Extra name→value entries overlaid last. |
strip_secrets | Drop secret-named variables (default true). |
The sandbox
The sandbox confines a spawned child at the OS level — sandbox-exec (Seatbelt)
on macOS, bubblewrap (with Landlock where the kernel supports it) on Linux ≥
5.13. It is off by default; turn it on with the sandbox config.
| Profile | Reads | Writes | Network |
|---|---|---|---|
off | unrestricted | unrestricted | unrestricted |
workspace | workspace + writable roots | workspace + writable roots | per network |
readonly | workspace + writable roots | denied | per network |
strict | workspace only | workspace only | per network |
denylists roots refused even inside the workspace (e.g..git, a secrets dir).networkisoff(no egress) orallowlist. The allow-list is not enforced yet: on macOS and Linux anallowlistpolicy leaves egress open, because no child's traffic is routed through the TLS-SNI relay. Useoffto block egress.
On a Linux kernel older than 5.13, Landlock is unavailable and bubblewrap alone provides the confinement (a warning is logged, never a silent disable).
Reference aliases
refs maps a short @name to a directory. A path of the form @name/rest in any
file tool (read / edit / write) expands to <refs[name]>/rest, and that
reference root is auto-allowed through the external-directory prompt — so a shared
docs or vendor tree can be reached without a prompt for every file. An unknown
@name is left literal and still prompts, and an alias that walks out of its root
(@name/../escape) is caught by the external-directory boundary just like any
other outside path.
Filesystem guards
The file tools carry their own safety, independent of the rules:
.envis never read.readrefuses.envand*.env/*.env.*files (secrets);.env.examplestill reads.- Read-before-write.
writerefuses to overwrite an existing file you have not read this session ("file exists but has not been read this session"); a brand-new file writes freely. One exception (2026-09-28): a file under 64 KiB that this session itself created — its birth time is at or after the session's first tool call in this process, e.g. a file abashcommand generated — can be overwritten without a read. A file that existed before the session is refused even when it was edited since, and on a filesystem that reports no birth time the exception never applies. - Stale detection.
editandwriterefuse a target that changed on disk since it was last read — detected by content hash, so a same-size, same-mtime change is still caught. Read it again first. Other files changing — a build, a file abashcommand wrote, the session's own database — never cost you a read: a read stays valid until the file itself changes (2026-09-28).
Managed (enterprise) config
An administrator can layer a managed config over every project. It is loaded
last, so project < user < managed — managed always wins. Its location is the OS
admin path, or $OC_MANAGED_CONFIG (see
02 · Configuration).
Alongside it, a signed requirements.json carries hard requirements — disabling
bypassPermissions, an MCP-server allow-list, and config pins. It is verified
with ed25519 against the trusted key in $OC_REQUIREMENTS_PUBKEY: an unsigned
or tampered requirements file is refused, never applied. With no key configured,
managed requirements are simply not enforced.
oceancode inspectshows the provenance of every effective config key — which layer (project / user / managed / requirements) set it.oceancode doctorreports crash recovery state: orphaned sessions and captured panic reports. See 07 · Headless and server.
See also
- 02 · Configuration — the config keys and variables.
- 07 · Headless and server —
oceancode doctorand the permission-mode routes.