Policy Conformance
Policy Conformance
The bundled Policy plugin turns your security posture into a versioned,
machine-checked contract. You write the rules you want enforced into a
policy.jsonc file; openclaw policy check reads the live config + workspace as
evidence and reports any drift. It is observe-only — it surfaces findings,
it does not rewrite runtime behavior — which makes it safe to run on a schedule
and safe to wire into CI.
Lobster uses it as stage 1 of a daily security audit (see Audit Logging and Security Hardening). It replaced a hand-rolled shell script that compared every agent’s tool/exec config against a fixture — the policy engine now expresses that per-agent matrix natively.
What it checks (and what it doesn’t)
The engine is a config-conformance tool. It observes:
- Secrets posture (SecretRef sources, declared providers)
- Auth profile metadata and modes
- Gateway exposure (bind, auth, Tailscale Funnel, Control UI, HTTP endpoints)
- Network SSRF escape hatches
- MCP servers and model providers (allow / deny)
- Channel provider enablement and DM/group ingress posture
- Per-agent tool posture — profile,
alsoAllow,deny,exec.security,exec.host,exec.ask,elevated, filesystem workspace-only - Per-agent sandbox posture — required mode, allowed backends, container mount/network/namespace rules
- Agent workspace access (
none/ro/ …)
It does not read runtime/operator state such as exec-approvals.json, the
gateway log, the cron store, or the filesystem layout of your skills. Those are
not config conformance and stay outside the engine. (For Lobster, a thin
companion script still covers that operational floor.)
Enable the plugin
policy is a bundled optional plugin and is allowlist-gated like every bundled
plugin:
openclaw plugins enable policyThen point it at your authored policy file (config lives under
plugins.entries.policy.config):
"plugins": { "allow": ["policy", /* … */], "entries": { "policy": { "enabled": true, "config": { "enabled": true, // absolute path is the most reliable; a workspace-relative // default does not always resolve under launchd/cron. "path": "/Users/you/.openclaw/policy.jsonc" } } }}Run a check
openclaw policy check # human outputopenclaw policy check --json # structured findings + attestationopenclaw policy check --severity-min error # gate on errors onlyThe same findings also appear in openclaw doctor --lint once the plugin is
enabled, so a single doctor run covers config health and policy drift.
Clean output looks like this:
{ "ok": true, "checksRun": 52, "checksSkipped": 0, "findings": [], "attestation": { "policy": { "path": "policy.jsonc", "hash": "sha256:…" }, "workspace":{ "scope": "policy", "hash": "sha256:…" }, "findingsHash": "sha256:…", "attestationHash": "sha256:…" }}A check runs only when the matching rule is present in policy.jsonc. An
empty policy file runs nothing; you opt into each guarantee by authoring its
rule.
Authoring policy.jsonc
Every field is optional. The observed state is whatever is already in your OpenClaw config; the rule declares what’s allowed. A selection of the most useful rules:
{ // ── Secrets ────────────────────────────────────────────── "secrets": { "requireManagedProviders": true, // every SecretRef → a declared provider "allowInsecureProviders": false, "denySources": ["exec"] // forbid exec-sourced secrets },
// ── Auth profiles ──────────────────────────────────────── "auth": { "profiles": { "requireMetadata": ["provider", "mode"], "allowModes": ["api_key", "oauth", "token"] } },
// ── Gateway exposure ───────────────────────────────────── "gateway": { "exposure": { "allowNonLoopbackBind": false, "allowTailscaleFunnel": false }, "auth": { "requireAuth": true, "requireExplicitRateLimit": true }, "controlUi":{ "allowInsecure": false }, "remote": { "allow": false }, "http": { "denyEndpoints": ["chatCompletions", "responses"] } },
// ── Tool posture (global) ──────────────────────────────── "tools": { "profiles": { "allow": ["minimal"] }, "exec": { "allowSecurity": ["deny", "allowlist"], // forbid "full" "allowHosts": ["gateway"], "requireAsk": ["always"] }, "elevated": { "allow": false }, // require elevated disabled "denyTools": ["group:runtime", "group:fs"] // require these denied }}See the full rule reference at
docs.openclaw.ai/cli/policy — it also
covers mcp.servers.allow/deny, models.providers.allow/deny,
channels.denyRules, network.privateNetwork.allow, and
agents.workspace.allowedAccess.
Per-agent overlays (scopes)
Top-level rules apply fleet-wide. When one set of agents needs stricter policy than the baseline — the classic “main agent is permissive, the family-facing agents are locked down” split — use a named scope:
{ // permissive fleet-wide envelope: the union of every legitimate value, // including any intentional outliers (e.g. a trusted node agent on host // "node", or a deny-by-default agent). "tools": { "exec": { "allowSecurity": ["allowlist", "full", "deny"], "allowHosts": ["gateway", "node", "auto"] } },
"scopes": { "restricted": { "agentIds": ["family-agent", "groups-agent"], "tools": { "exec": { "allowSecurity": ["allowlist"], "allowHosts": ["gateway"] }, "denyTools": ["write", "edit", "browser", "cron", "gateway", "sessions_spawn"], "alsoAllow":{ "expected": ["read", "message", "web_search", /* exact list */ ] } } }, "lockdown-sandbox": { "agentIds": ["release-agent"], "sandbox": { "requireMode": ["all"], "allowBackends": ["docker"] } } }}Selectors and the sections they support:
| Selector | Supports |
|---|---|
agentIds | tools.*, agents.workspace.*, sandbox.* |
channelIds | ingress.channels.* |
Key rules of the overlay system:
- Additive. Global claims still run; a scope adds its own claims against the same observed config.
- Strictness, not replacement. A scoped value must be equally or more
restrictive than any baseline or earlier scope for the same field — allow-
lists are treated as subsets, deny-lists as supersets, booleans as fixed
requirements. A weaker scoped value is rejected as
policy/policy-jsonc-invalid. This is what prevents a scope from quietly loosening policy. - Inheritance. If an
agentIdisn’t inagents.list[], the scoped rule is evaluated against the inherited global/default posture for that id. - An agent may appear in multiple scopes as long as each governs different fields.
The strictness model is why the global envelope is deliberately loose: it must admit every agent’s legitimate value (so a trusted full-trust agent passes), while the scopes carry the real per-agent guarantees.
Sandbox posture
sandbox.requireMode / allowBackends / containers.* enforce the presence
of sandboxing. There is intentionally no “require mode == off” rule — an absent
or off sandbox is the permissive default and needs no assertion. If your
security model is not sandbox-based (isolation via exec-allowlist + host
routing + tool-deny instead), you simply don’t author sandbox rules. When a
container rule applies to a backend that can’t expose that field, the engine
emits policy/sandbox-container-posture-unobservable rather than silently
passing.
Locking the policy (attestation)
Each check emits a stable attestationHash over the policy + evidence +
findings. Record it, or pin it so the gateway refuses to start if the policy
file or the observed posture drifts from the approved snapshot:
"plugins": { "entries": { "policy": { "config": { "expectedHash": "sha256:…", // the policy file itself "expectedAttestationHash": "sha256:…" // policy + evidence + findings}}}}How Lobster uses it
Lobster’s policy.jsonc (versioned in the repo at config/policy.jsonc) sets:
- The global rules above (secrets, auth, gateway).
- A loose global tool-posture envelope that admits the intentional outliers (a
trusted
node-hosted agent onexec.security=full, a deny-by-default planner). - Per-agent scopes pinning the main agent and each family/group agent to
exec.security=allowlist+exec.host=gateway, a required tool-deny set (nogateway, car-control, voice-call, rawprocess, or workspace mutation), and the exactalsoAllowgrant list so any unreviewed tool addition shows up as a finding.
openclaw policy check runs daily and reports 0 findings / 52 checks on a
clean state. Anything that drifts — an agent silently flipped to full, a new
tool added to an allowlist, the gateway exposed to Funnel — turns the run red and
pages the owner.
See also
- Security Hardening Guide — the broader checklist
- Secrets Management — the SecretRef posture the policy enforces
openclaw path— edit singlepolicy.jsoncleaves without disturbing comments- docs.openclaw.ai/cli/policy — upstream rule reference and findings catalog