Changelog
Notable releases of the Provenrail SDK, the open-source verifier, and the platform. The Python package is on PyPI and the recording SDK is on npm.
Last updated
rm -rf: it is git reset --hard (#34327, #7232, #17190), git checkout -- . (#81508), git clean -fd (#45974, #87360), git stash (#85879), git worktree remove --force (#46444), a framework database reset against a live database (#80868, #90808). The default install allowed all forty commands we took from those reports. Three new packs, armed by default, cover them, and every one is in the test suite as the claim it is.git reset --hard on a tree with nothing uncommitted and nothing unpushed destroys nothing at all. The git rules run git status --porcelain, check for commits your remote has not seen, and ask only when there is work here that exists nowhere else. A prompt about nothing is how people learn to click through prompts.rm is now screened by where it points, not by its flags. Measured over the corpus below, the two rm rules produced 687 of 923 interruptions and almost none were dangerous: rm -rf .next out before a build, rm -f coverage.xml, rm -rf app/en during a refactor. A delete inside your repository, under a temp directory, in a package cache or in a build directory now runs without a word. A home directory, a whole disk, a two-segment system path, an unset variable followed by a slash, or --no-preserve-root is refused. A delete that merely leaves the project asks.docs/security.md explaining what rm -rf / does, a migration containing DROP TABLE legacy, or a test fixture holding a fake token was denied on the Write and Edit tools. An agent that cannot write down the commands the guard blocks is an agent working around the guard. The secrets pack still screens file writes, because putting a real credential in a file is the harm it exists to catch.wrangler deploy --dry-run deploys nothing and wrangler d1 execute --local runs against a file in .wrangler/. Both used to interrupt.chmod +x was denied because a UUID in the path contained the digits 4732; the mode is now read as the mode. class="min-w-0 truncate text-sm" was denied as a SQL TRUNCATE. A newline inside --command "DELETE FROM ..." split the flag that made the call harmless away from the statement it qualified, so a local test database was refused five separate times in the corpus. .env.example and the other template spellings no longer ask.pr whenever it finds one, and pr guard hook read its policy from .provenrail.json. With no such file it armed nothing and allowed everything, while the zero-install engine in the same plugin armed the default packs. So the documented upgrade path, uv tool install provenrail, turned the guard off, with nothing said. Both engines now answer that question the same way: no config file means the defaults, and a config file, once present, still wins completely including an explicit empty use.pr from 0.2.30 answered the hook for a 0.4 plugin, so none of the git, database or cloud rules existed as far as that machine was concerned, and nothing anywhere said why. The shim now compares versions and lets whichever engine knows more rules answer. Eight tests drive the shim itself, including that a crashing CLI produces exit 0 and no output, which is the one failure mode a hook is allowed to have.Bash|Edit|Write|MultiEdit|NotebookEdit|WebFetch|Task. That looked careful and was a hole: an MCP server's deleteVolume, which is the exact shape of the public incident this site cites, was never handed to the hook, and neither was a Read of ~/.ssh/id_ed25519. It is now wired to every tool, and scope is declared per rule with a new not_tool field, which is visible in /guard-rules instead of buried in a matcher nobody reads.~/.ssh/id_*, ~/.aws/credentials, ~/.kube/config, .netrc, .npmrc, a keychain file; public keys excluded). An MCP tool whose method name deletes, drops, destroys, terminates or purges. And a write to .provenrail.json or .claude/settings.json: the FAQ has always said that editing the policy is itself a tool call the guardrail sees, and it saw it and did nothing, which is a different sentence.tool and not_tool accept |-separated alternatives. fnmatch has none, so a rule covering several tools had to be written several times or not at all. The MCP rule was written once with pipes in it, matched nothing, and reported itself armed, which is the failure this catalogue exists to avoid.tools/measure_guard.py runs the same measurement over your own transcripts and makes no network call./guard-card. A paste-ready summary of what the guard has stopped here, safe to post because of what it leaves out: every command is reduced to its verb and flags, and every operand is dropped, so git reset --hard origin/main shows as git reset --hard and an environment assignment carrying an API key shows as export. The directory is a truncated hash rather than a name. Checked against all 36,977 corpus commands: no path, hostname, URL or key survives the reduction.rm -rf / matched nothing and the call was allowed. A valid shell script, and a complete defeat of every rule the product advertises, in both engines identically. Matching now covers the whole argument, and an argument genuinely too large to screen asks a human rather than passing, because one the engine could not read is not one known to be safe.blast-radius.tool-call-cap matches every tool, and a limit rule under its cap returned an allow immediately, so {"use": ["blast-radius", "destructive"]}, the obvious thing to write when you want both, let rm -rf / through for the first 500 calls of every session while reporting itself armed. An allow found in the ruleset is provisional now, and is only returned once nothing later denies.pr attest-verify now checks the head the document names. It was the one field nothing re-derived, and it is the field the printed headline uses, so a signer could attest to a range ending several commits back while the document claimed to cover the tip.pr attest --blame works on repositories with binary files. git blame echoes the file it is blaming, so a tracked image made the command fail with an error about a bundle it had never been given.Co-authored-by trailer with it, and the commit was reported as human-authored with nothing anywhere saying so. Records are NUL-separated now, which git forbids inside commit content, and the field split is bounded so nothing can be discarded. The parse also cross-checks its own commit count against git and refuses rather than signing a document covering fewer commits than the range it names.recorded.pofky/[email protected] writes the attestation on every push, so it is signed at the moment the commits landed rather than months later by someone with a reason to shade it. It refuses a shallow checkout instead of attesting to a stump, and removes the signing key from the workspace before the job ends./plugin install provenrail-guard@provenrail used to print homework: install the package, run quickstart, then arm the rules. Anyone who stopped after the first line had a plugin called "guard" that blocked nothing. The plugin now carries its own dependency-free engine, so 26 rules are armed on the next tool call with no package, no account and no config file. Installing the CLI later signs the same history in place rather than starting an empty one./guard-status and /guard-rules. The status report leads with what has actually been stopped, not with what is configured, because a guardrail that has never fired and one that is silently broken look identical from the outside.pr attest: which of this code an AI wrote, in a form your customer can check. Software contracts have started asking. Every other way of answering, a Co-authored-by: trailer or a git note, is written by the party being asked and can be edited afterwards. pr attest names the source of every finding and signs the result, which binds it to commit ids that are themselves content hashes, so the document cannot be pointed at a different tree or rewritten once a dispute begins. pr attest-anchor adds an independent RFC 3161 timestamp over a 32-byte root; no line of your code is ever sent. What it proves, and what it does not.pr attest-verify re-derives rather than re-reads. The receiving side runs it against their own clone, and it checks three things: that the document is unchanged since signing, that every attested commit exists there with the same author and dates, and that running the detectors again 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.pr anchor-push bundle.json, and hold a receipt with a public authority's timestamp on it, over your own run. One per account, and the tooling stays free whether or not you ever buy a plan.pr anchor-push stopped asking for what it already knows. It defaults to the hosted service, to the licence key pr activate stored, and to writing anchor-receipt.json, so the whole command is the bundle you want anchored. Run it with no bundle, a missing file, or no key, and it prints the commands that fix it instead of an argument error.pr verify now names the one thing a green result cannot prove. A run anchored only by you is intact and unaltered, and says nothing about when it happened, because the same side wrote both the records and the time. The verifier says so, at the moment you are holding a proof you built entirely yourself, rather than leaving you to read it on a pricing page later.confidence=0.87 raised "floats are not allowed in records", leaving you to find which field it meant. The error names the value, the field path, and what to pass instead.pr anchor-push --url https://provenrail.com works with the licence key you already have, and returns a receipt carrying an RFC 3161 timestamp from a public authority, so the date is not ours to assert either. What travels is a stream label, a fingerprint of your records and a count; you keep every record. There is no field in the request a record could arrive in, which is why this is the only part of Provenrail we host. The receipt has a URL an auditor opens with no account and no permission from you.gen_time written beside the token rather than the one inside it. That field is not covered by the authority's signature, so a receipt could show one date while its token said another and every cryptographic check would still pass. The reported time now comes out of the token, and a receipt that misstates its own evidence fails.pr anchor-verify no longer reports success as a warning. Findings were sorted into fail-or-warning with no third case, so the strongest result the command can produce, a timestamp whose signature and certificate chain both validated, printed under [warn]. To an auditor that reads as a problem with the evidence.pr anchor-push sends the Merkle root of a local bundle and how far it reaches, and nothing else: the endpoint that receives it has no field a record could arrive in. pr anchor-verify then proves a receipt covers a bundle offline, without calling the service that issued the receipt, because a check that asks the issuer whether its own receipt is good proves nothing. Coverage of a stream can only grow, so a shorter or forked history is refused rather than signed.record_model_call now fails immediately, and a capture that fails at runtime warns instead of passing in silence.pr verify --tlog-pubkey none used to print TAMPERING DETECTED and exit 1, so a shell typo raised a security alarm and gated a build. --tsa-root and --max-cosig-age also work on pr verify now, not just on the pr-verify script.pr quickstart. A failed start left its pid file behind, and the next attempt refused to run naming a process that no longer existed.pr quickstart now leads with a zero-code proof. It used to hand you a Python snippet to paste into a file you had to create, so the first thing a new user did was author code, before anything had shown the tool works. It now offers pr demo and pr verify bundle.json first: a real signed run and a verdict on it, in two commands, before you write a line.pip while every other page teaches uv.GET /v1/streams/{id}/receipts. Chain links only, never record bodies, so a write token can check its own continuity without becoming a way to read what other agents wrote.python -m provenrail, for when pr is not our pr. pr is also the POSIX paginator from coreutils, and on Windows under Git Bash that one wins on PATH. There was previously no way around it. Every subcommand now works through the module entry point, which nothing can shadow.invalid token (401), an existing stream came back as unknown stream (404), and the record-count check crashed (500). A client is entitled to read a 401 as "my credentials were revoked", stop, and drop the run, so this could lose records for no reason other than a second agent writing at the same instant. Every statement is now serialised and its rows read before the lock is released. Measured on a 400-request burst from 16 threads: 58 failures before, 0 after, across 1,200 requests.pr command on each.pr spend, budget caps in .provenrail.json, and pr reconcile against a provider invoice were built but appeared nowhere on the site. Same for pr verify-content and pr disclose.pr report --out now writes the file for every format. It was honoured only with --html. With --md or the default JSON the flag was silently ignored and the report went to standard output, so an evidence file you believed you had saved did not exist and nothing said so.pr diff exits 1 when the runs differ, following the same convention as diff and git diff --exit-code. It used to exit 0 either way, so the one question it exists to answer, did the rerun do the same thing, could not be gated on without parsing its output.pr pack lists what is actually in the archive. The printed contents were a hardcoded list that had drifted: it named an attestation file the pack does not contain and left out four that it does, including the rendered report an auditor opens first./v1/meta carried the literal 0.2.0 long after the package moved on, so every deployed sink misreported itself in the one field you read during an incident.pr reconcile documents what its exit code means: 1 when the invoice bills for something that never went through the recorder, and unchanged by a price variance on a model you did record, because our figure there is an estimate from list prices.pr guard receipt took its exit code from pr risk, which exits 1 when it finds blocked actions. Finding them is the reason you run it, so a successful export reported itself as a failure, with the same code as an export that produced nothing at all. It now exits 0 when the receipt was written. pr risk keeps the gating exit code for CI, and both are documented in --help.--use DESTRUCTIVE stopped setup with an unknown-pack error. Pack names are now matched case-insensitively.--use that names no pack now refuses, instead of arming something you did not choose. pr guard hook --use " " parsed to an empty list and fell back to whatever the configuration file said, under a flag that reads as if it selected those packs. Both pr guard hook and pr guard install now refuse and say so, and a refused install writes nothing to disk.pr verify exits 2 when it could not read the file at all and 1 when it reached a verdict that is not a pass. A CI job that treats every non-zero result alike turns a mistyped path into a tampering alert. Both meanings are now in --help and the README.pr verify at a PDF, an archive, or a half-finished download raised a raw decoder error out of the file read. It now gets the same plain sentence broken JSON gets. Both cases also print a RESULT: line, so a script reading standard output for a verdict is never met with silence.pr guard hook --use works. The flag was accepted and then ignored: with no configuration file present the hook allowed everything while its own help text said the packs were armed. It now arms exactly the packs named, and a mistyped pack name refuses to enforce and says so instead of silently arming nothing.pr --version. The first thing anyone needs for a bug report.stream_id, a seq that is not a number) made the CLI report TAMPERING DETECTED while the browser threw and showed a neutral "could not verify". Same file, opposite readings, which is the exact failure a second implementation exists to prevent. Both now decide the shape up front by identical rules, and a test asserts they agree on the verdict and the full finding set across 26 malformed shapes.server_head is checked. The bundle carries the hash of the chain's last record and neither verifier ever looked at it, so an export could claim any head at all, including one from a different stream, and still verify.PUT /v1/account/plan took the plan from the request body behind an owner-only permission, so a free account could name itself enterprise and receive unlimited events, seats, SSO and exports. An upgrade now requires the payment provider's secret; a downgrade stays self-service.rm and -rf separated by a tab matched nothing. Argv arrays had the same problem through the decide() API. Both are normalized before matching now.rm -f one-file.txt is no longer refused outright. It was denied by a pack that is on by default, and a guardrail that blocks ordinary work gets switched off within a day, taking the working rules with it. Recursion is what cannot be undone, so recursion is what is refused; a forced non-recursive delete asks for a recorded human decision instead.o1 and o3-mini were billed at half the real figure, and Gemini 2.5 Pro kept its base cache rate above the 200k tier where the vendor doubles it, so spend caps under-counted exactly the long-context calls where caching is worth using.kubectl get pods --namespace prod needed a human approval because the deploy rule matched any command carrying the namespace rather than the verbs that change something, and a rule that stops people looking at production is a rule they turn off. SELECT * FROM "users", the spelling Postgres and most ORM codegen emit, matched nothing at all. pr pack wrote an evidence pack for a bundle that fails verification and exited 0, so only the recipient learned the truth.chmod 777 lived in a pack the default install never armed; TRUNCATE orders slipped through because the rule required the optional TABLE keyword; dd of=/dev/ and kubectl delete namespace had no rule at all; and the token pattern matched only the retired sk- key format, missing every current sk-proj- and sk-ant- key.access pack is armed by default, so world-writable chmod and disabling MFA are blocked out of the box. Existing projects keep the packs written in their .provenrail.json; an upgrade never starts blocking on its own.deploy*, which Claude Code never emits, so it was inert for the host we ship for. A connection string pointed at production and a deploy command now ask for a human, rather than being silently allowed.pr guard install writes the policy at the repo root, but agents are routinely launched from a package inside a monorepo. The lookup only checked the current directory, so a session started in apps/web found no policy and allowed everything, silently. It now searches upward to the repo root. This was the worst failure available: you installed it, so you believed you were covered..provenrail.json, so a session started in a subdirectory no longer opens a second, invisible journal with its blast-radius counters reset to zero.limit rule counting in memory reset on every call and capped nothing, while pr guard status still reported it as armed. A cap that never fires is worse than no cap. Counts are now carried across hook processes, per session, and pruned after seven days.deny and require_oversight never read it, which means editing or deleting it cannot unblock anything. pr guard reset clears it./plugin marketplace add pofky/provenrail then /plugin install provenrail-guard@provenrail. No settings file to edit. The hook shim fails open: if Provenrail is not installed it exits silently rather than blocking every tool call, and warns once a day instead. A guardrail that bricks your agent gets uninstalled before it guards anything.pr guard install arms the destructive, secrets and production rule packs and wires them into Claude Code's own tool hooks. No SDK, no code change. 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.pr guard status shows what is armed and what it blocked; pr guard receipt exports the proof so you can pr verify it yourself. pr risk now counts escalations separately from denials.Honest scope: this covers tool calls the host routes through its hooks. It cannot constrain a process that never calls them.
.provenrail.json. Nothing is on by default: an upgrade must never start blocking your agent on its own.pr rules lists the catalogue with each rule's false-positive risk; pr rules --check bundle.json reports which rules would match the tool names in a real recorded run, and says plainly that content rules cannot be judged from names alone..provenrail.key keeps runs on a pinned stream signing under one identity.ComplianceCallbackHandler that signs and hash-chains every model and tool call off-box.verify.js is an independent verifier (WebCrypto Ed25519, JCS canonicalization, RFC 6962 Merkle proofs) that runs client-side, so the hosted verifier trusts not even our server.pr pack builds a self-contained auditor ZIP; pr diff compares two integrity-checked runs so a difference is a real behavioural change.pr export pulls a run you recorded out of the local sink into a bundle, so you can close the full loop on your own machine: record, pr export, pr verify, with no account.pr quickstart now saves a read token in .provenrail.json, which is what makes pr export work with zero extra setup.pr activate verifies a signed license key fully offline and runs the self-hosted deployment at the licensed tier. Plan limits and feature gates are enforced server-side; the integrity guarantee never depends on any of it.uv tool install provenrail, which brings its own pinned Python.pr verify and an in-browser verifier that trust nobody, backed by a frozen public conformance suite both must agree on.Dates reflect public release. For the precise commit history, the SDKs, verifier, and spec are open source under MIT.
← Back to home