Quickstart

Record your first run in 60 seconds

Install the SDK, wrap your agent, and verify the result yourself. No account is required to use the open-source software. Python and TypeScript are byte-for-byte compatible.

Last updated

New to the terminal? If you vibe-coded your way here and have never run a command line, start with Provenrail from zero instead. It spells out every keystroke, from opening the terminal to your first verified proof. This page is the faster reference once you are comfortable.
Install Guard a coding agent Python TypeScript Verify Guardrails & alerts Spend caps Transcripts & redaction CLI Self-host the recording server Anchor your chain AI code attribution Team & SSO Licensing Source

Install

The CLI (pr quickstart, verifier, and recording server). Recommended: install with uv, which brings its own pinned Python, so a later brew upgrade or system Python change cannot orphan it:

uv tool install provenrail

No uv yet? One line installs it: curl -LsSf https://astral.sh/uv/install.sh | sh.

Prefer pip? It works, but install it inside a virtualenv, not a system or Homebrew Python. A global pip install breaks the day Homebrew upgrades Python (the package lands in the old interpreter's site-packages). If that happens, recover with pipx reinstall provenrail or switch to the uv command above:

python3 -m venv .venv && source .venv/bin/activate
pip install provenrail

Using the SDK from your own project? Add it to that project's environment, again not system Python:

uv add provenrail   # or: pip install provenrail (venv active)

TypeScript / Node 20+ (recording SDK):

npm install provenrail

Guard a coding agent (no code)

If you run Claude Code, this is the shortest path to something useful, and it needs nothing installed. Inside Claude Code:

/plugin marketplace add pofky/provenrail
/plugin install provenrail-guard@provenrail

That is the whole setup. The plugin carries its own dependency-free engine, so 44 rules are armed on the next tool call with no package, no account and no config file. /guard-status shows what it has actually stopped, /guard-card prints that history in a form safe to paste anywhere, and /guard-rules lists every rule. The full guide covers what each pack blocks and why some rules ask instead of deny.

To wire the hooks yourself instead, or to record as well as block:

pr quickstart       # local recording server, no account, nothing leaves your machine
pr guard install    # arms destructive + secrets + production + access, installs the hooks

From your next Claude Code session in that folder:

pr guard status     # what is armed, and what it blocked
pr guard receipt    # export the proof, then pr verify it yourself
pr guard uninstall  # removes only our hooks; your own are untouched

The verdict is computed offline, before anything touches the network, so a recording server that is down can never turn a deny into an allow. A decision that cannot be recorded is journalled locally and reported as unsigned and pending, never as proof.

Honest scope. This covers the tool calls Claude Code routes through its hooks. It cannot constrain a process that never calls them. Per-session limit rules carry their counts in a local state file so a blast-radius cap holds across hook processes; that file is editable, so it is a convenience rather than evidence, and deny and require_oversight never read it. Clear it with pr guard reset. Other agent hosts are not claimed until their hook contract has been read and tested.

Python quickstart

One command sets up a local recording server (the "sink" in the API docs) and writes .provenrail.json, so your code carries no URLs or tokens:

pr quickstart   # starts the local recording server + writes config

Then two lines in your code:

import provenrail as fr

with fr.record("my-agent"):
    ...   # your agent runs; model and tool calls are captured

fr.record(...) provisions a stream, opens a signed session, and seals and drains it off-box when the block exits. A decorator form exists too: @fr.recorded("nightly-job"). Stop it with pr quickstart --stop; point at a recording server you host with pr quickstart --url <URL>.

Run your agent as many times as you like: each run becomes its own sealed session on the same stream, and one export verifies them all. The first run also creates .provenrail.key, the device signing key reused by every later run. Keep it out of version control (add it to .gitignore next to .provenrail.json); losing it only means future runs sign under a new identity.

Drop-in capture (one line per SDK)

from provenrail.integrations import instrument_openai, instrument_anthropic, instrument_mcp

instrument_openai(openai_client, fr)      # every model call captured
instrument_anthropic(anthropic_client, fr)
instrument_mcp(mcp_session, fr)           # every MCP call_tool captured

LangChain / LangGraph (Article 12 callback handler)

from provenrail.integrations.langchain import ComplianceCallbackHandler

with fr.record("my-agent") as rec:
    chain.invoke(x, config={"callbacks": [ComplianceCallbackHandler(rec)]})

ComplianceCallbackHandler emits a signed, hash-chained record of every model and tool call, the cryptographically verifiable audit log the LangChain community asked for in issue #35691.

Claude Agent SDK (PreToolUse / PostToolUse hooks)

from provenrail.integrations.claude_sdk import provenrail_hooks
from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions

with fr.record("my-claude-agent") as rec:
    options = ClaudeAgentOptions(hooks=provenrail_hooks(rec))
    # every tool call the agent makes is recorded off-box

Agno (agent-level tool hooks)

from provenrail.integrations.agno import provenrail_tool_hook

with fr.record("my-agno-agent") as rec:
    agent = Agent(model=..., tools=[...], tool_hooks=[provenrail_tool_hook(rec)])

Every tool call an Agno agent makes becomes a signed, hash-chained record, the cryptographic receipt layer the Agno community asked for in issue #7518. Failures are recorded as evidence and re-raised unchanged, so Agno's own error handling is untouched.

Hermes Agent (observer plugin hooks)

from provenrail.integrations.hermes import register_provenrail

def register(ctx):                       # ~/.hermes/plugins/provenrail/__init__.py
    register_provenrail(ctx, agent_name="my-hermes-agent")

Registers on the hermes.observer.v1 contract, so every tool dispatch across the CLI, gateway, cron, and subagents is recorded. Hermes closed its in-tree audit-log proposal (issue #487) precisely because a hash chain verified only against itself proves nothing against an operator with write access. That is the gap the off-box chain, trusted timestamps, and witnessed transparency log are built to close.

TypeScript quickstart

import { record } from "provenrail";

await record("my-agent", async (pr) => {
  await pr.recordModelCall("openai", "gpt-5", { prompt }, out, { usage });
});

A run recorded in TypeScript is byte-for-byte compatible with one recorded in Python: the same recording server accepts it and the same two verifiers prove it. Node 20+ is required (WebCrypto Ed25519).

Verify a run

Verification trusts neither the agent nor the recording server. Anyone can run it, with no account:

pr verify bundle.json --pin pin.json

To verify your own recorded run after pr quickstart, export it from the local recording server first (this uses the read token quickstart saves), then verify the bundle:

pr export my-run.json     # pulls your sealed run out of the recording server
pr verify my-run.json     # recompute everything; trust nobody

Or verify in your browser, with the bundle never leaving your device: provenrail.com/verify. Try the live verified demo or watch it catch a tampered run.

Guardrails and instant alerts

By default Provenrail is a recorder: it observes and proves, it does not intervene. A policy turns it into an enforcer as well. Rules are evaluated at the dispatch boundary, the action is blocked before it runs, and the decision is written into the same signed chain, so the record proves the guardrail was in force and that the call really was stopped.

Nothing is suspicious by default. There is no built-in threat library and no anomaly detection. If you declare no policy, nothing is ever blocked and no alert can fire. That is deliberate: we would rather you state your own rules than have us guess and be wrong in both directions.

Start with the prebuilt packs

You do not have to write rules from a blank page. The catalogue ships packs of prebuilt rules for the things agents actually do damage with, and you enable them by name:

{
  "policy": { "use": ["destructive", "secrets", "money"] }
}
Prebuilt guardrail packs
PackCovers
destructivedelete/drop/truncate tools, rm -rf, destructive SQL, unbounded DELETE
secretsAWS keys, private key blocks, API tokens, JWTs, .env reads
moneytransfers, payments, refunds, charges (human approval, not a hard block)
productiondeploys, migrations, DNS, force push, terraform destroy
accesspermission grants, IAM changes, chmod 777, MFA disabling
exfiltrationunbounded SELECT *, outbound uploads, paste sites
blast-radiusper-session caps on email, messaging, and total tool calls

You can enable a whole pack or a single rule by id ("use": ["secrets.aws-access-key"]), mix packs with your own rules, and a misspelled name is rejected rather than skipped. List everything with pr rules (add --verbose for each rule's false-positive note).

Enabling a pack is not proof of coverage. These rules match tool names and argument text, and every codebase names its tools differently: a rule for delete_* does nothing if your tool is called remove_record. Turn the guess into evidence by checking against a real recorded run:

pr rules --check bundle.json   # which rules would match YOUR tool names

Or declare your own rules

No code change is needed, so whoever owns the deployment can set the rules even if they did not write the agent:

{
  "policy": {
    "rules": [
      {"id": "no-destructive-tools", "effect": "deny",
       "event_type": "tool_call", "tool": "delete_*",
       "reason": "destructive tool"},

      {"id": "no-credentials-in-args", "effect": "deny",
       "arg_contains": "AKIA[0-9A-Z]{8}",
       "reason": "argument looks like an AWS key"},

      {"id": "wire-needs-human", "effect": "require_oversight",
       "tool": "wire_transfer"},

      {"id": "email-burst", "effect": "limit",
       "tool": "send_email", "max_per_session": 20}
    ],
    "session_spend_cap_usd": 5.0
  }
}
Policy rule effects
EffectWhat it does
denyBlocks the action and records the denial.
require_oversightAllows it only if a human-oversight event was recorded in this session first; otherwise blocks.
limitAllows up to max_per_session matches, then blocks.

Rules match on event_type, tool, resource and provider using case-insensitive globs, plus an optional arg_contains regex over the call's arguments. A malformed policy is rejected loudly rather than ignored: an unknown field, a bad regex, a duplicate rule id or a limit with no cap all raise at startup. A typo that silently disables a guardrail is the worst possible failure, because you would believe you were protected.

Get told the moment something is blocked

Register a webhook for the policy.denied event. It fires at ingest, in the same moment the record arrives, not on the anchor schedule:

curl -X POST $SINK/v1/webhooks -H "Authorization: Bearer $ACCOUNT_KEY" \
  -d '{"url":"https://ops.example/hooks/pr","events":["policy.denied"]}'

You get one signed POST per blocked action, carrying the rule, the reason, the target, the session and the record hash. Every delivery is HMAC-SHA256 signed with the per-webhook secret in the X-Provenrail-Signature header, so you can confirm it came from your recording server. Delivery runs off the ingest path, so a slow or dead endpoint of yours can never slow down or block your agent.

Verify it over the raw request body, before any JSON parsing, and compare in constant time:

import hmac, hashlib, json, time

def handle(raw_body: bytes, headers, secret: str):
    expected = "sha256=" + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    if not hmac.compare_digest(expected, headers["X-Provenrail-Signature"]):
        raise ValueError("not from the sink")          # never == on a MAC
    event = json.loads(raw_body)
    # Replay defence is inside the signed body, so it cannot be stripped:
    #   event["id"] is unique per alert  -> drop one you have already handled
    #   event["at"] is when it was sent  -> reject anything older than a few minutes
    if seen(event["id"]): return
    if time.time() - iso_to_epoch(event["at"]) > 300: raise ValueError("stale")
    act_on(event)

A delivery that gets no 2xx is retried twice and then dropped, so your handler should be idempotent, which the id check above already makes it.

Alert endpoints must be publicly reachable. A webhook pointing at localhost, a private range, or a link-local address is rejected, and the host is re-resolved at delivery time as well, so a name that turns internal later stops being called. This defeats SSRF and DNS rebinding, but it also means you cannot test alerting against a server on your laptop: use a public endpoint or a tunnel.

The other events are about the record rather than the behaviour: integrity.tampered, integrity.recovered and integrity.first_anchor, evaluated after each anchor.

Review what was blocked

pr risk bundle.json          # every denial, grouped by rule; exit 1 if any
pr risk bundle.json --json   # same, machine-readable for CI

It exits non-zero when anything was blocked, so CI can fail a run in which the agent tried something forbidden. In the dashboard, denials appear as a red Blocked tile and as red rows in the session timeline. In the SIEM export (/v1/streams/{id}/export.ndjson) each denial is one line with the rule and reason, ready for Splunk, Elastic or Datadog.

Because the decisions live inside the signed chain, pr verify proves they were recorded at the time and have not been edited since. That is the part a log file cannot do.

Honest scope. Enforcement covers the dispatch points the SDK wraps. An agent that bypasses the SDK entirely is not constrained by a policy, and we never claim otherwise. What a clean record does prove is that, for every call that went through the recorder, the stated policy was applied and the recorded decisions match it. For capture that an agent cannot skip, put pr sidecar in front of the model API and lock egress to it.

Spend: caps that bind, and a bill you can check

An agent that loops overnight is a bill you find out about from the invoice. Provenrail records the token usage each model call reports, prices it, and enforces caps at the same boundary as the guardrails, so the stop is recorded in the same signed chain as everything else.

Cap the spend

Budgets sit next to your rules in .provenrail.json. Scope is session, day or total; warn_at is a fraction of the limit that warns without ever changing the verdict.

{
  "policy": {
    "use": ["destructive", "money"],
    "budgets": [
      { "id": "per-session", "scope": "session", "limit_usd": 0.50, "warn_at": 0.8 },
      { "id": "daily",       "scope": "day",     "limit_usd": 5.00 }
    ]
  }
}

pr guard status shows every budget, what it has spent and what is left.

See what a run cost

pr spend my-run.json          # per session and per model
pr spend                      # the cross-run ledger, all agents
pr spend --agent billing-bot  # one agent

Check it against the real bill

Our figure is an estimate from published list prices, so the number that matters is the gap between it and what you were actually charged. Feed in the provider's usage export:

pr reconcile my-run.json --invoice openai-usage.csv

It exits 1 when the invoice bills for a model that never went through the recorder, which is the finding worth alerting on: spend from an uninstrumented service, a scheduled job, or a machine outside your deployment. A price variance on a model you did record is reported for you to read and does not change the exit code, because a list-price estimate is expected to drift from a negotiated rate.

Honest scope. Budgets bind model calls made through the SDK, using the usage each call reports. Tool hooks carry no model spend. A model we have no price for is counted as unpriced rather than as zero, and pr spend --json reports unpriced_calls so a total is never silently short.

Prove a transcript, and disclose a redacted one

By default the record stores a hash of each prompt and response, not the text, so the record itself leaks nothing. To show someone that the transcript you hold is the one that was recorded, check your copy against the fingerprint:

pr verify-content my-run.json --file prompt.json --field request

It exits 0 on a match and 1 when the content is not what was recorded. Where fields were redacted at capture, pr disclose renders a view of the bundle with named fields opened for a specific recipient, leaving the rest sealed:

pr disclose my-run.json --openings keys.json

CLI reference

If pr runs the wrong program. pr is also the POSIX paginator from coreutils. On most systems the install directory comes first on PATH and ours wins; on Windows under Git Bash it does not, and pr --version answers pr (GNU coreutils) 8.32. Use the module entry point instead, which nothing can shadow: python -m provenrail verify bundle.json. Every subcommand is identical.
pr quickstart        # local recording server + config, zero tokens
pr demo              # records a session, anchors it, writes bundle.json + pin.json
pr export my-run.json  # export your own recorded run, locally
pr verify bundle.json --pin pin.json     # verify, trusting nobody
pr activate <key>     # store your licence key once; other commands find it
pr anchor-push bundle.json    # independent timestamp; sends a fingerprint, no records
pr anchor-verify bundle.json anchor-receipt.json  # check a receipt offline
pr attest --blame      # signed statement of which commits an AI agent had a hand in
pr attest-anchor ai-attestation.json   # independent RFC 3161 time on that statement
pr attest-verify ai-attestation.json   # what your customer runs, against their own clone
pr rules             # list prebuilt guardrail packs; --check bundle.json for coverage
pr risk bundle.json    # every action a policy blocked (exit 1 if any)
pr report --regime eu-ai-act bundle.json --md   # map the record to a regime
pr pack bundle.json    # self-contained evidence pack (zip) for auditors
pr diff run-a.json run-b.json            # diff two runs (exit 1 if they differ)
pr spend bundle.json   # what this run cost, per session and per model
pr reconcile bundle.json --invoice bill.csv  # recorded spend vs the provider's bill
pr verify-content bundle.json --file prompt.json  # prove a transcript you hold was the one recorded
pr disclose bundle.json --openings keys.json   # reveal redacted fields to a named recipient
pr ots-verify proof.ots --data-sha256 H  # verify a Bitcoin (OpenTimestamps) proof
pr serve --anchor rfc3161               # run the recording server yourself (real trusted time)
pr sidecar --upstream https://api.openai.com   # out-of-process capture proxy
pr witness --log <origin>=<pubkey>        # independent witness on separate infra

Self-host the recording server

The recording server (the "sink" in the API and the source) is the append-only service that receives records. You run it; your records never reach us. For real third-party trusted time, anchor with RFC 3161:

pr serve --anchor rfc3161 --tsa https://freetsa.org/tsr

Or with Docker:

docker compose up
Harder to skip. Run pr sidecar as an outbound proxy and lock model egress to it, so capture is mandatory rather than a default. Add --fail-closed to refuse any call that cannot be recorded.

Anchor your chain

Everything above proves your records are internally consistent. It cannot prove they existed before the argument started, because you signed them and you hold them. That is the one thing you cannot do for yourself, and it is what anchoring is for: you publish a fingerprint of your chain somewhere you do not control, and later you can show the fingerprint has not moved.

pr anchor-push bundle.json

That is the whole command: it defaults to the hosted service and to the licence key pr activate stored, and writes the receipt to anchor-receipt.json. Point it elsewhere with --url, or pass a different key with --key.

Your first anchor is free. Sign in at provenrail.com/account, claim the key, and run pr activate <key> once. One anchor per account, so you can hold a receipt with a public authority's signature on it before deciding whether to pay for more. After that it is a Builder or Team plan, and the key you already have names your account and your plan, so the service verifies it without a lookup and there is nothing else to sign up for.

What actually travels

Three things: a stream label you choose, a 64-character fingerprint of your records (a SHA-256 Merkle root over their hashes), and how many records it covers. You keep every record. There is no field in the request a record could arrive in, so we cannot read your prompts, reconstruct your chain, or show anyone what your agents did, because we are never sent it. That is the reason we can host this part and not the rest.

What you get back

A receipt carrying an RFC 3161 trusted timestamp from a public authority, so the date is not ours to assert either, and a URL an auditor can open with no account and no permission from you. Save the receipt:

The receipt is written for you as anchor-receipt.json (use --receipt-out for a different path). Then anyone can check it against your records, offline, without asking us anything:

pr anchor-verify bundle.json anchor-receipt.json

That recomputes the root from the records in the bundle, compares it with the one under the signature, validates the timestamp's certificate chain, and verifies the bundle's own hash chain. If someone edited a record after it was anchored, this is what says so.

What it refuses

Coverage of a stream only ever grows. If you anchor 1,000 records, something goes wrong at 400, and you try to anchor the shorter chain, the service refuses rather than signing over your own history. The same length cannot get two different roots either. An exact repeat of a request you already made returns the original receipt rather than a second one, so retries are safe.

Or anchor to yourself. pr serve --anchor rfc3161 on a machine you control accepts the same command with a different --url. You get the append-only history and the receipt and a real trusted timestamp; what you do not get is independence from yourself, which is the half worth paying for.

AI code attribution

Supplier contracts have started asking which parts of a codebase an AI agent wrote. The standard answer, a Co-authored-by: trailer or a git note, is written by the party being asked and can be rewritten afterwards, so it is a claim rather than evidence.

pr attest --blame                      # writes ai-attestation.json, signed with your device key
pr attest-anchor ai-attestation.json   # RFC 3161 timestamp from an independent authority
pr attest-verify ai-attestation.json --receipt attest-receipt.json

pr attest reads the commits in a range, detects AI-assisted ones from commit trailers and author or committer identity, and names the source of every finding so a reader can weigh it rather than trust it. --blame adds what share of the lines currently in the tree came from AI-assisted commits; lines from commits outside the range are reported as unattributed, never as human. --records bundle.json raises the evidence grade of any commit authored inside a recorded agent session, because a signed record is not something the committer typed.

What signing buys is specific: git commit ids are content hashes, so the document cannot later be pointed at a different tree, and cannot be quietly rewritten once a dispute begins. What it does not buy is the truth of the findings, and the document says so in its own limits field. A commit whose author stripped the trailer reads as human-authored. Detection covers nine named tools plus generic trailers, so AI involvement is understated rather than overstated.

pr attest-verify is what the receiving side runs. It checks the signature, that every attested commit exists in their clone with the same author and dates, and that re-running the detectors reaches the same verdict. A document that passes the first two and fails the third was edited before signing, which a signature alone cannot catch. Full detail: what it proves and what it does not.

Team and SSO

On the Team plan you can invite up to 10 teammates with role-based access and connect your identity provider, so staff sign in with the IdP you already use. Everything is account-authenticated against your own server; the integrity guarantee is unchanged on every plan.

Members and roles

Invite a teammate and they get their own key (shown once). Four least-privilege roles: owner (billing and everything), admin (manage streams, members, webhooks), member (run agents, export their own streams), and viewer (read only). An actor can only grant roles at or below its own.

curl -X POST $URL/v1/members -H "Authorization: Bearer $ACCOUNT_KEY" \
  -d '{"role":"admin","email":"[email protected]"}'
{
  "member_id": "mbr_9cf5...",
  "role": "admin",
  "api_key": "pr_mk_rPk8...",          # shown once, store it now
  "note": "store this key now; it is shown only once"
}

Single sign-on (OIDC)

SSO is API-first: you configure your IdP once, then your staff present an ID token from that IdP to receive a session key. Provenrail makes no network call (the JWKS is pinned out of band), and validation is strict, only RS256 and EdDSA are accepted, with issuer, audience and expiry all checked. New users are provisioned just-in-time at the default role.

# 1. Owner configures the org IdP once (issuer, audience, pinned JWKS, default role)
curl -X PUT $URL/v1/sso/config -H "Authorization: Bearer $ACCOUNT_KEY" \
  -d '{"issuer":"https://acme.okta.com","audience":"provenrail",
       "jwks":{ ... },"default_role":"member","email_domain":"acme.com"}'

# 2. A teammate authenticates with your IdP, then exchanges the ID token for a member key
curl -X POST $URL/v1/sso/login -d '{"id_token":"eyJhbGciOiJSUzI1Ni..."}'
{
  "member_id": "mbr_9ca5...",
  "role": "member",                    # JIT-provisioned at the default role
  "api_key": "pr_mk_lAFz..."
}
Strict by design. The IdP signing key is selected from your pinned JWKS by kid; alg: none and HMAC algorithms are rejected, so there is no algorithm-confusion bypass. An optional email_domain restricts which addresses may provision. Every login is written to the tamper-evident audit log.

Licensing

Provenrail is open-core and dual-licensed:

Get a commercial license key from your account, then activate it on your server:

pr activate prl_live_...        # verifies offline, stores the key
pr serve                       # now runs at your licensed tier

Verification is fully offline: the package ships the Ed25519 public key and checks the signed key locally, so nothing phones home and a licensed build works air-gapped. The license is a commercial control, not DRM; the open-source integrity guarantee is identical on every tier.

Source and spec

The wire format and verification steps are a frozen, public specification, so a third party can write an independent verifier and check the same bundles. The in-browser verifier at /verify is a second, independent implementation of that spec, kept in lockstep with the Python one. Source code is open under the licenses above.