04 · Policies and Guardrails
This is the file you will actually come back to. It answers: "my agent can't reach X — how do I open it?" and "why is it locked down like this?"
Everything is in NemoClaw/nemoclaw-blueprint/policies/:
policies/
├── openclaw-sandbox.yaml # the baseline, applied to every NemoClaw sandbox
├── openclaw-sandbox-permissive.yaml # a relaxed variant for experimentation
└── presets/
├── brave.yaml brew.yaml discord.yaml github.yaml
├── huggingface.yaml jira.yaml npm.yaml outlook.yaml
├── pypi.yaml slack.yaml telegram.yaml
The anatomy of the baseline policy
openclaw-sandbox.yaml has four sections. Read them in this order — it's the mental order you'll think about the sandbox in.
1. filesystem_policy — read‑only by default
filesystem_policy:
include_workdir: false # MUST stay false — see below
read_only:
- /usr, /lib, /proc, /dev/urandom, /app, /etc, /var/log
- /sandbox # home dir is READ-ONLY
- /sandbox/.openclaw # gateway config is IMMUTABLE
read_write:
- /tmp
- /dev/null
- /sandbox/.openclaw-data # writable agent/plugin state (symlink target)
- /sandbox/.nemoclaw # plugin state
Why this is weird and important:
include_workdir: false— iftrue, OpenShell auto‑adds the workdir (/sandbox) toread_write, and because Landlock grants the union of matching rules, that would override theread_onlybelow. So keep itfalseand list writable paths explicitly. This is a real bug they hit (issue #804).- Home is read‑only. The agent cannot write arbitrary files into
/sandboxor modify its own runtime environment. The only writable carve‑outs are the two data dirs,/tmp, and/dev/null. - Gateway config is immutable.
/sandbox/.openclawholds auth tokens and CORS settings. Writable OpenClaw state (agents, plugins, workspace) lives in/sandbox/.openclaw-dataand is symlinked in.
Consequence: filesystem policy is locked at sandbox creation (Landlock). If you change it, you need nemoclaw onboard to recreate.
2. process — drop privileges
process:
run_as_user: sandbox
run_as_group: sandbox
Combined with OpenShell's seccomp, this is why the agent runs as a non‑root user. Also locked at creation.
3. network_policies — the egress allowlist
Each named block is a policy bound to specific binaries, allowing specific hosts, ports, methods, paths. Here is the shape of the allowlist that ships by default:
| Policy block | Allowed for these binaries | Hosts | Methods/paths |
|---|---|---|---|
claude_code |
/usr/local/bin/claude |
api.anthropic.com, statsig.anthropic.com, sentry.io |
Anthropic: POST /v1/messages, POST /v1/messages/batches, GET /v1/messages/batches/**, POST /v1/complete. Sentry: GET /** only — POST is blocked to prevent exfil. |
nvidia |
/usr/local/bin/claude, /usr/local/bin/openclaw |
integrate.api.nvidia.com, inference-api.nvidia.com |
POST /v1/chat/completions, POST /v1/completions, POST /v1/embeddings, GET /v1/models, GET /v1/models/** |
clawhub |
openclaw, node |
clawhub.ai |
GET /**, POST /** — plugin discovery + install |
openclaw_api |
openclaw, node |
openclaw.ai |
GET /**, POST /** — auth flows |
openclaw_docs |
openclaw |
docs.openclaw.ai |
GET /** — docs are read‑only |
npm_registry |
openclaw, npm, node |
registry.npmjs.org |
GET /** — fetch packages, never publish |
Things to notice:
- Binary pinning is the real seatbelt. A malicious skill running under
nodecannot quietly reachapi.anthropic.com— that's pinned to/usr/local/bin/claude. Even policies that share a host have per‑binary carve‑outs. - Sentry is explicitly half‑open.
POST sentry.io/**is blocked because Sentry is a multi‑tenant SaaS and any authenticated client can POST to any project — a path allowlist can't fix it, so the policy drops Claude Code crash telemetry on purpose (issue #1437).GETstays open because it has no request body. - GitHub is missing. That used to be in the base policy. Now
github.com/api.github.com/ thegitandghbinaries live only inpresets/github.yaml— you only get GitHub access if you explicitly opt in during onboard (issue #1583). - NemoClaw's
nim_serviceaddition (fromblueprint.yaml) addsnim-service.local:8000with full access when you pick thenim-localprofile.
4. Presets — opt‑in extensions
Each file in policies/presets/ is a small policy fragment you can add via nemoclaw <name> policy-add. For example, presets/telegram.yaml:
preset:
name: telegram
description: "Telegram Bot API access"
network_policies:
telegram_bot:
name: telegram_bot
endpoints:
- host: api.telegram.org
port: 443
rules:
- allow: { method: GET, path: "/bot*/**" }
- allow: { method: POST, path: "/bot*/**" }
- allow: { method: GET, path: "/file/bot*/**" }
binaries:
- { path: /usr/local/bin/node }
Notice /bot*/** — the bot token is in the URL path, but because OpenShell terminates TLS it can inspect and template it. The sandbox holds a placeholder token; the L7 proxy rewrites it to the real one stored in the OpenShell telegram provider.
"How do I open a new egress path?" — the decision tree
Is this a one‑off, exploratory, session‑local allow?
└── yes → let the agent try; it'll fail; approve in `openshell term`.
Session‑scoped, not persisted.
Is there already a preset for this integration?
└── yes → nemoclaw <name> policy-add (interactive, confirms endpoints)
└── yes → nemoclaw <name> policy-add --dry-run (preview first)
Is it a new integration you want saved for everyone?
└── add or edit a file in nemoclaw-blueprint/policies/presets/*.yaml
then nemoclaw onboard (rebuilds/retargets the sandbox)
Is it something the *baseline* should always allow?
└── edit nemoclaw-blueprint/policies/openclaw-sandbox.yaml
then nemoclaw onboard
(also works without rebuild for network-only changes:
openshell policy set <name> --policy file.yaml --wait)
Rule: prefer presets over editing the baseline. The baseline is the "least common denominator" everyone inherits; presets are the user‑visible opt‑in surface.
Approving live in openshell term
When the agent hits a blocked host, OpenShell logs it and surfaces it in the TUI:
openshell term
You see the destination host, port, binary, and HTTP method/path. You approve or deny. Approved endpoints are merged into the sandbox's running policy as a new durable revision and persist across sandbox restarts — they are not written back into your YAML file, but they do survive reboots of the sandbox. They only reset when you destroy and recreate the sandbox (e.g. via nemoclaw onboard). This matters in practice: if you approve a broad CDN once in a hurry, it stays approved until a recreate — so for anything you wouldn't want lingering, either deny it, or put it into a preset with appropriate scoping and re‑onboard.
Seeing the approval loop for the first time: the NemoClaw repo ships scripts/walkthrough.sh, which opens a split tmux session with the OpenShell TUI on the left and an agent on the right, so you can watch blocks appear and approve them live while the agent is actively trying things. It requires tmux and NVIDIA_API_KEY. This is the fastest way to feel how the approval loop works without having to build a whole scenario yourself.
Remote sandboxes: SSH in and run openshell term on the host, or port‑forward the gateway.
The four layers again, in "what do I do" form
| Layer | If I want to change it | How |
|---|---|---|
| Filesystem | Make a new path writable, or lock one down | Edit openclaw-sandbox.yaml filesystem_policy, run nemoclaw onboard (Landlock is locked at creation). |
| Process | Change user/group, seccomp profile | Edit openclaw-sandbox.yaml process:, run nemoclaw onboard. |
| Network | Add/remove hosts, methods, paths | Edit baseline or preset → nemoclaw onboard. Or openshell policy set for hot‑reload. Or approve live in openshell term. |
| Inference | Change backing model or provider | openshell inference set --provider <p> --model <m> (hot). Or rerun nemoclaw onboard to persist the choice via blueprint profile. |
Three "why is it designed this way" notes you'll appreciate
- Static vs dynamic policy lifecycles exist because Landlock and seccomp are kernel‑level and apply at process start; you can only tighten them at runtime, never loosen. OpenShell therefore locks filesystem/process at creation and keeps network/inference hot‑swappable.
- Binary pinning is not belt‑and‑suspenders — it's the thing that lets a single sandbox host several agents/tools (claude, openclaw, node, npm) without each one inheriting everyone else's reach. Policy scoping is by
binaries:path, not by a global "sandbox allowlist". - The sandbox trusts nothing it stores. All high‑value secrets live in OpenShell providers on the host. The sandbox holds placeholders. This is why
nemoclaw onboardexplicitly filtersDISCORD_BOT_TOKEN,SLACK_BOT_TOKEN,TELEGRAM_BOT_TOKEN, and provider API keys out of the sandbox creation command — so they can't leak as build args.
You've now got the concepts, the commands, and the policies. The back‑of‑napkin rule remains:
OpenClaw is the app. OpenShell is the jail. NemoClaw is how you put the app in the jail reproducibly, with NVIDIA's hardening defaults and a single command.
If you can run something in OpenClaw, you can run it in NemoClaw — either untouched inside the sandbox, or after adding a policy preset, or after flipping an inference profile. Doc 03 is the translation layer; this doc is the guardrails.
When you outgrow the presets
Everything above is the hand-curated NemoClaw view. When you need the full YAML schema — every field, every enforcement mode, every TLS option OpenShell supports — go upstream to the NVIDIA OpenShell docs:
- Policy Schema — complete YAML reference
- Sandbox Policies — applying, iterating, and debugging policies at the OpenShell layer
Next: 05-nemoclaw.md — deeper dive into the NemoClaw integration layer (plugin, blueprint, onboarding lifecycle).