Skip to content

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:

Terminal window
openclaw plugins enable policy

Then point it at your authored policy file (config lives under plugins.entries.policy.config):

openclaw.json
"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

Terminal window
openclaw policy check # human output
openclaw policy check --json # structured findings + attestation
openclaw policy check --severity-min error # gate on errors only

The 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:

SelectorSupports
agentIdstools.*, agents.workspace.*, sandbox.*
channelIdsingress.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 agentId isn’t in agents.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 on exec.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 (no gateway, car-control, voice-call, raw process, or workspace mutation), and the exact alsoAllow grant 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