Tamper-evident audit trail for AI coding agent activity. Records every session, tool invocation, and prompt exchange into a local SQLite database with SHA-256 hash chain integrity, so you can verify that no events were altered or deleted after the fact.
#install from npm
openclaw plugins install @constellation-network/gate-oc-audit
#run the interactive configuration tool
openclaw audit setup
| Job | CLI | SPA (in the gateway UI) | What you get |
|---|---|---|---|
| Confirm the plugin is healthy | openclaw audit status |
||
#/status (default route) |
|||
| One-screen snapshot: storage, integrity, anchor, file-watch, inventory, last security scan | |||
| See today's / this week's activity | openclaw audit report daily / β¦ weekly |
||
#/reports/daily , #/reports/weekly |
|||
| Activity, top tools, LLM spend, outbound messaging, anomalies, integrity footer | |||
| Per-cron rollup | openclaw audit report cron <job-id> |
||
#/reports/cron?jobId=β¦ |
|||
| One row per execution: started/ended, status, tool/LLM/outbound counts | |||
| Per-conversation rollup | openclaw audit report session <id> |
||
#/reports/session/<id> |
|||
| Timeline, tools, LLM cost, outbound, integrity | |||
| Surface anomalies | openclaw audit anomalies --since 24h |
||
#/anomalies |
|||
| Tamper, duplicate-outbound, denial spikes, installs, first-seen tools | |||
| Track LLM spend | openclaw audit spend --by model --since 7d |
||
#/spend |
|||
Token usage and costUsd grouped by provider / model / day / session |
|||
| Browse installed plugins/skills/tools/crons | openclaw audit inventory [kind] |
||
#/inventory |
|||
| Summary counters + per-kind tables (cron rows link to per-cron rollup) | |||
| Prove an event happened | openclaw audit smt proof <hash> then β¦ smt verify |
||
#/smt-tools |
|||
| Inclusion proof against tree roots and DE-anchored checkpoints | |||
| Independent third-party proof | Configure Digital Evidence anchoring (see below) | β | SMT roots anchored on the Constellation Digital Evidence network |
| Alert on file changes | Configure fileWatchPatterns + notificationWebhook |
||
| β | Slack/Discord/webhook ping when a watched path changes | ||
| Daily/weekly digests to a channel | Configure reportWebhook |
||
| β | Slack-compatible payload with the same projection as audit report |
||
| Export the trail for compliance | openclaw audit export csv --from β¦ --to β¦ |
||
#/events β Download button |
|||
| Streamed NDJSON or CSV with anchor references per row | |||
| Re-scan for tampering | openclaw audit verify |
||
#/verify |
|||
| Full SMT replay + DE checkpoint consistency check; exit 0 if clean |
If you don't know where to start, run openclaw audit status
after install β or open the SPA at openclaw audit ui
and land on the same snapshot at #/status
. It tells you everything that's wired up and what isn't.
Every SPA route is backed by an HTTP endpoint under /plugins/audit/api/
. The endpoints introduced alongside the SPA views (/status
, /anomalies
, /spend
, /inventory
, /report/session/:id
, /smt/proof
, /smt/verify-proof
, /smt/chain
) follow the same loopback policy as /api/report
β 403
outside loopback unless allowExportOnNonLoopback: true
is set. The wire JSON is byte-identical to the matching CLI --json
output, so dashboards can pin against the same schemas.
Reaching the UI from outside loopback.By default the routes register withauth: "plugin"
(no verification) and lean on the loopback bind for safety. To expose the UI/API on a shared network, setrequireGatewayAuth: true
so the routes register withauth: "gateway"
and the openclaw gateway authenticates every request:
openclaw config set plugins.entries.gate-oc-audit.config.requireGatewayAuth true
{ "config": { "requireGatewayAuth": true } }
Because the gateway then authenticates every caller,
requireGatewayAuth
subsumes the loopback gate: status, reports, anomalies, spend, inventory, SMT tools, export, and verify all serve normally off-loopback, no further opt-in needed. This is the recommended single knob for external access. TheallowExportOnNonLoopback
/allowVerifyOnNonLoopback
flags exist for the narrower case of exposing those routes off-loopbackwithoutgateway auth (e.g. behind your own reverse proxy) β leave them off whenrequireGatewayAuth
is set.
openclaw plugins install @constellation-network/gate-oc-audit
Requires openclaw >= 2026.4.24
as a peer dependency and Node.js β₯ 22.13 (uses the built-in node:sqlite
module).
That's it. The plugin automatically starts recording audit events when your agent runs.
openclaw audit setup
Interactive wizard that walks through everything the plugin needs:
- Adds
gate-oc-audit
toplugins.allow
(trusts the plugin). - Enables
hooks.allowConversationAccess
soprompt.input
/prompt.response
/agent.end
events are captured. - Optionally collects your Digital Evidence API key + org/tenant UUIDs and tunes the anchoring thresholds (event count, timer interval, minimum events per tick).
- Runs
openclaw audit status
at the end so you can confirm the trail is live.
Pass --yes
to accept defaults for the anchoring tuning (DE identity prompts still ask). Once the wizard finishes, skip to Quick check after install.
Two operator-policy opt-ins are required for full functionality. Both are decisions openclaw forces on the operator β the plugin cannot self-grant either. The setup wizard above writes both for you; the sections below document the equivalent openclaw config set
calls and JSON snippets for operators who provision config by hand or via configuration management.
Add gate-oc-audit
to plugins.allow
. When plugins.allow
is empty, openclaw still auto-loads discovered plugins but logs a warning on every startup:
[plugins] plugins.allow is empty; discovered non-bundled plugins may auto-load β¦
Setting an explicit allowlist silences the warning and locks down to the listed ids:
openclaw config set plugins.allow '["gate-oc-audit"]'
Or directly in the config JSON:
{
"plugins": {
"allow": ["gate-oc-audit"]
}
}
If you already have other trusted plugins in plugins.allow
, append "gate-oc-audit"
to the existing array rather than replacing it.
Non-bundled plugins must explicitly opt in to receive raw conversation content from the llm_input
, llm_output
, before_agent_finalize
, and agent_end
hooks. Without this opt-in, openclaw blocks those hook registrations and logs:
[plugins] typed hook "llm_input" blocked because non-bundled plugins must set plugins.entries.gate-oc-audit.hooks.allowConversationAccess=true β¦
Three of the audit plugin's most important event types (prompt.input
, prompt.response
, agent.end
) will be missing from the audit trail until this is set.
openclaw config set plugins.entries.gate-oc-audit.hooks.allowConversationAccess true
Or directly in the config JSON:
{
"plugins": {
"entries": {
"gate-oc-audit": {
"hooks": { "allowConversationAccess": true }
}
}
}
}
The plugin also logs a warning at startup if a tool call is observed without any preceding llm_input
event, which usually indicates this opt-in is missing.
openclaw audit status
This is the single best command to confirm the plugin is recording. It prints one screen covering:
Storageβ DB size vs cap, event count, oldest event, next prune** Integrity**β sequence head, SMT trees + root, last checkpoint, conversation-hook state (ENABLED
/DISABLED
/ENABLED-but-silent
if noprompt.input
seen in 24h)Digital Evidence anchorβ active / inactive, anchors today, last anchor tx hash, circuit-breaker state** File watching**β patterns watched / ignored, changes in last 24h** Inventory**β plugins / skills / tools / cron counts** Last security scan**β timestamp + finding counts
Add --json
for a single-line, machine-readable snapshot. If Conversation hook
shows DISABLED
, the operator hasn't set allowConversationAccess
(see above) and prompt.input
/ prompt.response
/ agent.end
events are missing from the trail.
All commands open the audit DB read-only via SQLite WAL, so they coexist with the running gateway without lock contention.
See Quick check after install above. Use --json
to pipe to jq
.
openclaw audit list
openclaw audit list --last 20
openclaw audit list --type tool.invoked
openclaw audit list --category prompt --session <session-id>
Verify SMT proofs for recent events and check DE checkpoint consistency:
openclaw audit verify
Exits with code 0
if all proofs and checkpoints are valid, 1
if any verification fails.
openclaw audit inventory # summary across plugins/skills/tools/crons
openclaw audit inventory plugins # detail for one kind
openclaw audit inventory skills --json
Generate a daily or weekly activity digest with inline anomaly detectors. The projection covers Activity / Cron schedule / Top tools / LLM spend / Outbound messaging / Anomalies / Integrity, rendered as human text (default), single-line JSON (--json
), or a self-contained HTML document (--html
).
openclaw audit report daily # today (UTC), human text
openclaw audit report daily --date 2026-05-17 # specific UTC day
openclaw audit report daily --tz local # use local-time day boundary
openclaw audit report weekly # this ISO week (UTC)
openclaw audit report weekly --week 2026-W19 # specific ISO week
openclaw audit report daily --json # single-line JSON
openclaw audit report daily --html > report.html # standalone HTML
openclaw audit report cron <job-id> # per-cron rollup, one row per execution
openclaw audit report cron <job-id> --last 5 --json
openclaw audit report cron <job-id> --html > cron.html
openclaw audit report session <session-id> # per-conversation rollup
Detector knobs (capped on both CLI and HTTP):
| Flag | Default | Max | Description |
|---|---|---|---|
--dup-window-sec |
|||
60 |
|||
3600 |
|||
R5a duplicate-outbound: sha256-equal message.sent within this window to the same channel + recipient is flagged |
|||
--lookback-days |
|||
30 |
|||
365 |
|||
| R5b first-seen-tool: tools invoked in the window but absent from this trailing day count are flagged | |||
--top-tools |
|||
10 |
|||
1000 |
|||
| Cap for the Top tools section |
Anomaly detectors emitted in the anomalies
block:
R5a duplicate outboundβ same content hash sent to the same channel + recipient inside--dup-window-sec
.duplicateOutboundTruncated: true
indicates the underlyingmessage.sent
scan hit its 100k-row cap and a duplicate beyond that point could have been missed.R5b first-seen toolsβ tool names invoked in the window that did not appear in the prior--lookback-days
window. Calendar-day arithmetic in the report's timezone keeps the lookback DST-tolerant.
The Integrity footer pins the report to a sequence point: last event id / sequence / content_hash
, plus the last DE-anchored checkpoint (id, smtRoot
, deTxHash
, sequence range, createdAt
) when one exists. A consumer can cross-check the footer against openclaw audit verify
to confirm the report covers a tamper-evident slice of the trail.
The Cron schedule section lists the cron jobs configured for openclaw on the machine the report was generated on, so an operator can see the configured schedule alongside the in-window execution counters. Primary source is openclaw's canonical store at <openclawDir>/cron/jobs.json
(one entry per id
in the jobs[]
array); legacy <jobId>.cron.*.json
per-file manifests in the openclaw root are also surfaced as a fallback and merged in for ids not already covered by jobs.json
. Each entry shows the job name and a compact schedule string (cron <expr> (<tz>)
, every <ms>
, at <iso>
, or unknown (<raw>)
when the schedule
field doesn't parse). Symlinked, oversized (>1 MiB for jobs.json
, >64 KiB for legacy manifests), and structurally invalid sources are deliberately skipped so a stray file can't redirect or DoS the report. Reports generated without filesystem access emit configured: []
.
Per-cron rollup (R9). openclaw audit report cron <job-id>
projects the trail as one row per cron execution for a given jobId
, newest first. Each row pairs the cron.executed
event with its matching agent.end
(by sessionId
metadata.runId
) and attributes tool / LLM / outbound-message activity that fired on the same session between the two timestamps. --last N
(default 20, max 1000) bounds the rollup; when the store has more executions than fit, truncated: true
is surfaced in all output formats. When a matching entry exists in openclaw's cron store (<openclawDir>/cron/jobs.json
, or a legacy <jobId>.cron.*.json
file as fallback), its schedule is inlined in the rollup header (Schedule: cron <expr> (<tz>)
, etc.); manifest: null
when nothing matches or no openclawDir
was supplied. Output is human text (default), single-line JSON (--json
), or a self-contained HTML document (--html
). The JSON shape is published at schemas/audit-cron-rollup.schema.json
so dashboards can pin against schemaVersion: 1
.
The same projection is available over HTTP at GET /plugins/audit/api/report?period=daily|weekly&date=&week=&tz=&format=json|html&dupWindowSec=&lookbackDays=&topTools=
. The JSON shape is published at schemas/audit-projection.schema.json
so dashboards can pin against schemaVersion: 1
.
HTTP endpoint and loopback.Like/api/export
, the report route is unauthenticated and returns403
when the gateway binds beyond loopback unlessallowExportOnNonLoopback: true
is set. The CLI reads the local DB directly and is unaffected.
openclaw audit spend [--by provider|model|day|session] [--since 24h] [--until now] [--limit 1000] [--json]
aggregates prompt.response
metadata across a time window, grouping by provider, model, calendar day, or session id. Token columns include input, output, cache-read, and cache-write counts; costUsd
is summed verbatim from the per-event metadata.costUsd
.
Default grouping is model
; bucket labels for that mode are formatted as provider/model
(e.g. openai/gpt-5
) so cross-provider model name collisions don't merge. --by day
buckets are always UTC dates regardless of --tz
β the formatter prints a note when --tz local
is used with --by day
. --limit
(default 1000, max 100000) caps the number of buckets; when the cap trims a result, truncated: true
appears in all outputs. The JSON shape is published at schemas/audit-spend.schema.json
. Totals match the daily report's LLM-spend section when the windows align.
openclaw audit export # JSON Lines (default, streamed)
openclaw audit export csv # CSV (streamed, stable column order)
openclaw audit export --type tool.invoked --limit 100 # cap rows
openclaw audit export --from 2025-01-01T00:00:00Z --to 2025-02-01T00:00:00Z
openclaw audit export --security-only # security / config / system categories
openclaw audit export --include-content # include decompressed content column / field
Each emitted row carries the DE anchor reference (anchor.deTxHash
, anchor.smtRoot
, anchor.sequenceStart
, anchor.sequenceEnd
, anchor.createdAt
) for the checkpoint covering its sequence, or null
when no DE-anchored checkpoint covers it yet. Output is streamed in fixed-size batches via a sequence cursor, so retention pruning during the export can't shift the window and silently drop rows. The same shape is available over HTTP at GET /plugins/audit/api/export?format=json|csv&from=&to=&type=&category=&session=&securityOnly=&includeContent=&limit=
.
--include-content
and redaction.redactPromptText
rewrites prompt / message content tosha256:<hex>
before insert, andredactToolArgs
does the same for tool-call arguments. Neither switch coverstool.result
content (tool stdout / stderr / output bodies). If you set both flags and runaudit export --include-content
, the prompt and message bodies are hashed but tool outputs are still emitted verbatim. Operators that need a fully redacted export should either (a) skip--include-content
, or (b) filter--category
away fromtool
events.
HTTP endpoint and loopback.GET /plugins/audit/api/export
is unauthenticated; the plugin relies on the gateway being bound to loopback (gateway.bind: "loopback"
, the default) for safety. When the gateway binds beyond loopback the export route returns403
unless you explicitly opt in:
openclaw config set plugins.entries.gate-oc-audit.config.allowExportOnNonLoopback true
{ "config": { "allowExportOnNonLoopback": true } }
The CLI (
openclaw audit export β¦
) is unaffected β it reads the local DB directly and doesn't traverse the HTTP gate.
openclaw audit smt root # Show current SMT root and entry count
openclaw audit smt root --tree <key> # Root for a specific tree
openclaw audit smt trees # List all SMT trees
openclaw audit smt proof <hash> # Generate inclusion/exclusion proof for a hash
openclaw audit smt proof <hash> --tree <key>
openclaw audit smt verify --proof '<json>' # Verify a proof against known tree/checkpointed roots
openclaw audit smt chain <conversationId> --tree <key>
Proof verification checks both internal consistency (siblings hash to the claimed root) and root legitimacy . A self-consistent proof with an unknown root is rejected.
Exit codes for smt verify
:
| Exit code | Meaning |
|---|---|
| 0 | Proof is valid β internally consistent and root matches a known anchor |
| 1 | INVALID β root not recognized by this node, or proof is internally inconsistent |
| 2 | UNVERIFIABLE β no SMT trees or DE checkpoints exist to verify against |
Live-root window: proofs are verified against current tree roots and DE-checkpointed roots. When a new event advances the tree from root R1 to R2, proofs generated at R1 will be rejected unless a checkpoint captured R1. On active systems there is always a brief window between tree advancement and the next checkpoint where recently-generated proofs cannot be verified. To avoid this, verify proofs before appending new events, or ensure the checkpoint interval is short enough for your use case.
Config key:in openclaw config this plugin lives undergate-oc-audit
(the manifest id),notthe npm package name@constellation-network/gate-oc-audit
. Openclaw logs a warning about the mismatch on load β it's expected and safe to ignore.
Set values via the CLI:
openclaw config set plugins.entries.gate-oc-audit.enabled true
openclaw config set plugins.entries.gate-oc-audit.config.dbPath "$HOME/.openclaw/audit.db"
openclaw config set plugins.entries.gate-oc-audit.config.localRetentionDays 365
openclaw config set plugins.entries.gate-oc-audit.config.localMaxSizeMb 500
Or directly in the config JSON:
{
"plugins": {
"entries": {
"gate-oc-audit": {
"enabled": true,
"config": {
"dbPath": "~/.openclaw/audit.db",
"localRetentionDays": 365,
"localMaxSizeMb": 500
}
}
}
}
}
| Option | Default | Description |
|---|---|---|
dbPath |
||
~/.openclaw/audit.db |
||
| Path to the SQLite database file | ||
localRetentionDays |
||
365 |
||
| Delete events older than this many days | ||
localMaxSizeMb |
||
500 |
||
| Prune oldest events when the DB exceeds this size |
| Option | Default | Description |
|---|---|---|
rateLimitPerSec |
||
100 |
||
| Max audit events written per second | ||
rateLimitBufferSize |
||
10000 |
||
| Buffer capacity for events that exceed the rate limit |
Two channels, deliberately separate so incident pokes and periodic digests can be routed to different rooms:
openclaw config set plugins.entries.gate-oc-audit.config.notificationWebhook "https://hooks.slack.com/services/AAA/BBB/CCC"
openclaw config set plugins.entries.gate-oc-audit.config.reportWebhook "https://hooks.slack.com/services/AAA/BBB/DDD"
{
"config": {
"notificationWebhook": "https://hooks.slack.com/services/AAA/BBB/CCC",
"reportWebhook": "https://hooks.slack.com/services/AAA/BBB/DDD"
}
}
| Option | Default | Description |
|---|---|---|
notificationWebhook |
||
| β | Webhook URL for incident alerts (config changes, integrity violations, DE divergence) | |
reportWebhook |
||
| β | Webhook URL for daily and weekly audit digests. Payload is Slack-compatible {text, blocks, projection} ; receivers that ignore extras still get a pretty message, and ETL receivers parse the full projection (same schema as audit report ). |
Cadence: daily digests fire shortly after local midnight, weekly digests after local Monday 00:00. The scheduler polls every ~5 minutes, so a digest "scheduled" for 00:00 may arrive anywhere in [00:00, 00:05)
. After a long downtime only the most recently completed window is pushed (no backfill spam).
Privacy: recipient
values in anomalies.duplicateOutbound[]
are replaced with a truncated SHA-256 digest (sha256:<16-hex>
) before the payload leaves the machine. This is a correlation hash β enough entropy to recognise the same recipient across reports, not a security primitive β so phone numbers / emails / @handles never reach the webhook receiver. Not hashed and sent verbatim: channel names (slack
, discord
, β¦), events[].id
and events[].sequence
, events[].sessionId
, tool names (topTools[].toolName
), and the integrity footer's last-event hashes. If your session IDs or tool names embed customer identifiers, treat the webhook URL like the audit DB itself and route only to endpoints you control.
Stamp a stable user identifier on every event. Resolved once at plugin startup and applied to every insert (locally as user_id
, on the gateway as plugin_user_id
).
openclaw config set plugins.entries.gate-oc-audit.config.userId "alice@example.com"
{ "config": { "userId": "alice@example.com" } }
| Option | Default | Description |
|---|---|---|
userId |
||
| β | Identifier to stamp on every event. If unset, falls back to the OPENCLAW_USER_ID env var, then the USER env var, then NULL. The first non-empty value wins |
| Option | Default | Description |
|---|---|---|
redactPromptText |
||
false |
||
Replace content of prompt.* and message.* events with "sha256:<hex>" before DB write. Length metadata (contentLength / promptLength ) is preserved. |
||
redactToolArgs |
||
false |
||
Replace tool.invoked metadata.args with { hash: "sha256:<hex>" } (hash computed over canonicalized JSON of the already key-sanitized args). |
||
scanToolArgs |
||
true |
||
Scan each tool invocation's serialized arguments with the ToolScanner "args" profile (injection, jailbreak, base64/obfuscation, shell-exec, sensitive-env patterns). Findings are recorded as a security.scan_result event tagged source: "tool_invocation" ; high-severity findings also fire a notificationWebhook alert. Advisory only β it never blocks the call. Scans the in-memory args before redaction, so it works alongside redactToolArgs . Set false to disable. |
Hashes allow independent verification β anyone with the original plaintext can re-hash and confirm it matches the audit record, without the plaintext ever touching the DB.
Nested under the smt
key. Set values via the CLI:
openclaw config set plugins.entries.gate-oc-audit.config.smt.treeKey "auto"
openclaw config set plugins.entries.gate-oc-audit.config.smt.maxTreeSize 500000
Or directly in the config JSON:
{
"config": {
"smt": {
"treeKey": "auto",
"maxTreeSize": 500000
}
}
}
| Option | Default | Description |
|---|---|---|
smt.treeKey |
||
auto |
||
Tree identifier (auto derives from machine ID) |
||
smt.maxTreeSize |
||
500000 |
||
| Max leaves per tree | ||
smt.checkpointDir |
||
~/.openclaw/smt-checkpoints |
||
| Directory for tree checkpoint files | ||
smt.checkpointIntervalMs |
||
300000 |
||
| Interval between tree checkpoints (ms) | ||
smt.epochDurationMs |
||
3600000 |
||
| Epoch duration for subtree freezing (ms) | ||
smt.pruneAfterEpochs |
||
0 (disabled) |
||
| Freeze subtrees older than this many epochs |
| Option | Default | Description |
|---|---|---|
fileWatchPatterns |
||
[] |
||
| Glob patterns for files to monitor for changes | ||
fileWatchIgnorePatterns |
||
[] |
||
| Glob patterns to exclude from file watching | ||
fileWatchIntervalMs |
||
1000 |
||
| Polling interval for file changes (ms, min 100) | ||
fileWatchUsePolling |
||
false |
||
| Use polling instead of native FS events |
| Option | Default | Description |
|---|---|---|
openclawDir |
||
~/.openclaw |
||
| Path to the OpenClaw config directory to watch for skill/tool/workspace/cron changes |
Anchor SMT roots to the Constellation Digital Evidence network for independent, tamper-proof verification. Follow the Digital Evidence setup guide to provision an account, generate API credentials, and fund a wallet for x402 micropayments. Two authentication methods are supported:
Prefer not to run these by hand?
openclaw audit setup
walks through the API-key path interactively: it writes theplugins.allow
andhooks.allowConversationAccess
opt-ins, collects your API key + org/tenant UUIDs, configures the anchoring tuning, and runsopenclaw audit status
to verify.
Option 1 β API key
openclaw config set plugins.entries.gate-oc-audit.config.deApiKey "your-api-key"
openclaw config set plugins.entries.gate-oc-audit.config.deOrgId "your-org-uuid"
openclaw config set plugins.entries.gate-oc-audit.config.deTenantId "your-tenant-uuid"
Or in the config JSON:
{
"plugins": {
"entries": {
"gate-oc-audit": {
"enabled": true,
"config": {
"deApiKey": "your-api-key",
"deOrgId": "your-org-uuid",
"deTenantId": "your-tenant-uuid"
}
}
}
}
}
Create a free account at https://digitalevidence.constellationnetwork.io and generate an API key from your dashboard.
Option 2 β Wallet key file (x402 micropayments)
openclaw config set plugins.entries.gate-oc-audit.config.deWalletKeyFile "/path/to/wallet.key"
Or in the config JSON:
{
"plugins": {
"entries": {
"gate-oc-audit": {
"enabled": true,
"config": {
"deWalletKeyFile": "/path/to/wallet.key"
}
}
}
}
}
The file should contain a SECP256K1 private key (64-char hex). Organization and tenant IDs are derived automatically from the wallet address β no registration required. Submission uses x402 micropayments (USDC on Base) via the @constellation-network/digital-evidence-sdk-x402
package.
| Option | Default | Description |
|---|---|---|
deApiKey |
||
| β | API key for DE anchoring | |
deOrgId |
||
| β | Organization UUID (required with API key) | |
deTenantId |
||
| β | Tenant UUID (required with API key) | |
deWalletKeyFile |
||
| β | Path to wallet private key file (alternative to API key) | |
deSigningKey |
||
| auto-generated | SECP256K1 private key (64-char hex) for signing fingerprints (see note below) | |
deEnv |
||
mainnet |
||
DE network environment (test , integration , or mainnet ). test requires the DE_TEST_URL environment variable pointing at a loopback URL (http(s)://localhost , 127.0.0.1 , or [::1] ); used for local development only. |
||
deEventThreshold |
||
100 |
||
| Events to accumulate before anchoring (event-count trigger) | ||
deTimerMinEvents |
||
1 |
||
| Minimum events required to anchor on a timer tick (clamped to >= 1) | ||
deIntervalMs |
||
300000 |
||
| Interval between timer-triggered anchoring attempts (ms) |
Ephemeral signing keys:WhendeSigningKey
is not configured, a new key pair is generated on each startup. This means fingerprints from different sessions are signed with different keys and cannot be verified against a single identity. PindeSigningKey
in your config if you need cross-session verifiable provenance.
The plugin subscribes to every public OpenClaw lifecycle hook and records each event into the audit trail. Full message/prompt content is stored gzipped; metadata contains a 50-char preview.
Sensitive values (secret
, password
, token
, apiKey
, auth
, credential
, passphrase
, jwt
, bearer
, cookie
, privateKey
) in tool arguments are automatically redacted before storage.
| Event type | Hook | Metadata captured |
|---|---|---|
prompt.model_resolve |
||
before_model_resolve |
||
| prompt length, trigger | ||
prompt.build |
||
before_prompt_build |
||
| prompt length, message count | ||
prompt.input |
||
llm_input |
||
| provider, model, prompt length, history message count, images count, content (gzipped) | ||
prompt.response |
||
llm_output |
||
| provider, model, token usage (input/output/cache read/write), content (gzipped) |
| Event type | Hook | Metadata captured |
|---|---|---|
agent.end |
||
agent_end |
||
| duration (ms), success, run ID, job ID, model provider/id | ||
agent.compaction_start |
||
before_compaction |
||
| message count, compacting count, token count, session file | ||
agent.compaction_end |
||
after_compaction |
||
| message count, compacted count, token count, session file | ||
agent.reset |
||
before_reset |
||
| reason, session file | ||
agent.subagent_spawning |
||
subagent_spawning |
||
| agent ID, child session key, label, mode | ||
agent.subagent_spawned |
||
subagent_spawned |
||
| agent ID, child session key, run ID, label, mode | ||
agent.subagent_delivery |
||
subagent_delivery_target |
||
| child/requester session keys, spawn mode, delivery channel/target | ||
agent.subagent_ended |
||
subagent_ended |
||
| target session key, target kind, reason, outcome, error, run ID |
| Event type | Hook | Metadata captured |
|---|---|---|
tool.invoked |
||
before_tool_call |
||
| tool name, sanitized arguments | ||
tool.result |
||
after_tool_call |
||
| tool name, duration (ms), error | ||
tool.denied |
||
after_tool_call |
||
| tool name, duration (ms), reason | ||
tool.persisted |
||
tool_result_persist |
||
| tool name, is synthetic |
tool.denied
is emitted instead of tool.result
when a before_tool_call
hook returns block: true
or a user/approval flow denies the call. Denials with free-form reasons (custom blockReason
set by a plugin, or engine-side loop-detector blocks) do not match the known phrases and will surface as tool.result
with the error populated.
| Event type | Hook | Metadata captured |
|---|---|---|
cron.executed |
||
before_model_resolve |
||
| agent ID, run ID, job ID, prompt length | ||
cron.failed |
||
agent_end |
||
| agent ID, run ID, job ID, duration (ms), error |
Emitted only when the agent run's ctx.trigger === "cron"
. cron.executed
marks the start of a cron-triggered run; it is not guaranteed to be paired with a cron.failed
or agent.end
if the process exits abnormally before the run completes.
| Event type | Hook | Metadata captured |
|---|---|---|
message.received |
||
message_received |
||
| direction, sender (with fallback chain), sender ID, channel, account, session key, run ID, thread ID, message ID, surface, content length, timestamp, content (gzipped) | ||
message.sending |
||
message_sending |
||
| direction, recipient, channel, session key, run ID, reply-to ID, thread ID, content length, content (gzipped) | ||
message.sent |
||
message_sent |
||
| direction, recipient, channel, account, session key, run ID, message ID, content length, success, error, timestamp, content (gzipped) | ||
message.claimed |
||
inbound_claim |
||
| channel, sender ID/name, is group, session key, run ID, thread ID, message ID, content length | ||
message.dispatched |
||
before_dispatch |
||
| channel, sender ID, is group, content length | ||
message.write |
||
before_message_write |
||
| agent ID |
| Event type | Hook | Metadata captured |
|---|---|---|
session.start |
||
session_start |
||
| session key, resumed from | ||
session.end |
||
session_end |
||
| session key, message count, duration (ms), reason, session file, transcript archived, next session id/key |
| Event type | Hook | Metadata captured |
|---|---|---|
gateway.start |
||
gateway_start |
||
| port | ||
gateway.stop |
||
gateway_stop |
||
| reason |
| Event type | Hook | Metadata captured |
|---|---|---|
system.install |
||
before_install |
||
| target type (skill/plugin), target name, source path, request kind, plugin/skill identifiers, scan summary (files, critical/warn/info counts) | ||
system.install_hook_unavailable |
||
| (registration failure) | error message |
system.install
records every plugin or skill install/update intercepted by openclaw's install pipeline, including the built-in security scan summary. Captures who installed what so unexpected supply-chain events leave an audit-trail signal. Hook is non-decisive β the plugin observes only and never blocks.
system.install_hook_unavailable
is appended each time registerHooks
runs and before_install
registration throws (typically once per process, but openclaw may re-register on config reload). This makes "we silently couldn't audit installs" a recorded event rather than a console warning that scrolls away.
Every audit event is committed as dual-hash (raw + censored) leaves in a Sparse Merkle Tree. The raw hash covers all event fields; the censored hash covers only the event type, category, and timestamp (for privacy-preserving verification).
- Inserting an event changes the SMT root, creating a tamper-evident chain of state transitions
- Deleting or modifying an event is detectable via inclusion/exclusion proofs
- The SMT root can be anchored to the Constellation Digital Evidence network for independent verification
Run openclaw audit verify
at any time to check SMT integrity and DE checkpoint consistency.
Running openclaw security audit --deep
may report a potential-exfiltration
warning for src/scanner.ts
and dist/scanner.js
. This is a false positive: the built-in tool scanner uses readFileSync
to read local skill/tool files for code-safety analysis, not to exfiltrate data. The warning is triggered because the deep audit heuristic detects filesystem reads in the same package as other code. It is safe to ignore.
- The database file is created with
0600
permissions (owner read/write only) - Sensitive keys (
secret
,password
,token
,apiKey
,api_key
,auth
,credential
,passphrase
,jwt
,bearer
,cookie
,privateKey
) are recursively redacted from tool arguments - The plugin is fail-open: if the database is unavailable, events are silently dropped and the agent continues normally. A degraded-mode warning appears in
audit list
output - CLI commands (
audit list
,audit verify
,audit export
,audit smt β¦
) open the audit DB read-only, so they coexist with the running gateway via SQLite WAL β no lock contention with the writer
On versions 0.2.0β0.2.4, openclaw audit report anomalies
constructed the SMT service for the CLI but never restored it from disk, so integrityViolations.tamperedEvents
was always []
and the report carried the note "SMT has no checkpointed leaves yet β tamper scan skipped."
even when the on-disk SMT was populated. The skip was visibly flagged by that note (and by the related unverifiedAnchored
list), but no tampered-event detection actually ran from the CLI.
Other integrity paths were unaffected:
openclaw audit verify
callsensureReady()
itself and has always run the full replay.- The control-UI
/api/verify
endpoint runs inside the gateway process, where the SMT is restored during boot, and also has always been correct.
To re-scan historical windows after upgrading, re-run openclaw audit report anomalies --since <window>
against the relevant time ranges; the note will now be null
whenever the on-disk SMT has checkpointed leaves, and any tampered events will be enumerated in integrityViolations.tamperedEvents
.
SMT checkpoint persistence moved from LevelDB to node:sqlite
. On-disk layout changed from <checkpointDir>/<treeKey>/
(a LevelDB directory) to <checkpointDir>/<treeKey>.db
(a single sqlite file).
On first startup after upgrading from 0.1.x, the plugin logs a warning for any legacy LevelDB directory it finds and skips it; trees rebuild from events on the next checkpoint. To silence the warning, delete the legacy directories:
rm -rf ~/.openclaw/smt-checkpoints/*/
The migration also drops the level
runtime dependency, eliminating the python3
/ build-essential
requirement at install time.
Each SMT tree is stored as <smt.checkpointDir>/<treeKey>.db
. With the default smt.treeKey: "auto"
(derived from machineId
), exactly one file is reused forever and the directory does not grow. Old .db
files become orphaned only if:
- you change
smt.treeKey
to a different value (the previous tree's file stays), - the machine's
machineId
changes β e.g., container rebuild, OS reinstall β leaving the old<oldMachineId>.db
behind.
The plugin does not GC these automatically. List the directory and delete any tree files you don't recognize:
ls -lh ~/.openclaw/smt-checkpoints/
rm ~/.openclaw/smt-checkpoints/<old-treeKey>.db
A small __verifier__.db
is also written on each checkpoint β it's a transient verification tree and is safe to leave in place.
npm install
npm run build # Compile TypeScript to dist/
npm test # Run the full test suite (unit + e2e)
npm run test:e2e # Run only the e2e suite (test/e2e.test.ts)
npm run clean # Remove dist/
The e2e suite simulates openclaw firing lifecycle events through the plugin's hook pipeline and verifies the resulting audit trail, SMT proofs, CLI handlers, and Digital Evidence publishing (against a local mock DE server). It runs as a separate CI job (.github/workflows/e2e.yml
) in addition to the main ci.yml
.
To install the plugin from a local checkout into OpenClaw, build a tarball with npm pack
and install it:
npm install
npm run build
TGZ=$(npm pack --silent)
openclaw plugins uninstall gate-oc-audit || true
openclaw plugins install "./$TGZ"
openclaw gateway restart
rm -f "./$TGZ"
The uninstall
step ensures a clean reinstall when iterating; the || true
guards against errors when no prior install exists. To also wipe the local extension directory and audit database for a fully clean state (development only):
rm -rf ~/.openclaw/extensions/gate-oc-audit/
rm -f ~/.openclaw/audit.db*