{"slug": "show-hn-tamper-evident-audit-trail-for-ai-coding-agent-activity", "title": "Show HN: Tamper-evident audit trail for AI coding agent activity", "summary": "Constellation Network released an open-source audit trail plugin for AI coding agents that records every session, tool invocation, and prompt exchange into a local SQLite database with SHA-256 hash chain integrity. The tool provides tamper-evident logging and verification capabilities, including SMT proofs and Digital Evidence anchoring for compliance. It aims to address accountability and security concerns in AI agent operations.", "body_md": "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.\n\n```\n#install from npm\nopenclaw plugins install @constellation-network/gate-oc-audit\n#run the interactive configuration tool\nopenclaw audit setup\n```\n\n| Job | CLI | SPA (in the gateway UI) | What you get |\n|---|---|---|---|\n| Confirm the plugin is healthy | `openclaw audit status` |\n`#/status` (default route) |\nOne-screen snapshot: storage, integrity, anchor, file-watch, inventory, last security scan |\n| See today's / this week's activity | `openclaw audit report daily` / `… weekly` |\n`#/reports/daily` , `#/reports/weekly` |\nActivity, top tools, LLM spend, outbound messaging, anomalies, integrity footer |\n| Per-cron rollup | `openclaw audit report cron <job-id>` |\n`#/reports/cron?jobId=…` |\nOne row per execution: started/ended, status, tool/LLM/outbound counts |\n| Per-conversation rollup | `openclaw audit report session <id>` |\n`#/reports/session/<id>` |\nTimeline, tools, LLM cost, outbound, integrity |\n| Surface anomalies | `openclaw audit anomalies --since 24h` |\n`#/anomalies` |\nTamper, duplicate-outbound, denial spikes, installs, first-seen tools |\n| Track LLM spend | `openclaw audit spend --by model --since 7d` |\n`#/spend` |\nToken usage and `costUsd` grouped by provider / model / day / session |\n| Browse installed plugins/skills/tools/crons | `openclaw audit inventory [kind]` |\n`#/inventory` |\nSummary counters + per-kind tables (cron rows link to per-cron rollup) |\n| Prove an event happened | `openclaw audit smt proof <hash>` then `… smt verify` |\n`#/smt-tools` |\nInclusion proof against tree roots and DE-anchored checkpoints |\n| Independent third-party proof | Configure Digital Evidence anchoring (see below) | — | SMT roots anchored on the Constellation Digital Evidence network |\n| Alert on file changes | Configure `fileWatchPatterns` + `notificationWebhook` |\n— | Slack/Discord/webhook ping when a watched path changes |\n| Daily/weekly digests to a channel | Configure `reportWebhook` |\n— | Slack-compatible payload with the same projection as `audit report` |\n| Export the trail for compliance | `openclaw audit export csv --from … --to …` |\n`#/events` → Download button |\nStreamed NDJSON or CSV with anchor references per row |\n| Re-scan for tampering | `openclaw audit verify` |\n`#/verify` |\nFull SMT replay + DE checkpoint consistency check; exit 0 if clean |\n\nIf you don't know where to start, run `openclaw audit status`\n\nafter install — or open the SPA at `openclaw audit ui`\n\nand land on the same snapshot at `#/status`\n\n. It tells you everything that's wired up and what isn't.\n\nEvery SPA route is backed by an HTTP endpoint under `/plugins/audit/api/`\n\n. The endpoints introduced alongside the SPA views (`/status`\n\n, `/anomalies`\n\n, `/spend`\n\n, `/inventory`\n\n, `/report/session/:id`\n\n, `/smt/proof`\n\n, `/smt/verify-proof`\n\n, `/smt/chain`\n\n) follow the same loopback policy as `/api/report`\n\n— `403`\n\noutside loopback unless `allowExportOnNonLoopback: true`\n\nis set. The wire JSON is byte-identical to the matching CLI `--json`\n\noutput, so dashboards can pin against the same schemas.\n\nReaching the UI from outside loopback.By default the routes register with`auth: \"plugin\"`\n\n(no verification) and lean on the loopback bind for safety. To expose the UI/API on a shared network, set`requireGatewayAuth: true`\n\nso the routes register with`auth: \"gateway\"`\n\nand the openclaw gateway authenticates every request:\n\n```\nopenclaw config set plugins.entries.gate-oc-audit.config.requireGatewayAuth true\n{ \"config\": { \"requireGatewayAuth\": true } }\n```\n\nBecause the gateway then authenticates every caller,\n\n`requireGatewayAuth`\n\nsubsumes 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. The`allowExportOnNonLoopback`\n\n/`allowVerifyOnNonLoopback`\n\nflags exist for the narrower case of exposing those routes off-loopbackwithoutgateway auth (e.g. behind your own reverse proxy) — leave them off when`requireGatewayAuth`\n\nis set.\n\n```\nopenclaw plugins install @constellation-network/gate-oc-audit\n```\n\nRequires `openclaw >= 2026.4.24`\n\nas a peer dependency and Node.js ≥ 22.13 (uses the built-in `node:sqlite`\n\nmodule).\n\nThat's it. The plugin automatically starts recording audit events when your agent runs.\n\n```\nopenclaw audit setup\n```\n\nInteractive wizard that walks through everything the plugin needs:\n\n- Adds\n`gate-oc-audit`\n\nto`plugins.allow`\n\n(trusts the plugin). - Enables\n`hooks.allowConversationAccess`\n\nso`prompt.input`\n\n/`prompt.response`\n\n/`agent.end`\n\nevents 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).\n- Runs\n`openclaw audit status`\n\nat the end so you can confirm the trail is live.\n\nPass `--yes`\n\nto accept defaults for the anchoring tuning (DE identity prompts still ask). Once the wizard finishes, skip to [Quick check after install](#quick-check-after-install).\n\nTwo 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`\n\ncalls and JSON snippets for operators who provision config by hand or via configuration management.\n\nAdd `gate-oc-audit`\n\nto `plugins.allow`\n\n. When `plugins.allow`\n\nis empty, openclaw still auto-loads discovered plugins but logs a warning on every startup:\n\n```\n[plugins] plugins.allow is empty; discovered non-bundled plugins may auto-load …\n```\n\nSetting an explicit allowlist silences the warning and locks loading down to the listed ids:\n\n```\nopenclaw config set plugins.allow '[\"gate-oc-audit\"]'\n```\n\nOr directly in the config JSON:\n\n```\n{\n  \"plugins\": {\n    \"allow\": [\"gate-oc-audit\"]\n  }\n}\n```\n\nIf you already have other trusted plugins in `plugins.allow`\n\n, append `\"gate-oc-audit\"`\n\nto the existing array rather than replacing it.\n\nNon-bundled plugins must explicitly opt in to receive raw conversation content from the `llm_input`\n\n, `llm_output`\n\n, `before_agent_finalize`\n\n, and `agent_end`\n\nhooks. Without this opt-in, openclaw blocks those hook registrations and logs:\n\n```\n[plugins] typed hook \"llm_input\" blocked because non-bundled plugins must set plugins.entries.gate-oc-audit.hooks.allowConversationAccess=true …\n```\n\nThree of the audit plugin's most important event types (`prompt.input`\n\n, `prompt.response`\n\n, `agent.end`\n\n) will be missing from the audit trail until this is set.\n\n```\nopenclaw config set plugins.entries.gate-oc-audit.hooks.allowConversationAccess true\n```\n\nOr directly in the config JSON:\n\n```\n{\n  \"plugins\": {\n    \"entries\": {\n      \"gate-oc-audit\": {\n        \"hooks\": { \"allowConversationAccess\": true }\n      }\n    }\n  }\n}\n```\n\nThe plugin also logs a warning at startup if a tool call is observed without any preceding `llm_input`\n\nevent, which usually indicates this opt-in is missing.\n\n```\nopenclaw audit status\n```\n\nThis is the single best command to confirm the plugin is recording. It prints one screen covering:\n\n**Storage**— DB size vs cap, event count, oldest event, next prune** Integrity**— sequence head, SMT trees + root, last checkpoint, conversation-hook state (`ENABLED`\n\n/`DISABLED`\n\n/`ENABLED-but-silent`\n\nif no`prompt.input`\n\nseen 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\n\nAdd `--json`\n\nfor a single-line, machine-readable snapshot. If `Conversation hook`\n\nshows `DISABLED`\n\n, the operator hasn't set `allowConversationAccess`\n\n(see above) and `prompt.input`\n\n/ `prompt.response`\n\n/ `agent.end`\n\nevents are missing from the trail.\n\nAll commands open the audit DB read-only via SQLite WAL, so they coexist with the running gateway without lock contention.\n\nSee [Quick check after install](#quick-check-after-install) above. Use `--json`\n\nto pipe to `jq`\n\n.\n\n```\nopenclaw audit list\nopenclaw audit list --last 20\nopenclaw audit list --type tool.invoked\nopenclaw audit list --category prompt --session <session-id>\n```\n\nVerify SMT proofs for recent events and check DE checkpoint consistency:\n\n```\nopenclaw audit verify\n```\n\nExits with code `0`\n\nif all proofs and checkpoints are valid, `1`\n\nif any verification fails.\n\n```\nopenclaw audit inventory                # summary across plugins/skills/tools/crons\nopenclaw audit inventory plugins        # detail for one kind\nopenclaw audit inventory skills --json\n```\n\nGenerate 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`\n\n), or a self-contained HTML document (`--html`\n\n).\n\n```\nopenclaw audit report daily                       # today (UTC), human text\nopenclaw audit report daily --date 2026-05-17     # specific UTC day\nopenclaw audit report daily --tz local            # use local-time day boundary\nopenclaw audit report weekly                      # this ISO week (UTC)\nopenclaw audit report weekly --week 2026-W19      # specific ISO week\nopenclaw audit report daily --json                # single-line JSON\nopenclaw audit report daily --html > report.html  # standalone HTML\nopenclaw audit report cron <job-id>               # per-cron rollup, one row per execution\nopenclaw audit report cron <job-id> --last 5 --json\nopenclaw audit report cron <job-id> --html > cron.html\nopenclaw audit report session <session-id>        # per-conversation rollup\n```\n\nDetector knobs (capped on both CLI and HTTP):\n\n| Flag | Default | Max | Description |\n|---|---|---|---|\n`--dup-window-sec` |\n`60` |\n`3600` |\nR5a duplicate-outbound: sha256-equal `message.sent` within this window to the same channel + recipient is flagged |\n`--lookback-days` |\n`30` |\n`365` |\nR5b first-seen-tool: tools invoked in the window but absent from this trailing day count are flagged |\n`--top-tools` |\n`10` |\n`1000` |\nCap for the Top tools section |\n\nAnomaly detectors emitted in the `anomalies`\n\nblock:\n\n**R5a duplicate outbound**— same content hash sent to the same channel + recipient inside`--dup-window-sec`\n\n.`duplicateOutboundTruncated: true`\n\nindicates the underlying`message.sent`\n\nscan 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`\n\nwindow. Calendar-day arithmetic in the report's timezone keeps the lookback DST-tolerant.\n\nThe Integrity footer pins the report to a sequence point: last event id / sequence / `content_hash`\n\n, plus the last DE-anchored checkpoint (id, `smtRoot`\n\n, `deTxHash`\n\n, sequence range, `createdAt`\n\n) when one exists. A consumer can cross-check the footer against `openclaw audit verify`\n\nto confirm the report covers a tamper-evident slice of the trail.\n\nThe **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`\n\n(one entry per `id`\n\nin the `jobs[]`\n\narray); legacy `<jobId>.cron.*.json`\n\nper-file manifests in the openclaw root are also surfaced as a fallback and merged in for ids not already covered by `jobs.json`\n\n. Each entry shows the job name and a compact schedule string (`cron <expr> (<tz>)`\n\n, `every <ms>`\n\n, `at <iso>`\n\n, or `unknown (<raw>)`\n\nwhen the `schedule`\n\nfield doesn't parse). Symlinked, oversized (>1 MiB for `jobs.json`\n\n, >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: []`\n\n.\n\n**Per-cron rollup (R9).** `openclaw audit report cron <job-id>`\n\nprojects the trail as one row per cron execution for a given `jobId`\n\n, newest first. Each row pairs the `cron.executed`\n\nevent with its matching `agent.end`\n\n(by `sessionId`\n\n+ `metadata.runId`\n\n) and attributes tool / LLM / outbound-message activity that fired on the same session between the two timestamps. `--last N`\n\n(default 20, max 1000) bounds the rollup; when the store has more executions than fit, `truncated: true`\n\nis surfaced in all output formats. When a matching entry exists in openclaw's cron store (`<openclawDir>/cron/jobs.json`\n\n, or a legacy `<jobId>.cron.*.json`\n\nfile as fallback), its schedule is inlined in the rollup header (`Schedule: cron <expr> (<tz>)`\n\n, etc.); `manifest: null`\n\nwhen nothing matches or no `openclawDir`\n\nwas supplied. Output is human text (default), single-line JSON (`--json`\n\n), or a self-contained HTML document (`--html`\n\n). The JSON shape is published at `schemas/audit-cron-rollup.schema.json`\n\nso dashboards can pin against `schemaVersion: 1`\n\n.\n\nThe same projection is available over HTTP at `GET /plugins/audit/api/report?period=daily|weekly&date=&week=&tz=&format=json|html&dupWindowSec=&lookbackDays=&topTools=`\n\n. The JSON shape is published at `schemas/audit-projection.schema.json`\n\nso dashboards can pin against `schemaVersion: 1`\n\n.\n\nHTTP endpoint and loopback.Like`/api/export`\n\n, the report route is unauthenticated and returns`403`\n\nwhen the gateway binds beyond loopback unless`allowExportOnNonLoopback: true`\n\nis set. The CLI reads the local DB directly and is unaffected.\n\n`openclaw audit spend [--by provider|model|day|session] [--since 24h] [--until now] [--limit 1000] [--json]`\n\naggregates `prompt.response`\n\nmetadata 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`\n\nis summed verbatim from the per-event `metadata.costUsd`\n\n.\n\nDefault grouping is `model`\n\n; bucket labels for that mode are formatted as `provider/model`\n\n(e.g. `openai/gpt-5`\n\n) so cross-provider model name collisions don't merge. `--by day`\n\nbuckets are always UTC dates regardless of `--tz`\n\n— the formatter prints a note when `--tz local`\n\nis used with `--by day`\n\n. `--limit`\n\n(default 1000, max 100000) caps the number of buckets; when the cap trims a result, `truncated: true`\n\nappears in all outputs. The JSON shape is published at `schemas/audit-spend.schema.json`\n\n. Totals match the daily report's LLM-spend section when the windows align.\n\n```\nopenclaw audit export                                         # JSON Lines (default, streamed)\nopenclaw audit export csv                                     # CSV (streamed, stable column order)\nopenclaw audit export --type tool.invoked --limit 100         # cap rows\nopenclaw audit export --from 2025-01-01T00:00:00Z --to 2025-02-01T00:00:00Z\nopenclaw audit export --security-only                         # security / config / system categories\nopenclaw audit export --include-content                       # include decompressed content column / field\n```\n\nEach emitted row carries the DE anchor reference (`anchor.deTxHash`\n\n, `anchor.smtRoot`\n\n, `anchor.sequenceStart`\n\n, `anchor.sequenceEnd`\n\n, `anchor.createdAt`\n\n) for the checkpoint covering its sequence, or `null`\n\nwhen 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=`\n\n.\n\n`--include-content`\n\nand redaction.`redactPromptText`\n\nrewrites prompt / message content to`sha256:<hex>`\n\nbefore insert, and`redactToolArgs`\n\ndoes the same for tool-call arguments. Neither switch covers`tool.result`\n\ncontent (tool stdout / stderr / output bodies). If you set both flags and run`audit export --include-content`\n\n, 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`\n\n, or (b) filter`--category`\n\naway from`tool`\n\nevents.\n\nHTTP endpoint and loopback.`GET /plugins/audit/api/export`\n\nis unauthenticated; the plugin relies on the gateway being bound to loopback (`gateway.bind: \"loopback\"`\n\n, the default) for safety. When the gateway binds beyond loopback the export route returns`403`\n\nunless you explicitly opt in:\n\n```\nopenclaw config set plugins.entries.gate-oc-audit.config.allowExportOnNonLoopback true\n{ \"config\": { \"allowExportOnNonLoopback\": true } }\n```\n\nThe CLI (\n\n`openclaw audit export …`\n\n) is unaffected — it reads the local DB directly and doesn't traverse the HTTP gate.\n\n```\nopenclaw audit smt root                           # Show current SMT root and entry count\nopenclaw audit smt root --tree <key>              # Root for a specific tree\nopenclaw audit smt trees                          # List all SMT trees\nopenclaw audit smt proof <hash>                   # Generate inclusion/exclusion proof for a hash\nopenclaw audit smt proof <hash> --tree <key>\nopenclaw audit smt verify --proof '<json>'        # Verify a proof against known tree/checkpointed roots\nopenclaw audit smt chain <conversationId> --tree <key>\n```\n\nProof verification checks both internal consistency (siblings hash to the claimed root) and root legitimacy . A self-consistent proof with an unknown root is rejected.\n\nExit codes for `smt verify`\n\n:\n\n| Exit code | Meaning |\n|---|---|\n| 0 | Proof is valid — internally consistent and root matches a known anchor |\n| 1 | INVALID — root not recognized by this node, or proof is internally inconsistent |\n| 2 | UNVERIFIABLE — no SMT trees or DE checkpoints exist to verify against |\n\n**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.\n\nConfig key:in openclaw config this plugin lives under`gate-oc-audit`\n\n(the manifest id),notthe npm package name`@constellation-network/gate-oc-audit`\n\n. Openclaw logs a warning about the mismatch on load — it's expected and safe to ignore.\n\nSet values via the CLI:\n\n```\nopenclaw config set plugins.entries.gate-oc-audit.enabled true\nopenclaw config set plugins.entries.gate-oc-audit.config.dbPath \"$HOME/.openclaw/audit.db\"\nopenclaw config set plugins.entries.gate-oc-audit.config.localRetentionDays 365\nopenclaw config set plugins.entries.gate-oc-audit.config.localMaxSizeMb 500\n```\n\nOr directly in the config JSON:\n\n```\n{\n  \"plugins\": {\n    \"entries\": {\n      \"gate-oc-audit\": {\n        \"enabled\": true,\n        \"config\": {\n          \"dbPath\": \"~/.openclaw/audit.db\",\n          \"localRetentionDays\": 365,\n          \"localMaxSizeMb\": 500\n        }\n      }\n    }\n  }\n}\n```\n\n| Option | Default | Description |\n|---|---|---|\n`dbPath` |\n`~/.openclaw/audit.db` |\nPath to the SQLite database file |\n`localRetentionDays` |\n`365` |\nDelete events older than this many days |\n`localMaxSizeMb` |\n`500` |\nPrune oldest events when the DB exceeds this size |\n\n| Option | Default | Description |\n|---|---|---|\n`rateLimitPerSec` |\n`100` |\nMax audit events written per second |\n`rateLimitBufferSize` |\n`10000` |\nBuffer capacity for events that exceed the rate limit |\n\nTwo channels, deliberately separate so incident pokes and periodic digests can be routed to different rooms:\n\n```\nopenclaw config set plugins.entries.gate-oc-audit.config.notificationWebhook \"https://hooks.slack.com/services/AAA/BBB/CCC\"\nopenclaw config set plugins.entries.gate-oc-audit.config.reportWebhook \"https://hooks.slack.com/services/AAA/BBB/DDD\"\n{\n  \"config\": {\n    \"notificationWebhook\": \"https://hooks.slack.com/services/AAA/BBB/CCC\",\n    \"reportWebhook\": \"https://hooks.slack.com/services/AAA/BBB/DDD\"\n  }\n}\n```\n\n| Option | Default | Description |\n|---|---|---|\n`notificationWebhook` |\n— | Webhook URL for incident alerts (config changes, integrity violations, DE divergence) |\n`reportWebhook` |\n— | 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` ). |\n\n**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)`\n\n. After a long downtime only the most recently completed window is pushed (no backfill spam).\n\n**Privacy:** `recipient`\n\nvalues in `anomalies.duplicateOutbound[]`\n\nare replaced with a truncated SHA-256 digest (`sha256:<16-hex>`\n\n) 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`\n\n, `discord`\n\n, …), `events[].id`\n\nand `events[].sequence`\n\n, `events[].sessionId`\n\n, tool names (`topTools[].toolName`\n\n), 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.\n\nStamp a stable user identifier on every event. Resolved once at plugin startup and applied to every insert (locally as `user_id`\n\n, on the gateway as `plugin_user_id`\n\n).\n\n```\nopenclaw config set plugins.entries.gate-oc-audit.config.userId \"alice@example.com\"\n{ \"config\": { \"userId\": \"alice@example.com\" } }\n```\n\n| Option | Default | Description |\n|---|---|---|\n`userId` |\n— | 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 |\n\n| Option | Default | Description |\n|---|---|---|\n`redactPromptText` |\n`false` |\nReplace content of `prompt.*` and `message.*` events with `\"sha256:<hex>\"` before DB write. Length metadata (`contentLength` / `promptLength` ) is preserved. |\n`redactToolArgs` |\n`false` |\nReplace `tool.invoked` `metadata.args` with `{ hash: \"sha256:<hex>\" }` (hash computed over canonicalized JSON of the already key-sanitized args). |\n`scanToolArgs` |\n`true` |\nScan 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. |\n\nHashes 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.\n\nNested under the `smt`\n\nkey. Set values via the CLI:\n\n```\nopenclaw config set plugins.entries.gate-oc-audit.config.smt.treeKey \"auto\"\nopenclaw config set plugins.entries.gate-oc-audit.config.smt.maxTreeSize 500000\n```\n\nOr directly in the config JSON:\n\n```\n{\n  \"config\": {\n    \"smt\": {\n      \"treeKey\": \"auto\",\n      \"maxTreeSize\": 500000\n    }\n  }\n}\n```\n\n| Option | Default | Description |\n|---|---|---|\n`smt.treeKey` |\n`auto` |\nTree identifier (`auto` derives from machine ID) |\n`smt.maxTreeSize` |\n`500000` |\nMax leaves per tree |\n`smt.checkpointDir` |\n`~/.openclaw/smt-checkpoints` |\nDirectory for tree checkpoint files |\n`smt.checkpointIntervalMs` |\n`300000` |\nInterval between tree checkpoints (ms) |\n`smt.epochDurationMs` |\n`3600000` |\nEpoch duration for subtree freezing (ms) |\n`smt.pruneAfterEpochs` |\n`0` (disabled) |\nFreeze subtrees older than this many epochs |\n\n| Option | Default | Description |\n|---|---|---|\n`fileWatchPatterns` |\n`[]` |\nGlob patterns for files to monitor for changes |\n`fileWatchIgnorePatterns` |\n`[]` |\nGlob patterns to exclude from file watching |\n`fileWatchIntervalMs` |\n`1000` |\nPolling interval for file changes (ms, min 100) |\n`fileWatchUsePolling` |\n`false` |\nUse polling instead of native FS events |\n\n| Option | Default | Description |\n|---|---|---|\n`openclawDir` |\n`~/.openclaw` |\nPath to the OpenClaw config directory to watch for skill/tool/workspace/cron changes |\n\nAnchor SMT roots to the [Constellation Digital Evidence](https://digitalevidence.constellationnetwork.io) network for independent, tamper-proof verification. Follow the [Digital Evidence setup guide](https://digitalevidence.constellationnetwork.io/get-started) to provision an account, generate API credentials, and fund a wallet for x402 micropayments. Two authentication methods are supported:\n\nPrefer not to run these by hand?\n\n`openclaw audit setup`\n\nwalks through the API-key path interactively: it writes the`plugins.allow`\n\nand`hooks.allowConversationAccess`\n\nopt-ins, collects your API key + org/tenant UUIDs, configures the anchoring tuning, and runs`openclaw audit status`\n\nto verify.\n\n**Option 1 — API key**\n\n```\nopenclaw config set plugins.entries.gate-oc-audit.config.deApiKey \"your-api-key\"\nopenclaw config set plugins.entries.gate-oc-audit.config.deOrgId \"your-org-uuid\"\nopenclaw config set plugins.entries.gate-oc-audit.config.deTenantId \"your-tenant-uuid\"\n```\n\nOr in the config JSON:\n\n```\n{\n  \"plugins\": {\n    \"entries\": {\n      \"gate-oc-audit\": {\n        \"enabled\": true,\n        \"config\": {\n          \"deApiKey\": \"your-api-key\",\n          \"deOrgId\": \"your-org-uuid\",\n          \"deTenantId\": \"your-tenant-uuid\"\n        }\n      }\n    }\n  }\n}\n```\n\nCreate a free account at [https://digitalevidence.constellationnetwork.io](https://digitalevidence.constellationnetwork.io) and generate an API key from your dashboard.\n\n**Option 2 — Wallet key file (x402 micropayments)**\n\n```\nopenclaw config set plugins.entries.gate-oc-audit.config.deWalletKeyFile \"/path/to/wallet.key\"\n```\n\nOr in the config JSON:\n\n```\n{\n  \"plugins\": {\n    \"entries\": {\n      \"gate-oc-audit\": {\n        \"enabled\": true,\n        \"config\": {\n          \"deWalletKeyFile\": \"/path/to/wallet.key\"\n        }\n      }\n    }\n  }\n}\n```\n\nThe 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](https://www.x402.org/) micropayments (USDC on Base) via the `@constellation-network/digital-evidence-sdk-x402`\n\npackage.\n\n| Option | Default | Description |\n|---|---|---|\n`deApiKey` |\n— | API key for DE anchoring |\n`deOrgId` |\n— | Organization UUID (required with API key) |\n`deTenantId` |\n— | Tenant UUID (required with API key) |\n`deWalletKeyFile` |\n— | Path to wallet private key file (alternative to API key) |\n`deSigningKey` |\nauto-generated | SECP256K1 private key (64-char hex) for signing fingerprints (see note below) |\n`deEnv` |\n`mainnet` |\nDE 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. |\n`deEventThreshold` |\n`100` |\nEvents to accumulate before anchoring (event-count trigger) |\n`deTimerMinEvents` |\n`1` |\nMinimum events required to anchor on a timer tick (clamped to >= 1) |\n`deIntervalMs` |\n`300000` |\nInterval between timer-triggered anchoring attempts (ms) |\n\nEphemeral signing keys:When`deSigningKey`\n\nis 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. Pin`deSigningKey`\n\nin your config if you need cross-session verifiable provenance.\n\nThe 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.\n\nSensitive values (`secret`\n\n, `password`\n\n, `token`\n\n, `apiKey`\n\n, `auth`\n\n, `credential`\n\n, `passphrase`\n\n, `jwt`\n\n, `bearer`\n\n, `cookie`\n\n, `privateKey`\n\n) in tool arguments are automatically redacted before storage.\n\n| Event type | Hook | Metadata captured |\n|---|---|---|\n`prompt.model_resolve` |\n`before_model_resolve` |\nprompt length, trigger |\n`prompt.build` |\n`before_prompt_build` |\nprompt length, message count |\n`prompt.input` |\n`llm_input` |\nprovider, model, prompt length, history message count, images count, content (gzipped) |\n`prompt.response` |\n`llm_output` |\nprovider, model, token usage (input/output/cache read/write), content (gzipped) |\n\n| Event type | Hook | Metadata captured |\n|---|---|---|\n`agent.end` |\n`agent_end` |\nduration (ms), success, run ID, job ID, model provider/id |\n`agent.compaction_start` |\n`before_compaction` |\nmessage count, compacting count, token count, session file |\n`agent.compaction_end` |\n`after_compaction` |\nmessage count, compacted count, token count, session file |\n`agent.reset` |\n`before_reset` |\nreason, session file |\n`agent.subagent_spawning` |\n`subagent_spawning` |\nagent ID, child session key, label, mode |\n`agent.subagent_spawned` |\n`subagent_spawned` |\nagent ID, child session key, run ID, label, mode |\n`agent.subagent_delivery` |\n`subagent_delivery_target` |\nchild/requester session keys, spawn mode, delivery channel/target |\n`agent.subagent_ended` |\n`subagent_ended` |\ntarget session key, target kind, reason, outcome, error, run ID |\n\n| Event type | Hook | Metadata captured |\n|---|---|---|\n`tool.invoked` |\n`before_tool_call` |\ntool name, sanitized arguments |\n`tool.result` |\n`after_tool_call` |\ntool name, duration (ms), error |\n`tool.denied` |\n`after_tool_call` |\ntool name, duration (ms), reason |\n`tool.persisted` |\n`tool_result_persist` |\ntool name, is synthetic |\n\n`tool.denied`\n\nis emitted instead of `tool.result`\n\nwhen a `before_tool_call`\n\nhook returns `block: true`\n\nor a user/approval flow denies the call. Denials with free-form reasons (custom `blockReason`\n\nset by a plugin, or engine-side loop-detector blocks) do not match the known phrases and will surface as `tool.result`\n\nwith the error populated.\n\n| Event type | Hook | Metadata captured |\n|---|---|---|\n`cron.executed` |\n`before_model_resolve` |\nagent ID, run ID, job ID, prompt length |\n`cron.failed` |\n`agent_end` |\nagent ID, run ID, job ID, duration (ms), error |\n\nEmitted only when the agent run's `ctx.trigger === \"cron\"`\n\n. `cron.executed`\n\nmarks the start of a cron-triggered run; it is not guaranteed to be paired with a `cron.failed`\n\nor `agent.end`\n\nif the process exits abnormally before the run completes.\n\n| Event type | Hook | Metadata captured |\n|---|---|---|\n`message.received` |\n`message_received` |\ndirection, sender (with fallback chain), sender ID, channel, account, session key, run ID, thread ID, message ID, surface, content length, timestamp, content (gzipped) |\n`message.sending` |\n`message_sending` |\ndirection, recipient, channel, session key, run ID, reply-to ID, thread ID, content length, content (gzipped) |\n`message.sent` |\n`message_sent` |\ndirection, recipient, channel, account, session key, run ID, message ID, content length, success, error, timestamp, content (gzipped) |\n`message.claimed` |\n`inbound_claim` |\nchannel, sender ID/name, is group, session key, run ID, thread ID, message ID, content length |\n`message.dispatched` |\n`before_dispatch` |\nchannel, sender ID, is group, content length |\n`message.write` |\n`before_message_write` |\nagent ID |\n\n| Event type | Hook | Metadata captured |\n|---|---|---|\n`session.start` |\n`session_start` |\nsession key, resumed from |\n`session.end` |\n`session_end` |\nsession key, message count, duration (ms), reason, session file, transcript archived, next session id/key |\n\n| Event type | Hook | Metadata captured |\n|---|---|---|\n`gateway.start` |\n`gateway_start` |\nport |\n`gateway.stop` |\n`gateway_stop` |\nreason |\n\n| Event type | Hook | Metadata captured |\n|---|---|---|\n`system.install` |\n`before_install` |\ntarget type (skill/plugin), target name, source path, request kind, plugin/skill identifiers, scan summary (files, critical/warn/info counts) |\n`system.install_hook_unavailable` |\n(registration failure) | error message |\n\n`system.install`\n\nrecords 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.\n\n`system.install_hook_unavailable`\n\nis appended each time `registerHooks`\n\nruns and `before_install`\n\nregistration 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.\n\nEvery 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).\n\n- Inserting an event changes the SMT root, creating a tamper-evident chain of state transitions\n- Deleting or modifying an event is detectable via inclusion/exclusion proofs\n- The SMT root can be anchored to the Constellation Digital Evidence network for independent verification\n\nRun `openclaw audit verify`\n\nat any time to check SMT integrity and DE checkpoint consistency.\n\nRunning `openclaw security audit --deep`\n\nmay report a `potential-exfiltration`\n\nwarning for `src/scanner.ts`\n\nand `dist/scanner.js`\n\n. This is a false positive: the built-in tool scanner uses `readFileSync`\n\nto 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.\n\n- The database file is created with\n`0600`\n\npermissions (owner read/write only) - Sensitive keys (\n`secret`\n\n,`password`\n\n,`token`\n\n,`apiKey`\n\n,`api_key`\n\n,`auth`\n\n,`credential`\n\n,`passphrase`\n\n,`jwt`\n\n,`bearer`\n\n,`cookie`\n\n,`privateKey`\n\n) 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\n`audit list`\n\noutput - CLI commands (\n`audit list`\n\n,`audit verify`\n\n,`audit export`\n\n,`audit smt …`\n\n) open the audit DB read-only, so they coexist with the running gateway via SQLite WAL — no lock contention with the writer\n\nOn versions 0.2.0–0.2.4, `openclaw audit report anomalies`\n\nconstructed the SMT service for the CLI but never restored it from disk, so `integrityViolations.tamperedEvents`\n\nwas always `[]`\n\nand the report carried the note `\"SMT has no checkpointed leaves yet — tamper scan skipped.\"`\n\neven when the on-disk SMT was populated. The skip was visibly flagged by that note (and by the related `unverifiedAnchored`\n\nlist), but no tampered-event detection actually ran from the CLI.\n\nOther integrity paths were unaffected:\n\n`openclaw audit verify`\n\ncalls`ensureReady()`\n\nitself and has always run the full replay.- The control-UI\n`/api/verify`\n\nendpoint runs inside the gateway process, where the SMT is restored during boot, and also has always been correct.\n\nTo re-scan historical windows after upgrading, re-run `openclaw audit report anomalies --since <window>`\n\nagainst the relevant time ranges; the note will now be `null`\n\nwhenever the on-disk SMT has checkpointed leaves, and any tampered events will be enumerated in `integrityViolations.tamperedEvents`\n\n.\n\nSMT checkpoint persistence moved from LevelDB to `node:sqlite`\n\n. On-disk layout changed from `<checkpointDir>/<treeKey>/`\n\n(a LevelDB directory) to `<checkpointDir>/<treeKey>.db`\n\n(a single sqlite file).\n\nOn 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:\n\n```\nrm -rf ~/.openclaw/smt-checkpoints/*/\n```\n\nThe migration also drops the `level`\n\nruntime dependency, eliminating the `python3`\n\n/ `build-essential`\n\nrequirement at install time.\n\nEach SMT tree is stored as `<smt.checkpointDir>/<treeKey>.db`\n\n. With the default `smt.treeKey: \"auto\"`\n\n(derived from `machineId`\n\n), exactly one file is reused forever and the directory does not grow. Old `.db`\n\nfiles become orphaned only if:\n\n- you change\n`smt.treeKey`\n\nto a different value (the previous tree's file stays), - the machine's\n`machineId`\n\nchanges — e.g., container rebuild, OS reinstall — leaving the old`<oldMachineId>.db`\n\nbehind.\n\nThe plugin does not GC these automatically. List the directory and delete any tree files you don't recognize:\n\n```\nls -lh ~/.openclaw/smt-checkpoints/\nrm ~/.openclaw/smt-checkpoints/<old-treeKey>.db\n```\n\nA small `__verifier__.db`\n\nis also written on each checkpoint — it's a transient verification tree and is safe to leave in place.\n\n```\nnpm install\nnpm run build    # Compile TypeScript to dist/\nnpm test         # Run the full test suite (unit + e2e)\nnpm run test:e2e # Run only the e2e suite (test/e2e.test.ts)\nnpm run clean    # Remove dist/\n```\n\nThe 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`\n\n) in addition to the main `ci.yml`\n\n.\n\nTo install the plugin from a local checkout into OpenClaw, build a tarball with `npm pack`\n\nand install it:\n\n```\nnpm install\nnpm run build\nTGZ=$(npm pack --silent)\nopenclaw plugins uninstall gate-oc-audit || true\nopenclaw plugins install \"./$TGZ\"\nopenclaw gateway restart\nrm -f \"./$TGZ\"\n```\n\nThe `uninstall`\n\nstep ensures a clean reinstall when iterating; the `|| true`\n\nguards against errors when no prior install exists. To also wipe the local extension directory and audit database for a fully clean state (development only):\n\n```\nrm -rf ~/.openclaw/extensions/gate-oc-audit/\nrm -f ~/.openclaw/audit.db*\n```\n\n", "url": "https://wpnews.pro/news/show-hn-tamper-evident-audit-trail-for-ai-coding-agent-activity", "canonical_source": "https://github.com/Constellation-Labs/gate-oc-audit", "published_at": "2026-06-15 23:01:09+00:00", "updated_at": "2026-06-15 23:18:14.997174+00:00", "lang": "en", "topics": ["ai-agents", "ai-safety", "ai-tools", "developer-tools", "ai-ethics"], "entities": ["Constellation Network", "OpenClaw", "SQLite", "SHA-256", "Digital Evidence", "Slack", "Discord"], "alternates": {"html": "https://wpnews.pro/news/show-hn-tamper-evident-audit-trail-for-ai-coding-agent-activity", "markdown": "https://wpnews.pro/news/show-hn-tamper-evident-audit-trail-for-ai-coding-agent-activity.md", "text": "https://wpnews.pro/news/show-hn-tamper-evident-audit-trail-for-ai-coding-agent-activity.txt", "jsonld": "https://wpnews.pro/news/show-hn-tamper-evident-audit-trail-for-ai-coding-agent-activity.jsonld"}}