Open source, pre-alpha, for journal-driven services
Kavach records every input, clock read and random read your service makes. A crash becomes a fixture that replays byte for byte, and a fix only counts when it survives every variation of the incident.
{"id":"evt-008","type":"deposit","account":"bob","amount":null}seq 32 crash: amount is nullRecord
Your handler reads time and randomness, and emits effects, through the env it's given. An in-process flight recorder keeps those records in memory and flushes a fixture when a step crashes, returns an error or breaks an invariant.kavach inspect null-amount.kavachenv.now() clock env.read(8) rand … env.emit("ledger.entries", entry) outputEvery input, clock read, random read and output becomes a record. The recorder keeps a window in memory and writes it to disk only when a step fails.
Replay
Recorded inputs are folded through your own handler code, with every clock and random read served from the fixture. Outputs are captured and compared, never executed, so replay needs no broker, database or network.kavach replay null-amount.kavach --bin ./ledger-old$ kavach replay null-amount.kavach --bin ./ledger-old service ledger · start genesis · 33 records · 8 steps replayed recorded crash at seq 31: amount is null result still_failing@31Verify
Replay the same journal under the old build and the new one. Kavach reports the first output where they diverge, then runs variants of the incident before it calls anything fixed.kavach diff null-amount.kavach --old ./ledger-old --new ./ledger-newledger-oldstill_failing@31ledger-newfixedfirst divergence at step seq 31: output 0 of step 31 differs old (none) new ledger.rejections {"event":"evt-008","reason":"missing amount"}Then 44 variants of the incident. The new build must pass every one the old build fails.
41 of 41 passfixed
env.now() clock
env.read(8) rand
…
env.emit("ledger.entries", entry) outputEvery input, clock read, random read and output becomes a record. The recorder keeps a window in memory and writes it to disk only when a step fails.
Guarding only the event type from the incident makes the recorded crash go away. It doesn't fix the bug. kavach diff perturbs the incident: fields changed or removed, the failing event moved earlier, earlier events dropped or redelivered, the clock shifted. Switch between two real fixes for the ledger demo and watch it run.
37of 41 reproducing variants pass
Variant 6
failing input (seq 31): "type" "deposit" → "transfer"The old build fails as in production. This fix still fails: still_failing@22.
- passes
- still fails
- doesn't reproduce, ignored
- not run yet
$ kavach diff null-amount.kavach --old ./ledger-old --new ./ledger-partial
old still_failing@31
new fixed
first divergence at step seq 31: output 0 of step 31 differs
old (none)
new ledger.rejections {"event":"evt-008","reason":"missing amount"}
variants 41 of 44 reproduce the incident on the old build · 37 of those pass on the new build
✗ 6 failing input (seq 31): "type" "deposit" → "transfer"
still_failing@22: crash: amount is null
✗ 29 failing input (seq 31): "type" "deposit" → "withdraw"
still_failing@22: crash: amount is null
✗ 37 failing input (seq 31): "type" "deposit" → "deposit-1"
still_failing@22: crash: amount is null
✗ 41 failing input (seq 31): "type" removed
still_failing@22: crash: amount is null
verdict variant_failed(6)@22Verdict for null check on deposits only: variant_failed(6)@22. 37 of 41 reproducing variants pass.
Coding agents are good at writing fixes and bad at knowing when to stop. kavach mcp hands them three tools that return the CLI's verdicts as JSON, so a fix isn't done until Kavach says fixed.
kavach_list_incidents- Every fixture, with how it failed
kavach_replay- Reproduce it before changing code
kavach_diff- Old build against new, plus variants
claude mcp add kavach -- kavach mcpAgents follow AGENTS.md: reproduce first, change code and never the fixture, and report the verdict exactly as printed.
- > Fix the crash in fixtures/null-amount.kavach
- kavach_replay (bin: /tmp/old)verdict still_failing@31 crash: amount is null
- Edit the ledger handler+ reject deposits whose amount is null
- kavach_diff (old: /tmp/old, new: /tmp/new)verdict variant_failed(6)@22variant 6: "type" "deposit" → "transfer"
- The fix only covered deposits. A transfer with a null amount still crashes, so I'll guard every event type instead.
- Edit the ledger handler- reject deposits whose amount is null+ reject any event whose amount is null
- kavach_diff (old: /tmp/old, new: /tmp/new)verdict fixed 41 of 41 reproducing variants pass
Measured 6 October 2026 on a shared 4-vCPU cloud VM. The command behind each number, and what it includes, are in BENCHMARKS.md, so you can rerun any of them.
| Recorder overhead per event | Median of 5 runs | |
|---|---|---|
| Replay of the demo crash, in process | Median of 5 runs | |
| Fix verification, 87 replays in process | 2 replays and 85 variants, median of 5 runs | |
| Size of the demo crash fixture | The fixture file on disk | |
| Replays that came out byte-identical | The same fixture, replayed 1,000 times | |
| Narrow fixes rejected, ten planted bugs | Crashes, returned errors and broken invariants |
Kavach is pre-alpha. It covers single-writer, journal-driven services; multi-writer state and network tapes are out of scope for now. Apache 2.0.