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 are read from process.env only — putting them in .env has no effect. Set them in the service definition (launchd plist or systemd unit) as described in Configuration:
# Linux (systemd unit)
Environment=NANOCLAW_EGRESS_LOCKDOWN=true

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.
mount-allowlist.json
{
  "allowedRoots": [
    { "path": "~/projects", "allowReadWrite": true, "description": "Development projects" },
    { "path": "~/Documents/work", "allowReadWrite": false, "description": "Work documents (read-only)" }
  ],
  "blockedPatterns": ["password", "secret", "token"]
}
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:
pnpm exec tsx setup/index.ts --step mounts --force -- --json '{"allowedRoots":[{"path":"/path/to/dir","allowReadWrite":true}],"blockedPatterns":[]}'
The key the validator reads is allowReadWrite. A readOnly key (shown in some upstream examples) is silently ignored, leaving the mount read-only. 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:
PolicyBehavior
strictMessage dropped silently and recorded in the dropped-messages log (ncl dropped-messages list).
request_approvalMessage dropped, and an Allow / Deny card is DM’d to an owner or admin. On approve, the sender becomes a group member and the original message is re-routed. Duplicate requests from the same sender are deduplicated while a card is pending.
publicAnyone can talk to the agent — the access check is skipped entirely.
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:
ncl messaging-groups update --id <mg-id> --unknown-sender-policy strict
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 (/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

Four limits bound what a runaway agent can consume. All are read from process.env only — set them in the service definition, not .env (see Configuration):
VariableDefaultLimits
CONTAINER_TIMEOUT30 minMax runtime for an agent container.
IDLE_TIMEOUT30 minHow long an idle container survives after its last result.
MAX_CONCURRENT_CONTAINERS5Simultaneously running agent containers.
CONTAINER_MAX_OUTPUT_SIZE10 MBOutput captured from a container.

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 June 10, 2026