# Auto mode & the OS sandbox

> auto mode lets the agent edit and run commands without per-call prompts, while a kernel-enforced sandbox keeps shell commands contained. This is safe autonomy — a contained replacement for "YOLO" mode — and it is the…

`auto` mode lets the agent edit and run commands without per-call prompts, while a kernel-enforced sandbox keeps shell commands contained. This is safe autonomy — a contained replacement for "YOLO" mode — and it is the **launch default**.

<!-- SOURCE: src/kin/harness/sandbox.py, src/kin/harness/sandbox_access.py, src/kin/harness/modes.py, src/kin/harness/loop.py, src/kin/harness/command_safety.py, src/kin/harness/git_trust.py, src/kin/harness/github_connection.py, src/kin/harness/tools/git_tool.py, src/kin/harness/tools/github_tool.py, src/kin/harness/tools/git_network.py, src/kin/harness/tools/shell.py, src/kin/harness/tools/run_code.py, src/kin/harness/tools/sandbox_access.py, src/kin/harness/settings/, src/kin/harness/tools/_util.py, settings.toml.example -->

## What auto does

In `auto` mode, file edits and shell commands run without asking:

- **Edits** (`write_file`, `edit_file`, `edit_notebook`) auto-allow, but are confined to the workspace. An edit that targets a path outside the workspace is refused with a result the model reads, not prompted. A write to a [protected path](#protected-write-paths) is downgraded to ask instead of auto-allowed.
- **Shell** commands run under the OS sandbox instead of prompting.
- **Bounded Git/GitHub publication** auto-allows through the first-party
  structured tools. Force-with-lease still asks every time.
- **MCP tools still ask.** An MCP tool's description is attacker-influenceable, so it is never auto-approved, even here.

`auto` is the default and one half of the 2-way cycle (`auto ↔ strict`); toggle to `strict` and back with ++shift+tab++ or a bare `/mode`. See [Modes & permissions](/docs/kin/guide/modes-and-permissions/).

## The OS sandbox

A command-pattern allowlist is not a security boundary — a shell composes unbounded ways to dodge a lexical filter — so the real containment in `auto` mode is an OS sandbox the kernel enforces regardless of what the classifier thinks.

| Platform | Backend | Mechanism |
|---|---|---|
| macOS | Seatbelt | `sandbox-exec` with a generated SBPL profile |
| Linux | bubblewrap | `bwrap` with a read-only root, a writable workspace bind, and a private `/tmp` |

The allowlist classifier still runs (a provably-safe `git status` is the common case); the sandbox is the layer that holds when the classifier is wrong.

## What's confined

Under the sandbox a shell command gets:

- **Writes confined to the workspace** — plus the per-user temp dir and `/tmp`. The workspace's `.git/hooks` and `.git/config` stay read-only (so a command cannot plant a hook), while the rest of `.git` stays writable so the agent can still `git add` / `commit`.
- **Network available by default** — package managers, HTTP clients, public Git transports, and live tests work inside the filesystem sandbox. Set global-only `sandbox_network = false` (or `KIN_SANDBOX_NETWORK=0`) to restore full network isolation; with networking off, sandboxed `uv`/`uvx`/`task` runs also get `UV_NO_SYNC=1` so uv runs against the already-populated `.venv` instead of dying on a DNS-blocked implicit sync.
- **Secrets and SSH identity scrubbed by default** — API keys, tokens, and any `*_KEY` / `*_TOKEN` / `*_SECRET` / `AWS_*` variable are stripped before the command runs. Credential stores (`~/.aws`, `~/.netrc`, and similar), `SSH_AUTH_SOCK`, SSH client configuration, and private keys stay masked. Network authority and SSH identity are separate: a command that genuinely needs agent-backed SSH asks for both network and SSH-agent access through the [scoped access flow](#scoped-sandbox-access). The grant exposes only the agent socket and read-only public client metadata; private-key files remain hidden.

The same posture covers the [`run_code` interpreter](/docs/kin/guide/tools/#code-interpreter-python) and diagnostics subprocesses: each uses the resolved global network setting, workspace confinement, and secret scrub.

This is a containment boundary, not a confidentiality promise against the network. A networked command can transmit any readable workspace content. An SSH-agent grant can additionally ask your agent to authenticate, but cannot read its private keys. Use `sandbox_network = false` when network egress itself is unacceptable; omit or decline SSH access when identity use is unacceptable.

## Protected-write paths

The OS sandbox only wraps shell *subprocesses* — `write_file` / `edit_file` write in-process, so in `auto` mode they also carry their own containment. Beyond the workspace-confinement check above, a write to a path whose *modification* would change how the agent or its tooling runs is downgraded from auto-allow back to **ask**, even in `auto`:

- anything under a `.git/` tree (repo state),
- lockfiles — `uv.lock`, `package-lock.json`, `yarn.lock`, `pnpm-lock.yaml`, `Cargo.lock`, `poetry.lock`, `Gemfile.lock`, `go.sum` (the audited dependency supply chain),
- package-manager / registry config — `.npmrc`, `.yarnrc`, `.yarnrc.yml`,
- harness / tooling config — `.kin/mcp.json`, `.mcp.json`, `.claude.json`, `.pre-commit-config.yaml`, `lefthook.yml`, `pyrightconfig.json`,
- shell rc / env files — `.bashrc`, `.zshrc`, `.bash_profile`, `.profile`, `.envrc`,
- `~/.ssh/config` — the SSH config the `ssh` tool trusts (a rewrite could repoint a deploy alias or inject a `ProxyCommand`),
- `.kin/settings.toml` — the file that governs this very gate (mode, sandbox, permissions), so it can't quietly loosen itself.

This is the write-side companion to the read-deny list (which guards secret *exfiltration*); it guards a silent self-rewrite of the dev environment. The match is realpath-aware, so a symlink or `..` spelling that resolves onto one of these can't dodge it. In `strict` mode every edit already asks, so this only changes behavior in `auto`.

## Structured Git credentials and Beam supervision

The generic `shell` remains the development surface for SSH, package managers,
HTTP clients, unusual Git administration, and live tests. It stays inside
Seatbelt/bubblewrap with secrets and credential stores masked. Everyday Git
instead uses the first-party [`git` tool](/docs/kin/guide/tools/#git-and-github): fixed argv,
workspace/repository confinement, and only SSH or HTTPS remotes. The
[`github` tool](/docs/kin/guide/tools/#git-and-github) uses the installed `gh` CLI for bounded
PR reads and creation.

These structured tools own a narrow credential exception that generic shell
does not inherit. Before remote Git can use an SSH agent or global/system HTTPS
credential helper, Kin asks in a dedicated modal for the resolved host route.
The session and persistent lifetimes store no secret: only transport, resolved
host/user/port, and a fingerprint of the effective trusted route. Repository
config cannot install a credential helper, proxy, include, askpass, or SSH
command for this path.

For a recognized `github.com` remote, Kin adapts both saved SSH and HTTPS URLs
to one command-scoped authenticated HTTPS route. The saved remote is not
edited. Only `gh` configuration and the platform keyring are unmasked for that
fixed helper; private SSH keys and unrelated Git credential files remain
hidden. Kin asks `gh` to emit only token-free account fields, and inherited
`GH_*` / `GITHUB_*` tokens are stripped. If no healthy account exists, one
human connection modal parks the operation, runs the fixed web login outside
the model, then continues that same operation. This is a credential handoff,
not a generic sandbox expansion.

Safe sync's local history mutation uses the ordinary workspace sandbox and
fixed Git argv. It starts only from a clean repository root, pins the exact
pre-sync object, disables executable/history side channels, and restores that
object on conflict or interruption. Its recovery refs carry object IDs, not
credentials or new authority.

`sandbox_network = false` controls generic sandboxed shell egress. It does not
disable an explicitly selected structured Git/GitHub operation: those tools
have their own `network` / `publish` permission kinds and host/auth boundary.
Disable or remove the remote/auth broker if that capability should not exist,
or use `strict` so every network/publication call asks.

Beam remains separate. An Outpost child whose authenticated
`v1_metadata.beam` value is exactly JSON `true` receives the internal
`KIN_BEAM=1` posture. Only that child registers `git-push` / `git-pull` /
`git-fetch`, and its shell refuses raw versions of those verbs. The exact
structured `git-push` call remains the bounded halt/approval/resume checkpoint;
ordinary `git` / `github` schemas and their grants are not registered there.

## Fail-closed

When no sandbox backend is available — an unsupported platform, a missing `bwrap`, or `KIN_SANDBOX=off` — `auto` mode does **not** run commands unconfined. Edits still auto-apply (confined to the workspace), but shell falls back to the allowlist's posture: a provably read-only command runs, and anything else asks for approval. An un-vetted command never runs without either containment or your explicit approval.

The one deliberate exception is `KIN_SANDBOX=container`, which trusts an external boundary (a disposable container) in place of the OS sandbox — see [Trust the container](#trust-the-container) below.

kin surfaces the containment posture so it's never a surprise. On a cold start in `auto`, it appears as a concise dim line in the welcome banner — e.g. *"auto · shell asks for anything not provably safe"*, *"auto · shell sandboxed (Seatbelt)"*, or *"auto · shell runs unconfined (container is the boundary)"*. When you **cycle into** `auto` mid-session (where there's no banner), kin prints the fuller posture as a one-off system note instead — and with no OS sandbox it says so loudly: *"auto mode — no OS sandbox available; edits auto-apply (confined to the workspace), and shell will ASK for anything not provably safe."*

> **Warning**
>
> Fail-closed is the load-bearing safety property. Cycling into `auto` on a box with no sandbox is safe precisely because a non-safe command still asks rather than running unconfined.

## Scoped sandbox access

The sandbox can now widen the smallest relevant capability without discarding
the whole boundary. `shell` and `run_code` may request a typed
`sandbox_permissions` object before execution:

- `write_paths` — up to 16 existing directory roots that need writes;
- `network` — outbound network access;
- `ssh_agent` — the SSH agent socket plus read-only public client metadata
  (requires `network = true`);
- `unconfined` — run this exact command with the sandbox fully lifted, when no
  granular capability can express what it needs (a syscall the profile blocks).
  This is the escape hatch, gated by one modal that shows the command before it
  runs; it caps at Session lifetime and still scrubs secrets from the
  environment. Requesting it prospectively is the intended alternative to
  provoking a denial just to obtain a retry id.
- `reason` — the model's explanation, shown to you but never treated as
  authority.

External directories are intentional. A request is not limited to the current
repository, so a build can ask for its real cache, sibling checkout, or output
directory. Kin canonicalizes every path, requires an existing directory the
current user can actually write, rejects virtual/device trees and
filesystem-wide ancestors such as `/` or your home directory, and keeps the
workspace, temp roots, `.git` hook/config protections (including in an
approved sibling repository), credential masks, and secret scrub as
higher-priority rules.

You get one dedicated modal showing the immutable executor, working directory,
operation, reason, and requested capabilities. You may deselect capabilities
and choose:

- **Once** — only this call. `run_code` uses a fresh one-shot kernel so the
  permission cannot leak into later cells.
- **Session** — subsequent sandboxed executors may use the selected
  capabilities until Kin closes.
- **Workspace** — persist the selected profile for this exact canonical
  workspace in global settings.
- **Decline** or **Decline with a note** — no expanded execution occurs and the
  model receives your reason.

Sensitive/system write roots and a complete sandbox bypass are capped at
**Session**. Ordinary external directories, network, and SSH access may be
workspace grants. The modal is the approval gate; Kin does not add a second
generic tool-approval prompt.

### Reactive denials and exact retry

When `shell` or `run_code` is denied, Kin classifies Seatbelt/bubblewrap
fingerprints including `EPERM`, Linux `EROFS` / `[Errno 30] Read-only file
system`, network isolation, and SSH authentication/configuration failures. A
recognized denial returns an opaque `sbx_…` denial id and a suggested granular
profile. The model calls:

```text
request_sandbox_access(denial_id="sbx_…")
```

The model does **not** repeat or reconstruct the command/code. The harness
retains the exact executor arguments and working directory, consumes the id
once, asks you for a capability subset and lifetime, then retries that
immutable operation. A shell command may have partially executed before the
denial; the modal says so. A `run_code` retry uses a fresh kernel and says that
state is not carried across.

If no granular path/network/SSH capability can express a real classified
denial, the reactive request may propose `unconfined = true`. That is the escape
hatch, not the normal path: it is bound to the exact operation, never persists
beyond the session, and still uses Kin's strict unconfined environment scrub.
The same escape hatch can be requested **prospectively** — a `shell`/`run_code`
call whose `sandbox_permissions` carries `unconfined = true` — so that when the
model already knows a command needs the sandbox lifted (and you have said so), it
asks once, showing you the actual command, instead of provoking a denial to farm
a retry id. It always prompts: a prospective bypass never rides auto mode's
silent allow.

DNS and routing failures are classified against the resolved network posture.
With `sandbox_network = false`, common resolver and no-route errors are
Kin-owned isolation denials. With networking already enabled, the same output
records no denial id because removing Kin's filesystem boundary would not fix
an upstream DNS, proxy, or host failure.

### Inheritance, resume, and revocation

Child agents cannot open approval UI. They report the required capability or
denial id to the parent; session/workspace grants resolved by the root are then
part of the root's effective sandbox profile. In headless mode a request is
journaled as needs-human. Resuming that journal in the TUI reopens the typed
modal before the next model turn and, if accepted, retries the exact persisted
arguments.

[`/grants`](/docs/kin/guide/modes-and-permissions/#managing-grants) lists ordinary approval
signatures, Git host-route trust, sandbox session grants, exact-operation
session bypasses, and workspace grants together. Revoking sandbox authority
also stops expanded background shells and closes an expanded `run_code` kernel
so a live process cannot retain authority that the UI says is gone. Revoking a
persistent Git host route makes the next remote Git operation ask again.

Legacy `sandbox_always_allow` command patterns are still readable for
migration, but are deliberately **inactive**: they cannot skip the sandbox.
`/grants` labels them as requiring reapproval and can remove them. The old
`ask_sandbox_override` name remains only as a dispatch alias for short-lived
compatibility; its old command/pattern arguments are rejected.

> **Note**
>
> Scoped sandbox access is registered only when Kin owns a usable local
> sandbox. Under `KIN_SANDBOX=off` or `KIN_SANDBOX=container` there is no
> inner boundary to expand, so proactive requests fail closed and the
> reactive tool is absent.

## Choosing a backend

`KIN_SANDBOX` selects the backend (or the matching `sandbox` key in the **global** settings file):

| Value | Behavior |
|---|---|
| `auto` | Resolve by platform — Seatbelt on macOS, bubblewrap on Linux. The default. |
| `seatbelt` | Force the macOS Seatbelt backend. |
| `bwrap` | Force the Linux bubblewrap backend. |
| `off` | Disable the sandbox. `auto` mode then falls back to the allowlist's ask posture (fail-closed). |
| `container` | Trust an external boundary. No OS sandbox, but `auto` mode's shell runs **allowed and unconfined** instead of asking — see below. |

Networking is a separate global-only switch:

| Env | Settings key | Effect |
|---|---|---|
| `KIN_SANDBOX_NETWORK=0` | `sandbox_network = false` | Isolate networking inside Seatbelt/bubblewrap and restore network-denial override hints. |

It has no effect under `KIN_SANDBOX=off` or `container`, where Kin does not own the network boundary.

### Trust the container

`KIN_SANDBOX=container` is for running kin inside a disposable, isolated container (or VM) that is *already* the security boundary — the [Outpost](/docs/kin/guide/outpost/) sets it by default. It is the opposite of `off`:

- `off` — "no sandbox, and nothing replaces it" → **fail-closed**: a non-safe shell command asks.
- `container` — "no sandbox because the environment *is* the boundary; trust it" → a non-safe shell command runs, unconfined, with the full environment.

Use it only where the boundary is real, because it removes the per-command approval for shell. It is a deliberate, **operator-set** grant — kin never auto-detects a container and silently drops the gate (that would let an injected prompt enumerate its own relaxed posture, false-positive on VMs/WSL, and erase the audit trail). And it is narrow: it relaxes **only** the shell gate the OS sandbox stood in for — MCP still asks, the planning freeze still denies, and the protected-write downgrade still asks. The launch note announces it (*"auto mode — external sandbox trusted (KIN_SANDBOX=container)…"*) so the grant is visible in the transcript.

You can also opt `strict` mode into the same sandbox so an un-vetted command runs contained instead of prompting:

| Env | Settings key | Effect |
|---|---|---|
| `KIN_SANDBOX_STRICT=1` | `sandbox_strict = true` | Sandbox shell in `strict` mode |

`KIN_SANDBOX=off` always wins over the per-mode opt-in. The sandbox and per-mode keys are global-only, so a cloned repo's project file cannot weaken your containment — see [settings.toml reference](/docs/kin/reference/settings-toml/).

> **Legacy `sandbox_default`**
>
> The old `KIN_SANDBOX_DEFAULT` / `sandbox_default` knob (and the now-removed `sandbox_accept_edits`) predate the two-mode collapse. `sandbox_default` is still honored as a fallback for `sandbox_strict` — since the old `default` mode now aliases to `strict`, an existing `sandbox_default = true` keeps flipping the same opt-in rather than going inert.

For the full "why it is safe" invariants — the SBPL profile, the bubblewrap argv, and the security guarantees — see the "Auto mode & OS sandbox" section of [REFERENCE.md](https://github.com/kinra-ai/kin/blob/dev/REFERENCE.md).
