Lock down network egress
By default, agent containers have normal outbound internet access. SetNANOCLAW_EGRESS_LOCKDOWN=true to force all agent traffic through the OneCLI gateway instead:
- Agent containers are placed on an
--internalDocker network (default namenanoclaw-egress, override withNANOCLAW_EGRESS_NETWORK). Internal networks have no route to the internet. - The OneCLI gateway container (default name
onecli, override withONECLI_GATEWAY_CONTAINER) is attached to that network with the aliashost.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.
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:
Gate additional mounts
Per-group container config can requestadditional_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
- Container path must be relative, non-empty, and contain no
..or:(prevents path traversal and Docker-voption injection). Validated mounts land under/workspace/extra/. - Host path is tilde-expanded and resolved through symlinks to its real path; it must exist.
- Blocked patterns — the real path must not match any pattern. Your
blockedPatternsare 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. - Allowed roots — the real path must sit under one of
allowedRoots. - 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.
/manage-mounts skill to view, add, or remove entries conversationally, or write the config through the setup step:
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.Restrict who can talk to your agents
Each messaging group carries anunknown_sender_policy that decides what happens when someone who isn’t an owner, admin, or member of the wired agent group writes in:
| Policy | Behavior |
|---|---|
strict | Message dropped silently and recorded in the dropped-messages log (ncl dropped-messages list). |
request_approval | Message 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. |
public | Anyone can talk to the agent — the access check is skipped entirely. |
request_approval. To harden a chat to silent-drop:
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 anowneroradminrole inuser_roles; everyone else gets a “Permission denied” reply. - Everything else passes through unchanged.
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 fromprocess.env only — set them in the service definition, not .env (see Configuration):
| Variable | Default | Limits |
|---|---|---|
CONTAINER_TIMEOUT | 30 min | Max runtime for an agent container. |
IDLE_TIMEOUT | 30 min | How long an idle container survives after its last result. |
MAX_CONCURRENT_CONTAINERS | 5 | Simultaneously running agent containers. |
CONTAINER_MAX_OUTPUT_SIZE | 10 MB | Output captured from a container. |