Quickstart
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
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
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:
rm -rf, terraform destroy, git push --force,
DROP TABLE, chmod 777 and leaked API keys are blocked
before they run, and the agent is told which rule fired..env, a deploy, a migration) becomes a
permission prompt instead of a hard block, and your approval is recorded as human oversight.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.
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.
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
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.
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
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.
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.
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).
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.
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.
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"] }
}
| Pack | Covers |
|---|---|
destructive | delete/drop/truncate tools, rm -rf, destructive SQL, unbounded DELETE |
secrets | AWS keys, private key blocks, API tokens, JWTs, .env reads |
money | transfers, payments, refunds, charges (human approval, not a hard block) |
production | deploys, migrations, DNS, force push, terraform destroy |
access | permission grants, IAM changes, chmod 777, MFA disabling |
exfiltration | unbounded SELECT *, outbound uploads, paste sites |
blast-radius | per-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
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
}
}
| Effect | What it does |
|---|---|
deny | Blocks the action and records the denial. |
require_oversight | Allows it only if a human-oversight event was recorded in this session first; otherwise blocks. |
limit | Allows 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.
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.
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.
pr sidecar in front of the model API and lock egress
to it.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.
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.
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
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.
pr spend --json reports
unpriced_calls so a total is never silently short.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
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
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
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.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.
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.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.
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.
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.
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.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.
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.
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"
}
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..."
}
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.Provenrail is open-core and dual-licensed:
pr verify verifier, the
in-browser verifier, and the spec. Use them anywhere, including commercially.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.
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.