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

# Permissions and sandbox

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


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](/oceancode/configuration)), or switch it live over the HTTP API
(see [Switching the mode live](#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](/oceancode/pager#shifttab-ring-and-permission-modes)).

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](/oceancode/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](/oceancode/jev-decisions#what-jev-decides)). 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, and their system prompt
carries no skills list (they could not load one).

### 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](/oceancode/mcp#held-until-trusted)).

### 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](/oceancode/headless-and-server#permission-mode)). 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: `find` with `-delete`, `-exec`, `-execdir`, `-ok`, `-okdir`, `-fprint`,
  `-fprint0`, `-fprintf` or `-fls`; `git branch` that deletes, renames, copies,
  forces or re-points (`-d`, `-D`, `-m`, `-M`, `-c`, `-C`, `-f`, `-u`, `--delete`,
  `--set-upstream-to`, `--unset-upstream`); `git remote` with `add`, `remove`,
  `rm`, `rename`, `set-url`, `set-head`, `set-branches`, `prune` or `update`; and
  `git tag` that creates, signs, forces or deletes (a name after `tag` creates a
  tag unless `-l`/`--list` makes 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,
  and `curl … | sh`. The floor peels wrappers and unwraps `bash -c`/command
  substitutions, so hiding the command behind `sudo`, `/bin/rm`, or `$(…)` does
  not evade it.
- **`shell` overrides.** `safe_list_extra` adds read-only commands; `allow` /
  `deny` layer `Tool(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` |

- `deny` lists roots refused even inside the workspace (e.g. `.git`, a secrets
  dir).
- `network` is `off` (no egress) or `allowlist`. **The allow-list is not
  enforced yet:** on macOS and Linux an `allowlist` policy leaves egress open,
  because no child's traffic is routed through the TLS-SNI relay. Use `off` to
  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:

- **`.env` is never read.** `read` refuses `.env` and `*.env` / `*.env.*` files
  (secrets); `.env.example` still reads.
- **Read-before-write.** `write` refuses 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 a `bash` command 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.** `edit` and `write` refuse 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 a `bash` command 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](/oceancode/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 inspect` shows the provenance of every effective config key — which
  layer (project / user / managed / requirements) set it.
- `oceancode doctor` reports crash recovery state: orphaned sessions and captured
  panic reports. See [07 · Headless and server](/oceancode/headless-and-server).

## See also

- [02 · Configuration](/oceancode/configuration) — the config keys and variables.
- [07 · Headless and server](/oceancode/headless-and-server) — `oceancode doctor` and
  the permission-mode routes.
