Skip to main content
NanoClaw’s isolation boundary is the Docker container: each agent runs in its own container with a scoped workspace mount (see Container lifecycle). There is no micro-VM or alternative sandbox runtime — hardening means tightening what those containers can reach. This page walks the defense-in-depth controls from network to filesystem to people.

Lock down network egress

By default, agent containers have normal outbound internet access. Set NANOCLAW_EGRESS_LOCKDOWN=true to force all agent traffic through the OneCLI gateway instead:
  • Agent containers are placed on an --internal Docker network (default name nanoclaw-egress, override with NANOCLAW_EGRESS_NETWORK). Internal networks have no route to the internet.
  • The OneCLI gateway container (default name onecli, override with ONECLI_GATEWAY_CONTAINER) is attached to that network with the alias host.docker.internal, so the injected proxy is the only reachable hop.
  • Agents run non-root without NET_ADMIN, so they can’t reconfigure the network from inside.
The setup is idempotent and self-healing — NanoClaw creates the network and re-attaches the gateway on every spawn if needed. It also fails fast: if lockdown is enabled but the network can’t be created or the gateway container can’t be attached, NanoClaw throws an EgressLockdownError and refuses to spawn the agent rather than silently running it with open egress. If you see that error, start the gateway container (or set NANOCLAW_EGRESS_LOCKDOWN=false to opt out). All three variables can be set in .env or the process environment (src/config.ts:84-88) — .env is the simpler path for a plain “edit and restart” flow; the service definition (launchd plist or systemd unit) still works too, as described in Configuration:

Gate additional mounts

Per-group container config can request additional_mounts — extra host directories mounted into the container. Every request is validated against an allowlist at ~/.config/nanoclaw/mount-allowlist.json. The file lives outside the project root on purpose: container agents can edit files in their workspace, so the security config has to sit where they can’t reach it. If the allowlist file doesn’t exist, all additional mounts are blocked. That’s the default posture; you opt in by creating the file. Mounts are also operator-only to edit: ncl groups config add-mount and remove-mount run only from the host socket — never from inside a container, even for an agent with global CLI scope — and take effect on the next ncl groups restart. Whatever they add still passes the allowlist check at spawn.
mount-allowlist.json
Validation rules, in order:
  1. Container path must be relative, non-empty, and contain no .. or : (prevents path traversal and Docker -v option injection). Validated mounts land under /workspace/extra/.
  2. Host path is tilde-expanded and resolved through symlinks to its real path; it must exist.
  3. Blocked patterns — the real path must not match any pattern. Your blockedPatterns are merged with a built-in default set (.ssh, .gnupg, .aws, .kube, .docker, credentials, .env, .netrc, id_rsa, id_ed25519, and similar) that you can’t remove.
  4. Allowed roots — the real path must sit under one of allowedRoots.
  5. Read-only by default — a mount is read-write only when it explicitly requests it and the matching root has allowReadWrite: true. Otherwise it’s forced read-only.
Rejected mounts are logged with the reason and skipped; the container still starts with its valid mounts. Use the /manage-mounts skill to view, add, or remove entries conversationally, or write the config through the setup step:
The validator accepts either key: allowReadWrite (native) or readOnly (written by the /manage-mounts skill and setup). If both are absent it defaults to read-only. readOnly: true means read-only, readOnly: false means read-write — the validator translates it to allowReadWrite = !readOnly and logs a warning when it does (src/modules/mount-security/index.ts, normalizeRoot). Likewise, a top-level nonMainReadOnly field may appear in configs written by setup, but the mount validator doesn’t read it.
The allowlist is cached in memory, so restart the service after changes.

Restrict who can talk to your agents

Each messaging group carries an unknown_sender_policy that decides what happens when someone who isn’t an owner, admin, or member of the wired agent group writes in: Channels auto-created by the router (someone @mentions or DMs the bot in an unwired chat) get request_approval. To harden a chat to silent-drop:
Sender approvals are handled entirely through the DM card — there’s no ncl resource for them (ncl approvals covers a different approval queue). Use ncl dropped-messages list to audit who got dropped and why. Note that the approval flow needs an owner or admin with a reachable DM channel — on a fresh install with no owner configured, approval requests are skipped and logged. Wirings can additionally set sender_scope known, which requires explicit membership even on a public messaging group — see ncl CLI.

The command gate

Slash commands are classified on the host before they ever reach a container:
  • Filtered commands (/start, /help, /login, /logout, /doctor, /config, /remote-control) are dropped silently — they manipulate host-side CLI state and never reach the agent.
  • Admin commands (/clear, /compact, /context, /cost, /files, /upload-trace) require the sender to hold an owner or admin role in user_roles; everyone else gets a “Permission denied” reply.
  • Everything else passes through unchanged.
If the permissions module isn’t installed (no user_roles table), admin commands are allowed for everyone — install the module to get the role check.

Cap container resources

These limits bound what a runaway agent can consume. Both can be set in .env or the process environment (see Configuration):
There is no env var for a container-count or output-size cap, and no concurrency-cap env var — CONTAINER_TIMEOUT, IDLE_TIMEOUT, CONTAINER_MAX_OUTPUT_SIZE, and MAX_CONCURRENT_CONTAINERS were removed from the code. Idle/stuck containers are now caught by the periodic host-sweep loop (src/host-sweep.ts) — see Container killed mid-task.

Credentials

Keep API keys and OAuth tokens out of agent containers entirely — the OneCLI Agent Vault injects them at the proxy layer so agents never see raw secrets. See Credentials.
Last modified on July 16, 2026