Vouchwell

Official registry Security

Read, verify and export a tamper-evident log of what an AI agent did. Read-only.

What can you do with Vouchwell?

  • It cannot prove an action it never saw. Vouchwell records what passes
  • The cryptography has not been reviewed by anyone independent. It is
  • A restored hub cannot detect its own staleness. The evidence was in the
  • **It cannot stop an attacker with the signing key from writing false
  • Witnesses are only as independent as you make them. Three witnesses on
  • Redaction is best-effort. The detectors catch known credential formats

Install

Add this to your client's MCP configuration (Claude Desktop: claude_desktop_config.json; Cursor: .cursor/mcp.json; VS Code and others use the same "mcpServers" shape).

NPM package vouchwell
Claude Code
claude mcp add vouchwell -- npx -y vouchwell@0.8.0 mcp --log
JSON configuration (Claude Desktop, Cursor, VS Code)
{
    "mcpServers": {
        "vouchwell": {
            "command": "npx",
            "args": [
                "-y",
                "vouchwell@0.8.0",
                "mcp",
                "--log"
            ]
        }
    }
}

Documentation

From the project's README on GitHub, refreshed weekly. Links open on GitHub.

Contents
  1. Vouchwell
  2. See it in one command
  3. For teams: the hub
  4. Why this is different
  5. How it works
  6. Commands
  7. Library use
  8. Coding agents
  9. What it does not do
  10. Status
  11. License

Vouchwell

Tamper-evident receipts for AI agent actions.

