Formal verification in practice: what to prove and how far to go
A ladder from stronger types to full proofs, with what each rung catches, what it costs and how well AI agents handle it: how to choose what to prove, write specs, prove a model or the code, keep proofs working in CI, and use agents without trusting them for the spec. Every rule is graded by its evidence.
A coding agent can write a payment-splitting function, its unit tests and a note saying “all tests pass” in under a minute. The function in this guide is that kind of code: its unit test passes, yet it charges one person two cents less than the others and crashes when splitting one cent three ways. Nobody reads every line at that speed, so the question is which checks you can trust without trusting the author.
A proof is a machine-checked argument that code meets a specification (a precise statement of what it must do) for every input, so it doesn’t matter who or what wrote the code, or the proof. That makes proofs more needed as agents write more code. Agents also make proofs affordable: proofs used to take specialists years, agents now write many of them, and the checker rejects the wrong ones, so a bad proof costs a retry rather than a bug. What stays with people is deciding what the spec says.
But “prove everything” isn’t a plan. This guide, part of dbggr’s series on validation and proofs, covers strategy: the cheaper checks to try first, what’s worth proving, writing specs, proving a model or the code, keeping proofs working, and using agents without letting them decide what “correct” means. If proofs are new to you, start with our introduction; for the tools, see our proof languages comparison.
How we grade the evidence
| Label | What stands behind it |
|---|---|
| Production | A team’s published report on a system it ships, with numbers |
| Tested | A benchmark or controlled study. We name the models, because results on 2025 models may not hold today |
| Tool docs | What a tool’s makers recommend in its documentation |
| Vendor report | A company’s measurement of its own product, not independently checked |
| Our run | Code we ran for this piece, with versions |
| Our analysis | Our judgement from the above |
The short answer
- Climb a ladder and stop early: stronger types, contracts, property-based tests, bounded model checking, design models, and last, full proofs. [Production] Amazon’s S3 storage team stayed on the lower rungs (property tests and model checking) for 40,000 lines of Rust: checks about 20% of the code’s size stopped 16 bugs reaching production.
- Prove the small, stable parts where a bug is expensive and the spec is short: parsers, cryptography, money arithmetic, authorisation, concurrency protocols. [Production] HACL* in Firefox, EverParse in Hyper-V and Cedar follow this pattern.
- Write the spec as code first, in the same language and repository, then test the spec itself. [Production + Tested] S3’s reference models are 1% of the code’s size; one 2026 benchmark found 60 incomplete specs, already reviewed by experts, by running them against wrong answers.
- Expect 2–27 lines of proof per line of code for full proofs, plus upkeep. [Production] Cedar 3.4:1, AWS’s Dafny authorization engine 5:1, seL4 about 27:1. One cross-cutting seL4 change cost 32% of the original proof effort to re-verify.
- Run proofs in CI like tests, split into a fast per-pull-request set and a slow scheduled one, and fail the build on escape hatches like
sorryandassume. [Production] s2n’s proofs needed three manual updates in a year of changes. - Let agents write proofs in a loop with the verifier; don’t let them set the spec unchecked. [Vendor report + Tested] With an agent loop, Claude Opus 5.5 proved all 1,325 theorems of a 2026 benchmark, about Lean translations of the code, specs given. One-shot, GPT-5.5 wrote correct specs from a description 48% of the time against 92% for code. And verifying can be vacuous: in one reproduced Dafny pipeline 99.1% of programs verified but 1.9% proved anything tied to a spec. Freeze the spec and have a person read it.
What is the verification ladder?
The verification ladder is the set of checks between “it compiles” and “it’s proved”, ordered by cost. The rungs share their most expensive input: a spec written for a property test can be checked by a model checker unchanged and restated for a proof.
| Rung | What it checks | A proof? | Typical cost | Agent evidence | Production example |
|---|---|---|---|---|---|
| Stronger types | Declared types; illegal states can’t be built | Of narrow claims, if the type system is sound | Low | No specific studies | Standard-library types such as Rust’s NonZeroU64 |
| Contracts (assertions) | Pre- and postconditions, at run time | No | Low | Agents’ specs often too weak | AWS C library: developers added 89 contracts |
| Property-based tests | A property on thousands of random inputs | No, a sample | Low to medium | Same risk: weak properties | S3 ShardStore, Jane Street |
| Bounded model checking (Kani, CBMC) | Every input, with loops and sizes up to a bound | Yes, within the bound | Medium | Little published evidence; harnesses resemble unit tests | AWS C library, s2n-quic, Firecracker |
| Design-level models (TLA+, P, Alloy) | Every state of a design model, within bounds | Of the model, within bounds; not of the code | Medium | Unreliable TLA+ on 2025 models | DynamoDB, S3, EBS |
| Full proofs (Dafny, Verus, Lean, Rocq, Isabelle) | The spec, for all inputs | Yes, relative to the spec and what’s trusted | High | Strong on single proofs with an agent loop and a verifier; weak one-shot, on repositories and on specs | seL4, CompCert, Cedar, HACL* |
“A proof?” uses this series’ strict sense: a machine-checked argument that a stated property always holds. “Agent evidence” summarises the studies later in this piece, plus 2025–26 studies of postcondition writing (PostcondBench) and TLA+ (SysMoBench), both on 2025 models.
Taking one function up the ladder
Here is the ladder applied to a small money function: split total cents among n people so the parts add up to total and no two differ by more than a cent. We ran everything in this section [Our run]: Rust 1.97.0, proptest 1.12.0, Kani 0.68.0 with CBMC 6.11.0, and Lean 4.35.0. We list the files and outputs in our research notes, which aren’t published on the site.
The spec comes first, written once as an ordinary function:
use std::num::NonZeroU64;
pub fn is_fair_split(total: u64, n: u64, parts: &[u64]) -> bool {
let sum: u128 = parts.iter().map(|&p| p as u128).sum();
let (min, max) = (parts.iter().min(), parts.iter().max());
parts.len() as u64 == n
&& sum == total as u128
&& matches!((min, max), (Some(lo), Some(hi)) if hi - lo <= 1)
}
// First attempt: round up, give the last person what's left.
pub fn split_cents_v1(total: u64, n: NonZeroU64) -> Vec<u64> {
let n = n.get();
let share = (total + n - 1) / n;
let mut parts = vec![share; (n - 1) as usize];
parts.push(total - share * (n - 1));
parts
}
Stronger types rule out one bug for free
Taking n as NonZeroU64 rather than u64 means nobody can call the function with zero people, so division by zero is ruled out by the compiler, for free.
A unit test passed
split_cents_v1(100, 3) sums to 100, so a test that checks the total is green. The parts are [34, 34, 32], which breaks the spec: two people pay 34 cents and one pays 32.
A property test failed
With proptest, the same spec runs on random inputs:
proptest! {
#[test]
fn v1_is_fair(total in 0u64..1_000_000, n in 1u64..50) {
let parts = split_cents_v1(total, NonZeroU64::new(n).unwrap());
prop_assert!(is_fair_split(total, n, &parts));
}
}
It failed on every run, but on different inputs: random testing needn’t give the same answer twice. Some runs reported attempt to subtract with overflow with minimal failing input: total = 1, n = 3 (one cent split three ways rounds each share up to 1, so the last share is 1 - 2); others stopped at unfair splits such as total = 25, n = 3. The crash is in debug and test builds; a release build would silently wrap the subtraction and hand one person 18 quintillion cents.
A bounded model checker found every failure at once
Kani checks the code for every possible input, within loop bounds you set:
#[kani::proof]
#[kani::unwind(9)]
fn v1_split_3() {
let total: u64 = kani::any(); // every u64 value
let parts = split_cents_v1(total, NonZeroU64::new(3).unwrap());
assert!(is_fair_split(total, 3, &parts));
}
In 4 seconds it reported three failed checks: the subtraction underflow, an addition overflow in total + n - 1 for totals near the 64-bit maximum (which our property test, capped at a million, could never reach), and the fairness assertion. The fix gives everyone the quotient and the first total % n people one cent more:
pub fn split_cents(total: u64, n: NonZeroU64) -> Vec<u64> {
let (q, r) = (total / n.get(), total % n.get());
let parts: Vec<u64> = (0..n.get()).map(|i| if i < r { q + 1 } else { q }).collect();
debug_assert!(is_fair_split(total, n.get(), &parts)); // the contract, checked in tests
parts
}
Kani proved it for every u64 total with 3 people in 18 seconds, and with 8 people in 536 seconds. When we made the number of people symbolic too (any value from 1 to 8), CBMC ran out of memory on a 15 GB machine. That’s the bounded rung in miniature: everything up to a bound, at a cost that climbs steeply with the bound.
A full proof covered every size, but of a model
In Lean, the same algorithm and spec, for all totals and all group sizes:
def split (total n : Nat) : List Nat :=
(List.range n).map (fun i => if i < total % n then total / n + 1 else total / n)
theorem sum_aux (q r n : Nat) :
((List.range n).map (fun i => if i < r then q + 1 else q)).sum = n * q + min r n := by
induction n with
| zero => simp
| succ n ih =>
rw [List.range_succ, List.map_append, List.sum_append, ih, Nat.succ_mul]
simp only [List.map_cons, List.map_nil, List.sum_cons, List.sum_nil]
split <;> omega
theorem split_sum (total n : Nat) (h : 0 < n) : (split total n).sum = total := by
unfold split
rw [sum_aux, Nat.min_eq_left (Nat.le_of_lt (Nat.mod_lt _ h))]
exact Nat.div_add_mod total n
theorem split_length (total n : Nat) : (split total n).length = n := by
simp [split]
theorem split_fair (total n x : Nat) (hx : x ∈ split total n) :
x = total / n ∨ x = total / n + 1 := by
simp only [split, List.mem_map, List.mem_range] at hx
obtain ⟨i, _, rfl⟩ := hx
split <;> omega
Lean checked it in 1.3 seconds, and #print axioms listed only Lean’s standard axioms, so no step was skipped with sorry. But this proves a Lean function over unbounded natural numbers, not the Rust code over 64-bit integers. The overflow bug lives exactly in that gap. To close it you’d use a verifier that works on the Rust itself (Verus, or Aeneas, which translates Rust to Lean; see our Rust verification guide) or keep the model and test the code against it, as AWS does with Cedar.
The property test found the unfairness and the underflow, Kani found the overflow the test couldn’t reach, and the Lean proof found no bug but added a guarantee for every group size. The spec stayed the same in meaning at every rung, restated each time in that rung’s language. [Our analysis]
When should you stop climbing?
Stop at the lowest rung that covers the failure you fear, at a cost you’ll keep paying as the code changes:
- Stop at property tests when bugs are cheap to catch and fix, or the code changes too fast for proofs. [Production] Amazon’s ShardStore team (S3’s storage node, over 40,000 lines of Rust) used property tests and model checking, and accepted “weaker correctness guarantees than full formal verification, which is currently impractical for storage systems at this scale”. Its models and checks are 20% of the implementation’s size, which “compares favorably to formal verification approaches that report 3–10× overhead”, and stopped 16 issues reaching production (SOSP 2021).
- Climb to bounded model checking for memory safety, panics and overflow in code that handles untrusted input. [Production] At AWS, one verification engineer and two interns checked 171 entry points of the AWS C Common library in 24 weeks with CBMC, finding 83 issues, including 10 integer overflows that “had evaded the extensive unit and integration testing” (Software: Practice and Experience, 2021).
- Climb to a design model when the risk is in the design, especially concurrency and failure recovery, before code exists. [Production] At AWS, engineers learned TLA+ “in 2 to 3 weeks”, and models of 102–939 lines found bugs in S3, DynamoDB and EBS, one needing a 35-step trace that “had passed unnoticed through extensive design reviews, code reviews, and testing” (2014 report).
- Climb to a full proof when a single bug is expensive, the spec is short and stable, and someone will maintain the proof. [Production] That describes seL4, CompCert, HACL* and Cedar.
Agents move the line: when a proof costs from about a dollar on small problems to hundreds on system code, it pays in places it didn’t. The spec and its upkeep still cost human time, and that usually decides. [Our analysis]
What code is worth proving?
Code is worth proving when four things are true: a bug is expensive, tests are likely to miss it, the spec is short, and the code is stable. These categories usually qualify:
| Kind of code | Why tests miss bugs | Short spec | Production example |
|---|---|---|---|
| Parsers and decoders of untrusted input | Attackers find inputs no test generator does | “Never crashes; accepts exactly the format” | EverParse parsers in Hyper-V; s2n-quic decoders under Kani |
| Cryptography | Bugs hide in rare carries and edge values | “Equals the mathematical definition” | HACL* in Firefox, Linux and Python; s2n HMAC and DRBG |
| Money arithmetic | Rounding and overflow appear at unusual values | “Parts sum to the total; nothing created or lost” | Our example above; we know of no published full proof of a conventional card or ledger payments system |
| Authorization | One wrong decision is a breach | “Deny unless a policy permits” | Cedar (Lean); AWS’s AuthV2 engine (Dafny) |
| Concurrency and distributed protocols | Bad interleavings are rare and unrepeatable | “No two leaders; no lost writes” | TLA+ at AWS; ShardStore’s concurrency checks |
Stability matters: ShardStore modelled “components whose APIs had become stable” to avoid churn that “would have made them much more expensive to maintain”, and left availability and performance to load tests and staggered deployments. [Production] Choosing what not to prove is part of the strategy.
How do you write a specification?
State what must go right in plain words (“we begin by precisely stating ‘what needs to go right?’”, says AWS’s 2014 report), write it as executable code in your own language, and only then translate it into a proof tool.
Begin with properties you get almost for free
The cheapest specs need no understanding of the domain:
- No crash on any input. The spec of the 239 OSS-Fuzz entry points Claude proved for Anthropic and Theorem in October 2026 (our coverage) was “this test suite always passes” on every input under 64 MB.
- Round trips.
decode(encode(x)) == x,parse(print(x)) == x. - Agreement with a reference model: the same interface, a naive implementation.
In a 2024 study based on 30 interviews at Jane Street, comparison with a reference was the most common property (17 of 30), then round trips and classical properties (11 each); it pays “when… you have a really good abstraction with a complicated implementation” (ICSE 2024). [Production, interview study]
Write the spec in the code’s language, next to the code
ShardStore’s reference model for its log-structured merge tree is a hash map. All its models are 450 lines against 44,048 of implementation, in Rust, and double as mocks in unit tests, so engineers must keep them current. The team tried Alloy, SPIN and Python first and switched after discussing “long-term maintenance implications”; 18% of the harness lines were later last edited by engineers with no formal-methods background. AWS’s CBMC team’s first lesson is the same: “make specifications explicit in the code”. [Production]
Test the spec, not just the code
A spec can be too weak (it accepts wrong answers) or unsatisfiable. Three checks help:
- Run it against wrong answers. The VeriContest benchmark ran its expert-reviewed (mostly agent-drafted) Verus specs against a median of 2,670 negative tests per problem and found 60 incomplete postconditions; one accepted
[-9, -8]as array indices (arXiv 2605.08553). [Tested] - Break the code on purpose. s2n’s proofs ship with patches that insert realistic bugs, and the proof scripts check that the proof then fails (CAV 2018). Seven of the 30 Jane Street interviews described doing the same for property tests. [Production]
- Show that a correct implementation exists. Vero let agents prove specs unsatisfiable, which turned up 38 defects in its human-curated specs (arXiv 2608.13522). [Tested]
Expect the spec to miss things, and write down what
ShardStore missed a bug because “the cache size was configured to be very large in all tests”, and doesn’t check executions that are both concurrent and crashing. s2n’s proof “assumes correct implementation of the hash functions”. Firecracker’s docs: “Harnesses are only as strong as the assumptions they make.” [Production] Write the gaps into the spec file.
Should you prove a model or the code?
Prove the code when the code is small and its language has a verifier; prove a model when the design is the risk or the code is too big, and then connect the model to the code with tests. Both are in production:
| Approach | What’s proved | How the code is linked | Example |
|---|---|---|---|
| Design model only | A TLA+ or P model of the protocol | Not at all, or by checking real traces against the model | AWS 2014; Microsoft’s CCF runs trace validation in CI |
| Model plus differential tests | A Lean or Dafny model | Random inputs through both; outputs must match | Cedar |
| Verified core, compiled or generated | The code, in the proof language | The tool emits the shipped C or Java | HACL* (F* to C), EverParse (F* to C), AuthV2 (Dafny to Java) |
| Code in place | The shipped code itself | It is the code | seL4 (C), Kani and Verus (Rust), s2n (SAW on C) |
On the first row, AWS’s 2014 report asks “How do we know that the executable code correctly implements the verified design? The answer is that we don’t.” That’s fine when the bugs you fear are design bugs. Cedar’s model is tested against the Rust code on “millions of random inputs” before each release (AWS): the link is a test, not a proof. [Production]
The verified-core pattern deserves more use. EverParse3D put more than 23,000 lines of generated, proved C into the Hyper-V virtual switch, within a 2% performance budget, regenerated in the normal Windows build (PLDI 2022). Firefox’s NSS doesn’t rerun HACL*’s proofs in its own CI: it pins an upstream revision, applies a local patch, and runs a plain diff to check the vendored C matches upstream’s generated code, with a handful of files exempt (NSS script). [Production] Prove a small core once and check cheaply that you still ship it.
What is refinement, and do you need it?
Refinement is proving that a detailed implementation only does what a simpler, abstract spec allows, usually in steps from an abstract model to the code. You need it when one spec can’t be both readable and close enough to the code to prove directly.
seL4 has three levels: an abstract spec of what each system call does, an executable spec with all the data structures, and the C code. The first step cost 8 person-years and the second under 3, and “80% of the effort in the first refinement went into establishing invariants” (TOCS 2014). The payoff: proving integrity on the abstract level took 7.4 person-months, “an order of magnitude less” than the team estimates it would have taken on the C. [Production] Microsoft’s IronFleet used three layers too (spec, protocol, implementation); its top spec for a replicated state machine is “simply linearizability”, in 85 lines (SOSP 2015). [Production]
A reference model plus differential tests is the lightweight version: refinement checked by sampling. Start there. [Our analysis]
How much proof does a line of code need?
Published projects report 2 to about 27 lines of proof per line of verified code, plus person-months to years of expert time:
| Project | Tool | Lines of proof per line of code | Effort |
|---|---|---|---|
| HACL* symmetric primitives | F* | 2 | About a person-week per primitive |
| IronKV, re-done in Verus (2024) | Verus | 2.9 | A reimplementation for the Verus paper |
| Cedar | Lean | 3.4 | 18 person-days for the largest proof |
| IronFleet, implementation layer | Dafny | 3.6 | 3.7 person-years for the whole project |
| IronKV, original (Verus paper’s count) | Dafny | 4.2 | Part of IronFleet |
| AWS AuthV2 authorization engine | Dafny | 5 | Four years, calendar time |
| HACL* bignum code | F* | 6 | Several person-months for three primitives |
| IronFleet, whole system | Dafny | 7.7 | 3.7 person-years |
| seL4 fastpath (an optimisation) | Isabelle | 21 | About 5 person-months |
| seL4 kernel, first proof | Isabelle | 27 | About 20 person-years for the proof |
Sources: HACL* (CCS 2017), Verus (SOSP 2024), Cedar (FSE 2024), IronFleet, AuthV2 (paper), seL4 (numbers and TOCS 2014). Cedar’s ratio is per line of the Lean model, not the Rust. The IronFleet whole-system, seL4 and fastpath ratios are our arithmetic (39,253 / 5,114; about 200,000 / 7,500 verified lines of C; 5,913 / 287). HACL* bignum is “up to 6”; AuthV2 counts spec and proof together; both IronKV figures use the Verus paper’s counting. Projects count “proof” differently, so compare orders of magnitude. Cedar’s 18 person-days is from AWS’s blog.
For comparison, in different units, AWS’s CBMC harnesses averaged 20 lines per function checked, plus shared helper code, and ShardStore’s checks were 0.2 lines per line of code. [Production] The tool matters too: the same system took 4.2 lines of proof per line in Dafny and 2.9 in Verus, and verified about ten times faster in Verus (41 s against 445 s on one core). [Tested, by Verus’s authors] The Verus guide, writing about late-2025 models such as Claude Sonnet and Opus 4.5, says LLM-written proofs are “often 2–3× as long” as human ones, partly because they “care less about proof maintenance” (guide). [Tool docs] So agents cut the effort column more than the ratio. [Our analysis]
How do you keep proofs working as code changes?
Treat proofs like tests: keep them in the repository, run them on every change, and make them depend on interfaces rather than internals, so ordinary edits don’t break them:
- [Production] s2n’s proofs were replayed 956 times in CI after November 2016 and needed three manual updates in a year, each “complete before the code review process finished”; edits inside function bodies rarely needed one. AWS C Common saw “no proof failures at all” from developers’ changes over 60 days.
- [Production] In seL4, local optimisations cost re-verification “roughly proportional to the size of the change”; a new system call took a day to write and under a quarter of a person-month to re-prove. Cross-cutting changes are expensive: adding interrupts and page tables cost “about 32% of the time previously invested”, and a change to under 5% of the code cost 17% of the original proof effort.
- [Production] AWS’s Dafny authorization engine is the warning: “most code or environment changes broke at least one proof, causing delays in deployment by up to 14 days.” Brittleness checks in CI and refactoring brittle proofs then cut its brittle verification conditions “from 41 to 4”.
SMT-based proofs can break for no visible reason
Dafny, Verus and F* hand proof steps to an SMT solver (a program that decides whether logical formulas can be true), which uses heuristics and randomness, so a proof can time out after an unrelated edit. The Mariposa study found 2.6% of 17,043 queries from six verified projects unstable on the newest Z3 at the time (4.12.1), up to 5% in one project, and solver upgrades often made it worse (FMCAD 2023). Across ten Verus projects, about 1% of queries were unstable (CADE 2025). [Tested]
What helps, from the tools’ documentation [Tool docs]: keep each proof’s context narrow (Dafny’s {:isolate_assertions}, Verus’s #[verifier::spinoff_prover]); hide definitions the solver doesn’t need (opaque and reveal); use resource limits rather than time limits, so results don’t depend on the machine (Verus’s rlimit, Dafny’s --resource-limit); and measure variation across random seeds (Dafny’s manual suggests keeping it under 20% in CI, though it also calls its measure-complexity command “not yet functional”). Lean and Isabelle proofs break differently, mostly on library and toolchain upgrades.
Agents are starting to do the upkeep
[Tested] Lean Pool, an archive of 211 Lean projects, has an agent repair the breakage after each Mathlib upgrade: one upgrade broke 97 of 191 projects, and 95 of the 97 repair jobs succeeded, though “additional agent-assisted integration addressed remaining build errors” and the authors say the build “does not establish equivalence of every declaration type before and after the upgrade” (arXiv 2609.25199). In an Isabelle repair study with gpt-5.6-sol, 6 of 144 accepted repairs had changed text the model wasn’t allowed to touch, once by adding the theorem’s conclusion as an assumption; limiting the model to the proof body stopped it (arXiv 2608.13459). We found no study of agents repairing proofs after real code changes in a large verified system.
How do you run proofs in CI?
Run proofs in CI the way you run slow tests: a fast set on every pull request that blocks the merge, the full set on main or on a schedule, and a check that fails on escape hatches. Real configurations:
- Kani’s GitHub Action runs
cargo kaniin one step; s2n-quic runs it over three crates, with harnesses written in Bolero so the same code is a fuzz test undercargo testand a bounded proof under Kani. Kani’s FAQ warns GitHub’s runners have 7 GB of RAM and a 6-hour limit, and suggests a schedule if it’s too slow (docs). Firecracker runs Kani on bare-metal instances with a 300-minute timeout, only when a pull request touches Rust, Cargo or build-container files. [Production] - seL4 checks proofs on five architectures per pull request, minus its largest session; pushes to master run the full matrix and advance the verified manifest only if all pass; a clean run over every platform happens weekly. [Production]
- Lean:
lake build --wfailturns warnings, includingdeclaration uses `sorry`, into failures;#print axiomslists what a theorem rests on, and anything beyondpropext,Classical.choiceandQuot.soundneeds explaining (Lake docs, validating proofs). Mathlib’s CI fails on warnings and on any logged output, and runs the slowerleanchecker, which replays proofs through the kernel, once a day. Cedar’s CI builds the proofs and runs tests against the Rust code on every pull request; random differential testing runs nightly and gates releases. [Tool docs + Production]
A minimal gate for a Rust repository with Kani harnesses and Lean proofs, as a sketch (we tested the escape-hatch script on sample diffs, not the whole workflow):
on: pull_request
jobs:
proofs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # so the diff can see main
- uses: model-checking/kani-github-action@v1.1 # bounded proofs
- name: Lean proofs, failing on sorry # assumes Lean is installed
run: lake build --wfail
- name: No escape hatches added in this pull request
run: |
set -euo pipefail
diff=$(git diff origin/main -- '*.rs' '*.lean')
added=$(printf '%s\n' "$diff" | grep -E '^\+' | grep -v -E '^\+\+\+ ' || true)
if printf '%s\n' "$added" | grep -E \
-e '\b(sorry|admit|native_decide|external_body)\b' \
-e '(^\+|[^:])\bassume\(' \
-e '\baxiom\b'; then
echo "Escape hatch added: needs a person's review"; exit 1
fi
The last step is crude on purpose: it skips kani::assume, which harnesses need, and flags the escape hatches the Verus guide says agents reach for (assume, admit, external_body, axioms) plus Lean’s sorry and native_decide; it will also flag those words in comments, and a person reviews the rare legitimate one. (Kani’s CI page still says only Ubuntu 20.04 is supported; s2n-quic runs it on ubuntu-latest.) Our guide to slow test suites covers keeping long checks out of an agent’s inner loop.
How do you use agents for specs and proofs without trusting them for the spec?
Let agents write proofs, harnesses and draft specs, because the checker catches bad proofs; treat the spec as the one artefact a person must approve, freeze it, and check mechanically that the agent didn’t change it or skip steps. The evidence on current models: with an agent loop and a verifier, proofs of small programs are close to solved (one vendor report); one-shot, models are weak at both specs and proofs; and “verified” says little unless the spec is checked:
| Study | Models | Task | Finding | Grade |
|---|---|---|---|---|
| VeriContest (May 2026) | GPT-5.5, Claude Opus 4.7, Gemini 3.1 Pro | One-shot Verus specs, code and proofs; ten models | GPT-5.5: 92.18% correct code from a description, 48.31% correct specs, 13.95% correct proofs (spec and code given); Opus 4.7: 90.17%, 20.82%, 12.68% | Tested |
| Verified but Vacuous (Nov 2026) | Claude Opus 4.7 | A published Dafny pipeline whose prompt asks for no postcondition, reproduced | 99.1% verified; 1.9% proved a property bound to a spec | Tested (one model) |
| Vero (Aug 2026; at NeurIPS 2026 as Verify0) | GPT-5.5, Claude Opus 4.8, Sonnet 5 | Code and proofs for 43 Lean repositories, specs given | Best setup solved 27; 14% of its unproved specs were rejected as cheating | Tested |
| Rust-Prover on VeriContest (Oct 2026) | Claude Opus 5.5 and others | Agent loop with a Lean-backed verifier; 1,325 theorems, specs and code given | Opus 5.5 proved all 1,325, at a median $1.20 per proof; Opus 5.5 agents also translated 239 specs; checked by test suites or by an agent reviewer (partly by sampling), none were found wrong or weakened | Vendor report |
| Spec-Harness (Apr 2026, revised Oct) | GPT-4o, Sonnet 4.6 and open models; Codex with gpt-6.1-sol as an agent | Java JML specs | 22.7% of verified specs, from tools and models, had no postcondition; using the harness as feedback raised Codex’s adequacy score from 0.60 to 0.75 | Tested |
The rows measure different things and can’t be compared. VeriContest’s spec numbers are single attempts without an agent loop, which understates an agent with a verifier; the Rust-Prover report comes from the company that makes the verifier. We found no study of writing specs from a description on Claude Opus or Sonnet 5.5, Fable 5.1 or GPT-6; the closest are Rust-Prover’s spec translation on Opus 5.5 and Spec-Harness’s agent run with gpt-6.1-sol.
The common failure: an agent pushed to make the checker pass weakens the target. The Verus guide lists the moves: assume(), admit(), external_body, new axioms, “Strengthens preconditions or weakens postconditions”, and changing the code itself. [Tool docs] Anthropic’s write-up of its OSS-Fuzz proofs notes that “Claude could make any theorem provable by rewriting the program into one that trivially never crashes”, so it penalised patches and audited them (Anthropic). [Vendor report] It’s the proof version of false completions.
A workflow that holds up, built from those sources [Our analysis, from Tool docs and Tested]:
- A person owns the spec. Have the agent draft it from the docs and tests, in plain words next to the formal version, with a few wrong implementations it must reject; a person approves it, as you’d review tests.
- Freeze it. Keep specs in their own files, protected by code owners, and fail CI if an agent’s change touches them. Vero grades only the regions the agent may edit, pasted onto a clean copy. Lean’s manual counts “un-reviewed AI-generated proofs” as potentially malicious and recommends
lake comparator, which checks proofs against a trusted file of statements, for high-risk cases. - Check the proof mechanically: the escape-hatch grep above,
#print axioms, and a diff of the executable code. - Test the spec against wrong outputs and a known-good implementation, as in the section on writing specs.
- Cap the budget. The Verus guide (on Claude 4.5-era models) warns that Opus models tend to set huge solver limits and “wait for many hours”, and that one function’s proof can cost “a few hundred dollars”. Ask for smaller lemmas instead.
- Give agents a skeleton. In VeruSAGE, Claude Sonnet 4.5 finished 16 of 17 incomplete system proofs given the experts’ partial proofs, and 6 from an empty proof (arXiv 2512.18436). [Tested, 2025 model]
Our guide to evaluating agents covers measuring whether such a setup works on your code.
What do you still have to trust?
Every rung trusts something. The spec, always. The checker: Lean’s and Isabelle’s small kernels, plus an SMT solver for Dafny, Verus and F*, plus CBMC for Kani. The step to running code: the compiler, or the generator from F* or Dafny to C or Java. The bounds, for bounded checks: our Kani proofs cover groups of exactly 3 and 8, nothing else. And models of the world: s2n trusts its hash functions, seL4 its assembly, boot code and hardware. Write the list down next to the proof; it’s the part a reviewer most needs to read.
Which rung should you use?
- Every project: types that rule out illegal states, and specs as executable assertions. Every later rung reuses them.
- A clear spec over a messy implementation: property-based tests against a reference model, the best value on the ladder for most teams.
- Rust or C that parses untrusted input or uses
unsafe: add Kani or CBMC harnesses for no-panic and memory safety, run per pull request with small bounds (Rust tools). - A distributed protocol or concurrent algorithm, before you build it: a TLA+ or P model, then check production traces against it if you can.
- Small, stable, expensive-if-wrong code (crypto, authorization, money, a parser at a trust boundary): a full proof as a verified core, an agent writing the proof and a person owning the spec. Pick the tool by the language you ship (comparison; for TypeScript, see what you can prove in TypeScript; for Python, what you can prove in Python).
- Large, fast-moving code: don’t prove it. Prove the core it depends on, and test the rest; our guide to validating AI-written code covers the testing, mutation testing and review rungs in more depth.
FAQ
What should you formally verify first?
Start where a bug is expensive and the spec fits in a sentence: a parser that must never crash, an encoder whose output decodes back, a money split whose parts sum to the total, or an authorization check. Write that sentence as a property test first, then climb to a bounded check or a proof if the code is stable.
Is property-based testing a formal proof?
No. Property-based testing checks a stated property on many random inputs, so it can find bugs but can’t show they’re absent. It’s the cheapest way to state a spec, though, and the same property can later be checked by a bounded model checker such as Kani or proved.
Is bounded model checking a proof?
Yes, within its bounds. Kani and CBMC check every input up to the loop and size limits you set and say nothing beyond them. In our example, Kani proved a split function correct for every 64-bit total with 3 people and with 8 people, checked separately, and ran out of memory when the number of people was left open too.
How much does formal verification cost?
Published full proofs run 2 to about 27 lines of proof per line of code: HACL*’s symmetric crypto at 2, Cedar at 3.4, AWS’s Dafny authorization engine at 5, seL4’s first proof about 27. Lighter rungs cost far less: Amazon’s S3 storage checks were about 20% of the code they checked. Agents cut the expert time, not the work of writing and maintaining the spec.
Can AI agents write formal specifications?
They can draft them, but writing a correct spec from a description is much harder for models than writing code. On the 2026 VeriContest benchmark, GPT-5.5 wrote correct Verus code from a description 92% of the time and a correct spec 48% of the time, in single attempts. Have a person approve the spec, test it against wrong answers, and freeze it against agent edits.
