Fine-grained, deterministic permissions for OpenClaw agents.
⚠️ Status: proof-of-concept. This project is currently experimental and will change wihout notice. File an issue if you are planning to use this for a production use case.
By default, an OpenClaw agent has full agency, and the only guardrail most setups have is
whatever gets written into an AGENTS.md prompt — which is non-deterministic: the same
rule can support two equally defensible readings depending on how the model interprets it
that turn (see the Crustodian example below).
Two questions worth asking about any OpenClaw agent you run: do you know what it can do right now? And are you confident it can't do anything else? For most OpenClaw setups, the honest answer to the second question is no.
SpiceClaw is OpenClaw (an open-source assistant that runs on your own machine) plus SpiceDB (a purpose-built, Zanzibar-inspired permissions database maintained by AuthZed): a plugin that gives OpenClaw agents fine-grained, deterministic permissions. An agent keeps every capability OpenClaw already gives it — SpiceClaw doesn't remove features — but it can't use a tool without an explicit grant, and it can't be prompted into an action nobody pre-authorized. It does not fork OpenClaw or manage its process lifecycle: you point an existing OpenClaw instance at it.
Linux with systemd only, today. spiceclawd is supervised via systemctl; there is
no launchd implementation yet, so the daemon half of the install does not run on macOS.
You need Node 22+, pnpm (corepack enable), and Docker (for the SpiceDB
sidecar).
If yes — your OpenClaw needs to be at or above the plugin's compat floor
(2026.6.9) and must have already generated its own config
(~/.openclaw/openclaw.json by default, or set OPENCLAW_CONFIG_PATH) — spiceclaw install refuses if that file doesn't exist yet. Skip to step 2.
If no — install OpenClaw
spiceclaw install will not perform this silently on your behalf — it fails loudly with
this exact guidance if either is missing:
sudo useradd --system --no-create-home --shell /usr/sbin/nologin spiceclaw-admin
sudo groupadd spiceclaw-relay
sudo usermod -aG spiceclaw-relay spiceclaw-admin
sudo usermod -aG spiceclaw-relay <the-user-openclaw-itself-runs-as>SpiceClaw retrofits onto OpenClaw locally, so this checkout needs to live on the same machine as the OpenClaw instance you're pointing it at:
git clone https://github.com/authzed/spiceclaw.git
cd spiceclaw
pnpm install && pnpm -r build
bash scripts/install-cli.sh # symlinks the built CLI into a bin dir on your PATHWithout the symlink step, invoke in place via pnpm --filter @spiceclaw/cli exec spiceclaw <cmd>.
The CLI resolves the plugin, the daemon build, the compose files, and the SpiceDB schema
relative to this checkout — keep it in place after installing, don't move or delete it.
spiceclawd (step 4) runs as its own dedicated spiceclaw-admin OS user, unrelated to
yours, and needs to traverse into this checkout to load its built admind/dist/main.js
— traverse only, not read your other files. If your home directory is 750 or tighter
(common on hardened hosts), that traversal fails with a misleading Cannot find module
error. Fix once, per host:
chmod o+x "$HOME" # lets other users pass THROUGH your home dir; doesn't make it listableCloning somewhere world-traversable instead (e.g. /opt/spiceclaw) avoids this entirely.
Monitor-first, no-lockout, reversible. Always pass --gateway-user (or
set SPICECLAW_GATEWAY_USER) to the OS user OpenClaw's own gateway process runs as —
install refuses outright if that resolves to root:
sudo mkdir -p /var/log/spiceclaw && sudo chown spiceclaw-admin:spiceclaw-admin /var/log/spiceclaw
sudo env "PATH=$PATH" OPENCLAW_CONFIG_PATH="$HOME/.openclaw/openclaw.json" \
spiceclaw install --owner <your-user-id> --gateway-user <openclaw-user> \
--audit-log /var/log/spiceclaw/audit.log
spiceclaw uninstall # (later) rolls back to vanilla OpenClaw (config restored byte-for-byte)--audit-log is optional but worth setting now: install bakes whatever's given at
install time into spiceclawd's unit, and step 7's review step has nothing to show
without it. (SPICECLAW_AUDIT_LOG still works too, as a lower-precedence fallback.)
Not bare sudo spiceclaw install: sudo resets both PATH and HOME by default, and
this command depends on each surviving from your shell, not root's —
PATH— step 3'sinstall-cli.shalways symlinks into a per-user bin dir (npm prefix -g's bin,~/.npm-global/bin, or~/.local/bin) so it never needs root to install itself, but that means it's on your PATH, notsudo's ownsecure_path; without it,sudo spiceclaw installfails outright with "command not found."HOME—installdefaults--configto~/.openclaw/openclaw.json; under a baresudo,~resolves to/root, not your real OpenClaw config.$HOMEabove is expanded by your own shell beforesudoruns, so it still names your real home directory even though the command executes as root.
There's no separate step for SpiceDB — this one command brings it up for you, along
with everything else: preflight-checks your OpenClaw version, starts the SpiceDB sidecar
(docker compose -f docker/spicedb-compose.yml up) and loads its schema, generates
spiceclawd's credential and launches it in monitor mode, seeds you as owner, syncs
OpenClaw's tool policy into the graph, and registers the plugin in OpenClaw's config.
It prints the ONE variable the plugin needs in OpenClaw's own process environment —
SPICECLAW_ADMIND_SOCKET_PATH — and nothing SpiceDB-shaped at all: the plugin holds zero
SPICEDB_* variables and talks to spiceclawd's check-relay over that socket instead of
dialing SpiceDB directly.
Just want the SpiceDB +
spiceclawdstack running on its own, without retrofitting an existing OpenClaw?spiceclaw up [--owner <id>]brings up the same sidecar + schema + daemon by itself — see Command reference.
spiceclaw install does not restart OpenClaw. Set the variable step 4 printed
wherever OpenClaw itself loads its env — its systemd unit's Environment=, an env file it
sources, docker run -e ... if it's containerized — and restart it through whatever
mechanism you normally use.
sudo env "PATH=$PATH" OPENCLAW_CONFIG_PATH="$HOME/.openclaw/openclaw.json" \
spiceclaw doctor --gateway-user <openclaw-user> --openclaw-version <your-openclaw-version>Same PATH/HOME reasons as step 4. --gateway-user matters here too: without it,
doctor under sudo resolves to root and several checks fail as a result — it loudly
caveats this rather than guessing, but it's easy to misread as a real problem.
--openclaw-version (get it from openclaw --version) is optional but worth passing:
without it, the version check [WARN]s that nothing was actually verified, rather than
reporting a real compatibility result.
Then check OpenClaw's own gateway log for these two signals:
registered tool: request_elevation
and spiceclaw-authz listed in the gateway's own plugin-count log line
(http server listening (N plugins: ..., spiceclaw-authz, ...)).
spiceclaw audit --denied-only --log /var/log/spiceclaw/audit.log # (or rely on SPICECLAW_AUDIT_LOG)
sudo env "PATH=$PATH" spiceclaw mode enforce # prompts; add --yes for non-interactiveaudit needs --log/SPICECLAW_AUDIT_LOG pointed at whatever path you set in step 4 —
without it, there's nothing to review. mode needs sudo (it rewrites spiceclawd's unit
file directly) and your real PATH, same reasons as step 4 — but not OPENCLAW_CONFIG_PATH
or --gateway-user; neither applies to this command. At a real terminal (not a script),
drop --yes and you'll get a Type 'yes' to confirm: prompt instead — that's invariant 7's
human-consent gate, not a bug.
With enforcement on, ask the agent to run something destructive it doesn't already hold
a grant for — an exec, a file write. Instead of a hard deny, the agent drafts a
time-boxed grant and asks a human to approve it right in the same chat:
agent: (calls request_elevation for "exec", ttlSeconds=900)
→ "still valid for 15m — reply /approve <planId> in this chat,
or /approve if it's your only pending request"
human: /approve
→ "approved elevation-request <planId>."
Or work it from a terminal instead of chat:
spiceclaw admin pending [--agent <id>] [--kind permission-change|elevation-request]
spiceclaw admin approve <planId>
spiceclaw admin reject <planId> [--reason <text>]Effective agent permissions need to live outside OpenClaw's reach, decide deterministically, apply just-in-time, and be expressive enough to scope a grant to one tool, one argument shape, one time window. OpenClaw's own policy is static config that governs what an agent can see — tool visibility. SpiceClaw is the layer underneath it that governs what an agent can actually execute.
flowchart LR
channels["channels / clients<br/>Telegram · Discord · Slack<br/>Signal · WhatsApp · Control UI"]
subgraph openclaw["OpenClaw (unmodified)"]
plugin["SpiceClaw plugin (in-process)<br/>channel admission · tool dispatch (+memory)<br/>outbound send · delimiter wrapping"]
end
admind["spiceclawd (admin-relay daemon)<br/>own OS user · holds the<br/>write-capable SpiceDB credential"]
spicedb[("SpiceDB sidecar<br/>schema · checks · grants")]
cli["spiceclaw CLI<br/>(own operator credential)"]
channels -- dial-out admission --> plugin
plugin -- "unix socket (check-relay —<br/>no SpiceDB creds in this process)" --> admind
admind --> spicedb
cli -- admin reads / confirm-gated writes --> spicedb
- Plugin (in-process) — the agent-loop seams: dial-out channel admission, tool
dispatch, memory read/write (memory is registered as tools, so
before_tool_callgates it), outbound send, and untrusted-output delimiters — via OpenClaw's plugin hook surface. It performs every check by askingspiceclawdover a unix socket; it never opens a SpiceDB connection itself and never writes the graph. spiceclawd(standalone daemon) — the only process that holds a write-capable SpiceDB credential. It serves the plugin's read-only check-relay, owns the out-of-band enforcement-mode toggle, and applies chat-approved permission changes (spiceclaw admin approve/reject) atomically.- SpiceDB sidecar + a shared
authzmodule — the one place all authorization logic lives (schema, checks, grants); the plugin andspiceclawdare thin call-sites into it. The CLI talks to SpiceDB directly under its own operator credential for admin reads and confirm-gated writes.
- Permissions live outside OpenClaw's reach.
spiceclawdruns as its own OS user and is the only process holding SpiceDB credentials — a fully compromised OpenClaw gateway process still can't read or forge a grant. - Decisions are deterministic. No model is ever asked "should this be allowed?" — every grant is written ahead of time, out of band, by a human or an approved plan; the plugin's runtime check is a graph lookup, not an inference.
- Expressive enough for what OpenClaw actually does. OpenClaw's static config sets tool visibility; SpiceClaw sets the grants underneath it — scoped per tool, with optional per-argument limits and TTLs, effective immediately, no gateway restart required.
Configuration is environment-driven. The essentials (full table in
INSTALL.md §Reference):
| Var | Meaning | Default |
|---|---|---|
SPICEDB_ENDPOINT |
SpiceDB gRPC endpoint, read by spiceclawd/the CLI (never by the plugin, which holds zero SPICEDB_* vars) |
— |
SPICEDB_TOKEN |
SpiceDB preshared key for the sidecar (generate a real one; rotate the dev placeholder) — shared by the sidecar itself and the CLI's own seed/migrate/check/etc. commands. Not spiceclawd's write-capable credential — that is a separate secret (ADMIND_SPICEDB_TOKEN at generation time), delivered to spiceclawd via its own dedicated 0600 file, never this env var (see "Secrets are generated" below) |
"" |
POSTGRES_PASSWORD |
Postgres datastore password for the SpiceDB sidecar (dev default spiceclaw-pg-dev; set a real one before any network-exposed deployment) |
spiceclaw-pg-dev |
SPICEDB_FLOOR_MODE |
fail-closed floor (deny or owner-only — never wider) |
deny |
SPICECLAW_DEMO |
1 enables the out-of-band mode toggle (non-production only) |
unset (pinned enforce) |
SPICECLAW_PROFILE |
production refuses demo outright |
unset |
Secrets are generated, never defaulted. spiceclaw install resolves + persists the
SpiceDB preshared key and postgres password into a 0600 docker/secrets/spiceclaw.env
(generated values shown exactly once; a re-run reuses the file), and generates
spiceclawd's own separate write-capable credential. Real values (not the dev
placeholders) are required before any network-exposed deployment.
Since install itself runs under sudo (step 4), that 0600 file would otherwise land
root-owned — even though it lives in your own checkout and every operator command above
(check, inspect, who-can, grant, revoke, ...) is meant to use it as you, not
root. install chowns it back to the invoking user automatically (via sudo's own
SUDO_UID/SUDO_GID), so this needs no manual fixup.