Formerly Proofwire. Renamed after 0.5.0 because another company was already using the name. The proofwire and @proof_wire/* packages point here; existing logs, settings, hubs and evidence keep working unchanged.

Your agents spend money, send mail, and touch customer data. When something goes wrong, "our logs say it didn't" is not an answer — your logs are a text file you can edit.

Vouchwell makes every agent action policy-gated before it runs and cryptographically provable afterwards. One line of config. No code change.

Run it standalone on one machine, or as a hub your whole organisation writes to — with shared policy, an approvals inbox, tenant isolation, and independent witnesses that make a hosted log worth believing.

Check a real log in your browser → Then try to cheat it: edit a receipt, cut the tail off, invent your own witnesses. Nothing you paste leaves the page.

npm install -g vouchwell
vw init

Then wrap any MCP server:

// .mcp.json — before
{ "command": "npx", "args": ["-y", "@acme/mcp-crm"] }

// after
{ "command": "vw", "args": ["proxy", "--namespace", "crm", "--", "npx", "-y", "@acme/mcp-crm"] }

That is the whole integration. Every tool call now hits your policy first, and every decision — allowed, blocked, or escalated to a human — lands in an append-only log that an outside party can verify without trusting you.


See it in one command

git clone https://github.com/vouchwell/vouchwell && cd vouchwell
npm install
npm run demo

The demo runs a real agent session through a real proxy, then attacks the resulting log four ways and shows each attack being caught:

1. An agent works through a queue of tasks
  ran      ops.query        a harmless read
  ran      ops.refund       $45 refund — inside budget
  ran      ops.refund       $30 refund — $75 committed
  BLOCKED  ops.refund       $90 refund — would reach $165
           budget refunds.daily would be exceeded: 75 already committed plus 90 proposed…
  BLOCKED  ops.query        destructive SQL
  ran      ops.send_email   outbound mail            (approved by dana@acme.test)
  BLOCKED  ops.query        a live credential in the args

3. Four ways to cover your tracks, and what each one costs
  Edit a receipt in place    ✓ caught — signature does not verify
  Delete a receipt           ✓ caught — expected seq 6, found 8
  Re-sign the whole chain    ✓ caught — checkpoint covers 11 entries, log holds 8
  Truncate the log           ✓ caught — entries have been removed

For teams: the hub

docker compose up -d
docker compose exec hub node packages/server/src/bin.js bootstrap

That runs on your machine over plain HTTP, for trying it out. For a real server with a domain, automatic HTTPS and a hardened setup, use deploy/: docs/DEPLOY.md walks through it end to end.

Built and driven for real: CI's docker job builds this image, boots it, registers a log, signs and pushes receipts, checkpoints, fetches the bundle back and verifies it, confirms a bad token is refused, and checks the container actually runs read-only and non-root (scripts/docker-smoke.mjs). It also caught a real bug: a receipt missing an actor field crashed the hub with a raw SQLite error instead of a clean 4xx — fixed in buildReceipt/verifyReceipt. The two- container witnessing flow below (docker compose up -d witness, vw remote add --name witness, vw cosign --remote witness) was run by hand against both containers, through to vw check --witnesses 1 on the resulting bundle.

vw remote add --url https://hub.acme.com --token <agent token>
vw proxy --namespace crm -- npx -y @acme/mcp-crm

The proxy now fetches your organisation's active policy at startup, enforces it, records locally, and streams receipts to the hub — where an operator sees this:

Acme Financial                                          ✓ verified
  11 receipts · 30d      3 actions blocked      1 log      11 entries

  RECENTLY BLOCKED
  48s   ops.query    DENY   arguments contain anthropic_key; policy forbids sending these to a tool
  48s   ops.refund   DENY   budget refunds.daily would be exceeded: 45 already committed plus 90
                            proposed, against a cap of 100 per 24h
  1m    ops.query    DENY   destructive SQL from an agent is never permitted

What the hub adds:

Shared policyVersioned, immutable, rolled back by version. Agents fetch the active one and record its hash in every receipt.
Approvals inboxEscalations reach a human in the console. Undecided requests expire into a denial — never into an approval.
Tenant isolationEvery row names its org. A credential cannot reach another organisation's data by any route, including by guessing an id.
Witness serviceCounter-signs only roots signed by the key it bound the log to, and refuses two roots at one size. This is what defeats a split view.
Its own audit trailEvery administrative action is hash-chained. We ask you to trust a tamper-evident record, so ours is one too.

The hub is not trusted and does not need to be. It never holds a signing key, so it cannot forge a receipt — and it is not load-bearing either: an agent whose hub is unreachable keeps running, keeps recording locally, and ships the backlog when it returns.

Keys can live in a KMS or HSM rather than the hub's database (VOUCHWELL_SIGNER), backups and restores have a drilled runbook, and invitations and password resets are built in.

Full deployment and operations guide: docs/HUB.md · for reviewers: docs/AUDIT-BRIEF.md.


Why this is different

Everyone is building agent observability — dashboards that show you what your agent did, which you have to take on faith. Vouchwell builds agent evidence: a record whose integrity a third party can check independently, using nothing but the file you hand them.

Observability toolsVouchwell
Shows what the agent did✅✅
Blocks the action before it runs❌✅
Survives an insider with DB access❌✅
Verifiable by someone who distrusts you❌✅
Erasure without breaking the audit trail❌✅
Survives the vendor being the adversary❌✅

The distinction matters the day a regulator, an insurer, or opposing counsel asks "prove it." A dashboard is a claim. A signed, witnessed Merkle root is evidence.


How it works

1. Policy runs first
{
  "version": 1,
  "rules": [
    { "id": "deny.destructive-sql",
      "when": { "params.sql": { "matches": "(?i)\\b(drop|truncate|delete\\s+from)\\b" } },
      "then": "deny",
      "reason": "destructive SQL from an agent is never permitted" },

    { "id": "escalate.customer-mail",
      "when": { "target": "*.send_email" },
      "then": "escalate" }
  ],

  "budgets": [
    { "id": "refunds.daily", "match": { "target": "*.refund" },
      "field": "metrics.amount_usd", "limit": 1000, "window": "24h", "then": "escalate" }
  ],

  "egress": { "denySecrets": true }
}

Four defaults chosen so the failure modes are safe:

  • A typo is a load error, not a skipped rule. A misspelled operator in a deny rule must never quietly read as "allow".
  • A pattern that could hang is a load error too. matches runs against arguments the agent chose, and a pattern like (a+)+$ takes exponential time on the right input. Patterns that repeat a group containing its own repetition, alternatives or optional parts, and backreferences, are refused when a policy loads or is published, with a suggested rewrite ((a|b)+ is [ab]+).
  • escalate with no approver resolves to deny. A system that degrades into "allow everything" under stress is worse than no system.
  • Budgets commit at decision time, not on completion. Otherwise three pipelined refunds all evaluate against an empty ledger and every one passes a cap they collectively blow through.
Start from a template

Eleven ready-made policies cover what nearly every deployment needs to stop:

TemplateWhat it does
secretsRefuses any call carrying an API key, token, private key, card number or IBAN
no-personal-dataAlso refuses emails, phone numbers and SSNs: for tools that must never see personal data
destructive-sqlRefuses DROP, TRUNCATE, ALTER TABLE, GRANT/REVOKE, and DELETE or UPDATE with no WHERE
sql-writes-need-approvalA person approves every INSERT, UPDATE, DELETE and MERGE
paymentsA person approves any payment over $500, or past $2,000 a day per person
outbound-messagesA person approves every send, post, reply, forward and publish
outbound-rate-limitMessages go out unapproved, at most 30 a session per hour
shell-safetyRefuses rm -rf, mkfs, force-pushes, chmod 777 and `curl … \sh`; file deletes need a person
production-guardA person approves anything whose environment is production
loop-guardStops the same tool after 60 calls in 5 minutes
read-onlyAn allowlist: lookups run, everything else is refused
vw init --template secrets,destructive-sql,payments      # a new project
vw policy template shell-safety loop-guard --out vouchwell.policy.json
vw policy template payments --explain                     # what it assumes

Combined, refusals are placed ahead of allows, so read-only can never let through something destructive-sql refuses. The output is ordinary policy: read it, edit it, push it to a hub with vw policy push, or load it in code with new Policy(composePolicy(['secrets', 'payments'])). Templates match the names most tools use (send_email, params.sql, params.command), which may not be yours, so check them against your own traffic first, as below.

Try a policy before it can block anything
vw proxy --monitor -- npx -y @acme/mcp-crm

Monitor mode evaluates the policy exactly as enforcement would, then forwards every call anyway, including ones the egress guard would stop. Nothing is blocked and no one is asked to approve anything. It is the status quo plus a record. vw log --would-block and vw stats then show what the policy would have stopped, and which rule did it, against your real traffic. When that list contains only things you want stopped, drop the flag.

The receipts stay truthful. A call that ran is recorded as allow, never as a deny that didn't happen. It also carries "enforced": false and, where the policy objected, "wouldBe": "deny" or "escalate", all inside the signature. So a bundle shows an auditor that the policy was only observed, and a monitored call counts against budgets because it really spent the money. Set "monitor": true in vouchwell.config.json to make it the default on a machine; --enforce overrides that. A hub's policy cannot switch it on.

Before you drop --monitor, or before you change a policy that is already enforcing, replay what you recorded against the new version:

vw policy test vouchwell.policy.json
vw policy test next.policy.json --fail-on-change   # exit 1 if any verdict differs

It lists each call whose verdict would change (deny → allow, allow → escalate and so on) and the rule that caused it. Each call is judged at the time it was recorded, and budgets count only what the new policy would have let through. The log keeps redacted arguments, not raw ones, so a call with masked values is marked ≈: a rule that looked at the masked value cannot be replayed exactly.

2. Every action gets a receipt
{
  "v": 1, "log": "lg_14b9c3dd", "seq": 7, "prev": "3fa4a544…",
  "ts": "2026-09-20T17:19:00.057Z", "phase": "intent",
  "actor": { "agent": "claude-opus-5", "principal": "ops@acme.test", … },
  "action": {
    "kind": "tool_call", "target": "ops.refund",
    "metrics": { "amount_usd": 45 },
    "params": { "hash": "74155b6d…", "size": 42, "preview": { "order": "ord_8821", … } }
  },
  "decision": { "outcome": "allow", "policy": "8ab2e528…", "rules": ["refunds.under-cap"] },
  "attest": { "alg": "ed25519", "kid": "pw106d7bf7…", "sig": "…" }
}

Four properties, each because a specific dispute is foreseeable:

  • Signed — Ed25519, deterministic, key never leaves the runtime.
  • Chained — each receipt commits to its predecessor's hash. Delete or reorder one entry and everything after it breaks.
  • Tree-anchored — RFC 6962 Merkle log, the same construction Certificate Transparency uses. Inclusion proofs tie one receipt to a published root; consistency proofs prove the log only ever grew.
  • Sealed — arguments are stored as salted commitments plus a redacted preview, never as raw payloads.
3. Nothing sensitive is in the log

The log holds sha256(salt ‖ payload) and a preview with secrets masked. Salts live in a separate file. So:

  • A receipt is publishable as written. No "sanitise before exporting" step to forget.
  • Erasure and audit stop being in conflict. vw shred --before 2026-01-01 destroys the salts. Those payloads become permanently unopenable — by you, by a court, by whoever steals the directory in 2029 — while every signature, chain link and inclusion proof still verifies. That is a real GDPR Article 17 erasure that does not gut your audit trail.
4. Witnesses close the last hole

A log signing its own root can show two different histories to two auditors — the classic split-view attack, and the one thing a self-hosted log cannot defend against alone. Independent witnesses only counter-sign a root that extends the last one they saw, so a split view requires every witness to collude.

vw witness keygen               # on the witness's machine
vw trust pw1a4f… <publicKey>    # on the log's machine
vw check evidence.json --witnesses 2 --witness-keys witnesses.json

witnesses.json holds the witnesses' public keys as their operators published them — never taken from the bundle. A bundle's own keyring comes from the party whose honesty is in question, so it cannot vouch for witnesses: an operator could add any number of fresh keys and "witness" their own checkpoints. Only signatures from keys you pinned are counted, and asking for --witnesses without pinning any is refused rather than answered by counting whatever the bundle contains.

Witnessing doesn't have to be a chore. Put "witnesses": ["w1", "w2"] in vouchwell.config.json and every vw proxy session ends with each witness signing. On a hub, vw witnesses add has every hub checkpoint co-signed, and a witness refusing a checkpoint lands in the audit trail. See the hub guide.

5. Your SOC sees it where it already looks

A hub streams receipts and its own audit trail to Splunk, Datadog, OpenTelemetry or a signed webhook. Delivery is at least once, with nothing lost while a destination is down, and an action's parameters never leave the hub:

VOUCHWELL_STREAM_TOKEN=<HEC token> vw streams add splunk --type splunk --url https://splunk.example.com:8088
vw streams add soc --type webhook --url https://soc.example.com/hook --receipts blocked

See streaming to your SIEM.


Commands

Setup
  vw init                        create a log, a starter policy, and a config

Run
  vw proxy -- <cmd...>           wrap an MCP server; enforce policy, write receipts
    --namespace <ns>             prefix tool names in receipts
    --principal <id>             who the agent is acting for
    --approve tty|webhook|deny   how escalations get resolved
    --monitor                    block nothing; record what policy would block
    --enforce                    gate even if the config says "monitor": true

Inspect
  vw log                         recent receipts  [--tail N --denied --would-block --unfinished --target X --json]
  vw stats                       totals, spend, busiest tools
  vw policy test [file]          replay the log against a policy  [--fail-on-change --json]
  vw dash                        browsable dashboard  [--port 7788]

Prove
  vw verify                      audit the local log end to end  [--fail-on-unfinished]
  vw prove <seq>                 inclusion proof for one receipt
  vw export [file]               evidence bundle for a third party
  vw check <file>                verify a bundle with nothing but itself [--log-key kid=pub --json]
  vw report                      evidence pack for auditors (AI Act, SOC 2); see docs/EVIDENCE.md

Hub
  vw remote add --url <hub> --token <key>   connect this machine (https, or
                                 http to localhost; --insecure to override)
  vw push                        ship local receipts the hub is missing
  vw remote-verify <log>         verify a hosted log from outside
  vw policy push|pull|list       manage the org's shared policy
  vw cosign                      have witnesses counter-sign your latest root
                                 (--remote a,b, or "witnesses" in the config)
  vw witnesses list|add|remove   outside witnesses for every hub checkpoint (admin key)
  vw streams list|add|test       events to Splunk, Datadog, OTel, a webhook (admin key)

Govern
  vw keys                        public keys to publish for verifiers
  vw witness keygen              create an independent witness identity
  vw trust <kid> <pubkey>        trust a witness or another signer
  vw shred --before <date>       destroy payload commitments, keep the audit trail

vw verify exits non-zero when a log has been altered — put it in CI. It also lists calls that were authorised and sent but never finished (the agent died mid-call); --fail-on-unfinished makes those exit 3.


Library use

Not on MCP? Wrap the tools themselves. The core is a small, dependency-free ES module.

import fs from 'node:fs';
import { ProofLog, Policy, Recorder, PolicyDenied } from '@vouchwell/core';

const rec = new Recorder({
  log: ProofLog.open('.vouchwell'),
  agent: 'support-bot',
  principal: 'ops@acme.com',
  policy: Policy.parse(fs.readFileSync('vouchwell.policy.json', 'utf8')),
  metrics: (tool, args) => (tool === 'stripe.refund' ? { amount_usd: args.amount } : {}),
  approver: async (req) => askSomeone(req),   // optional: who answers an escalation
});

const refund = rec.wrap('stripe.refund', async ({ order, amount }) => stripe.refunds.create({ ... }));
await refund({ order: 'o_1', amount: 45 });   // checked, recorded, then run; throws PolicyDenied if refused

The same rules as vw proxy, receipt for receipt: the policy (budgets and rate limits included) decides first; an allowed call gets an intent receipt before it runs and a linked outcome after; a refused one never runs; monitor: true records what would have been blocked without blocking it. Call rec.finalize() on shutdown.

Tools described as objects with an execute function (the Vercel AI SDK's tool(), Mastra) can be wrapped in one go:

import { recordTools } from '@vouchwell/core';
const result = await generateText({ model, tools: recordTools(rec, { weather, refund }), prompt });

Python agents have the same in vouchwell, with adapters for LangChain and the OpenAI Agents SDK.


Coding agents

Claude Code does most of its work with its own tools (the shell, file reads and edits, fetches), not MCP servers. Vouchwell records and gates those too, through Claude Code's hooks. The quickest way is the plugin, typed in Claude Code:

/plugin marketplace add Vouchwell/vouchwell
/plugin install vouchwell@vouchwell

Or set it up per project with the CLI, for Claude Code, OpenAI's Codex CLI or Cursor:

vw policy template coding-agent shell-safety secrets --out vouchwell.policy.json
vw hook install                    # Claude Code
vw hook install --client codex     # Codex CLI
vw hook install --client cursor    # Cursor

From the next session, every tool call is checked against the policy before it runs, and gets a receipt:

  • a refused call is denied, with the policy's reason;
  • an escalated one goes to the person at the keyboard;
  • the rest are recorded.

See docs/CODING-AGENTS.md.

Check it in CI

Commit the log as a bundle and let every push check it. The verify action runs the verifier from its own source: no install, no token. It fails the build if a bundle was edited, truncated, or signed by a key other than yours:

- uses: Vouchwell/vouchwell/verify@v0.8.0
  with:
    log-keys: evidence/log-keys.json   # from `vw keys`
Let an agent read its own record

vw mcp serves a log as an MCP server. Any MCP client (Claude Code, Claude Desktop, Cursor, Codex, and others) can then answer "what did you do?" and "prove it" from the record itself. Its tools are log_entries, verify_log, log_stats, check_bundle and export_evidence. Every tool reads; none can change the log.

{ "mcpServers": { "vouchwell": { "command": "npx", "args": ["-y", "vouchwell", "mcp", "--log", "/path/to/.vouchwell"] } } }
How this repository was built

An AI agent builds Vouchwell, and this repository records it doing so. Its Claude Code hooks run vw hook on every tool call made while working here. The logs are published in evidence/ as bundles that hold commitments only, so nothing from the machine leaks. CI verifies each one on every push with the verify action, against the keys pinned in evidence/log-keys.json.


What it does not do

Stated plainly, because a security tool that overstates its guarantees is worse than none:

  • It cannot prove an action it never saw. Vouchwell records what passes through it. An agent with a second, unwrapped path to the same API leaves no receipt. Route tools through the proxy and treat unwrapped credentials as the hole they are.
  • The cryptography has not been reviewed by anyone independent. It is checked against published RFC 6962 and RFC 8032 vectors rather than only against itself, but that is not the same thing. See docs/AUDIT-BRIEF.md.
  • A restored hub cannot detect its own staleness. The evidence was in the data the restore discarded; only an agent or an auditor holding later evidence can see the gap. Re-push from every agent after a restore.
  • It cannot stop an attacker with the signing key from writing false receipts going forward. It can stop them rewriting the past, once a checkpoint has been witnessed. Keep the key in a KMS or HSM in production.
  • Witnesses are only as independent as you make them. Three witnesses on infrastructure you control are one witness.
  • Redaction is best-effort. The detectors catch known credential formats and common PII. They will not catch a secret shaped like prose. Commitments, not redaction, are what keep payloads out of the log.

See docs/THREAT-MODEL.md for the full analysis, including the known gaps.


Status

Cryptography, policy engine, proxy, CLI, the multi-tenant hub and the Python SDK are complete and covered by 400+ tests: the published RFC 6962 Certificate Transparency reference tree and RFC 8032 Ed25519 vectors, exhaustive Merkle proof verification for every tree size up to 128 and every (m, n) consistency pair up to 48, and end-to-end tests that run a real agent through a real proxy against a real hub over HTTP.

npm test

Measured on a 4-core laptop: 2,300 receipts/s ingested, 9,500 inclusion proofs/s, 109 ms to export 4,000 receipts as a verifiable bundle, 2.5 KB per receipt on disk, and zero external dependencies. Reproduce with node packages/server/bench/load.js.

Wire format and policy schema are versioned ("v": 1) and will be migrated, not broken.

Guides: trying it on real agents · Python SDK · deploying a hub or witness · Slack approvals · single sign-on · streaming to your SIEM · evidence packs for auditors · the hub · threat model

License

Apache-2.0. The format, the verifier, and the CLI are open and will stay open: evidence you cannot verify without a vendor's permission is not evidence.

Frequently asked questions

What is Vouchwell?

Read, verify and export a tamper-evident log of what an AI agent did. Read-only.

How do I install Vouchwell?

It runs on your machine. In Claude Code: claude mcp add vouchwell -- npx -y vouchwell@0.8.0 mcp --log. For other clients, paste the JSON configuration from this page into their MCP settings.

Does Vouchwell need an API key?

Its registry entry lists no required key or secret. Some features may still need an account with the service it connects to.

Is Vouchwell open source?

Its code is public on GitHub, but the repository does not declare a standard license.

Who maintains Vouchwell?

It is published by Vouchwell. AI.tn lists it but does not maintain or endorse it.

From the official MCP Registry. Listing is not an endorsement: review a server's code and permissions before giving it access to your data.