Skip to content

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

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 is downgraded to ask instead of auto-allowed.
  • Shell commands run under the OS sandbox instead of prompting.
  • 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.

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 off — denied entirely. There is no per-host egress control, so network is all-or-nothing and defaults to off.
  • Secrets scrubbed from the child environment — API keys, tokens, and any *_KEY / *_TOKEN / *_SECRET / AWS_* variable are stripped before the command runs, and credential stores (~/.ssh, ~/.aws, ~/.netrc, and similar) are masked from reads.

The same containment covers the run_code interpreter: its persistent Python kernel is spawned inside the sandbox once, so every exec in the session rides the same workspace-confined, network-off, secrets-scrubbed boundary a shell command gets.

Protected-write paths

The OS sandbox only wraps shell subprocesseswrite_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 — .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.

The controlled network holes — ssh and the git network tools

Network is off under the sandbox, so the model cannot ssh or git push/pull/fetch out from a shell command. Four dedicated tools are the controlled exceptions: each runs outside the sandbox, with structured args instead of a raw command, so deploys and git network ops are possible without opening up a general network hole.

ssh

The ssh tool runs one remote command outside the sandbox — a tightly-scoped, opt-in, always-ask hole.

  • Opt-in by allowlist. The tool isn't even registered until you set an ssh_hosts allowlist (global-only — a cloned repo cannot add a deploy target), and the host must be on it.
  • Always asks, refused while planning. Its mcp kind means it asks for approval in both modes (never auto-runs, even in auto) and is denied during the planning freeze.
  • The argv is the boundary. Since there's no sandbox here, safety rests on structured args: host / user / port / command only — no raw flags. Each is validated to reject injection (control chars, newlines, %-tokens, shell metacharacters, and a leading - in host/user that ssh would otherwise parse as an option) before anything spawns, then the command runs via a fixed set of hardening ssh -o options (BatchMode=yes, EscapeChar=none, ForwardAgent=no, PermitLocalCommand=no, StrictHostKeyChecking=accept-new, …) with the remote command passed after -- as a single argument.
  • Honors your ~/.ssh/config so host aliases, IdentityFile, and ProxyJump/bastions work — kin does not override your config's ProxyCommand/ProxyJump. The remote output is framed as untrusted data before it re-enters the conversation.

The approval grant is scoped to the exact user@host:port, so an "always allow" never bleeds to another host or to the local shell tool. (Maintainers: the full threat model lives in REFERENCE.md § "SSH egress".)

git-push / git-pull / git-fetch

The git network tools run git push/pull/fetch outside the sandbox, modeled directly on ssh. Unlike ssh, they're always registered — a git remote/branch is a charset-restricted bare name (no host-takeover surface the way an arbitrary ssh destination is), so there's no allowlist to opt into.

  • remote/branch, structured, no raw flags. Each is validated against an anchored allowlist before anything spawns — no leading - (which git would otherwise read as an option), no control chars/%/\, and :/@ excluded entirely (kills ext::/file:///scp-style user@host: syntax outright). Every argv also carries forced -c protocol.ext.allow=never -c protocol.file.allow=never and positional args always ride after a literal --.
  • force only ever means --force-with-lease — no raw --force is exposed, so a push can't blindly clobber a remote that moved since your last fetch.
  • git-push/git-pull always ask (reuse ssh's mcp kind — ASK in both modes, denied while planning). git-fetch is lighter: it doesn't mutate the working tree or push anything out, so it auto-allows in auto (still asks in strict) via its own network kind — but it's still denied while planning, since fetch does write .git/objects and update remote-tracking refs. See Modes & permissions.
  • The approval grant is scoped to remote + branch + that tool's one risky flag, so an "always allow" for a push to origin/main never bleeds to a different branch, remote, or force=true. (Maintainers: the full threat model lives in REFERENCE.md § "Git network tools".)

Fail-closed

When no sandbox backend is available — an unsupported platform, a missing bwrap, or KIN_SANDBOX=offauto 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 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.

Sandbox override

When the sandbox blocks a command, the agent doesn't see a bare Operation not permitted and flail. The shell tool recognizes the Seatbelt/bwrap deny signature (an EPERM fingerprint under the sandbox + a non-zero exit) and returns a structured hint instead — naming the likely rule, the observed error line, and the fix:

sandbox denied (likely): command='uv run pytest'
  exit: 1
  likely rule: file-write* outside the workspace
  observed: fatal: could not write to /Users/blake/.cache/uv/sdists-v9/.git
  fix: confine the write to the workspace (e.g. UV_CACHE_DIR=$workdir/.cache/uv),
       OR call ask_sandbox_override to ask the user to run it un-sandboxed

The agent's two paths: modify the command to stay inside the sandbox, or call ask_sandbox_override to ask you to run it un-sandboxed. The override tool reuses the familiar question modal — you pick:

  • Allow once — run this command un-sandboxed, just this time.
  • Allow for this session — run it, and skip the sandbox for matching commands until you quit.
  • Always allow (add to settings) — run it, and persist the pattern to [sandbox_always_allow] in your global settings (a diff modal shows the change before it's written).
  • Cancel, or type a rewritten command into the "other" box.

The grant is "skip the kernel sandbox", not "leak your secrets". An un-sandboxed run still scrubs the environment three ways: credentials out (ANTHROPIC_API_KEY, *_TOKEN, AWS_*, …), a strict keep-list (only PATH / HOME / LANG / LC_ALL / TMPDIR / USER / SHELL + benign KIN_* survive), and the auto-load vector family stripped (BASH_ENV, PYTHONPATH, NODE_OPTIONS, LD_PRELOAD, …) so a poisoned startup hook can't turn your one click into code execution.

The override tool can't bypass an arbitrary command. It refuses unless the command's head is in a small per-session LRU of recently denied commands — i.e. the sandbox genuinely just blocked it. A prompt-injected agent can't ask to bypass rm -rf out of nowhere; only a command that actually hit the wall. You are still the final gate: nothing runs un-sandboxed without your click.

The persistent allowlist lives in ~/.kin/settings.toml (global only — a cloned repo's project file can never grant itself a bypass):

sandbox_always_allow = ["uv *", "task *"]

Each entry is an fnmatch pattern matched against the full command. The agent reaches this list only through the override tool's Always allow path (it is deliberately not a model-writable sampling knob); hand-edit the file to manage it directly. View and revoke per-session grants with /grants.

Note

ask_sandbox_override is registered only when a sandbox backend is available. Under KIN_SANDBOX=off or KIN_SANDBOX=container the tool doesn't exist — there's no sandbox to bypass (and no denials to populate its LRU).

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.

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

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.