Backstep sits between an AI agent and the tools it calls. Before the agent changes any data, it records what that data looked like, so any mistake can be reviewed and rolled back with one command.
Demo: an accounts table holds alice 100 dollars, bob 250, carol 75 and dave 500. An AI agent runs an UPDATE with no WHERE clause and every balance becomes 0. Then backstep undo-session runs and every balance is restored to its exact original value.
agent session · postgresBefore
AI agent
You
Bank accounts
Customer
Balance
alice
$100
bob
$250
carol
$75
dave
$500
Total
$925
1 Before
4 accounts, $925 in total
alice
$100
bob
$250
carol
$75
dave
$500
2 Agent's mistake
UPDATE accounts SET balance = 0; (no WHERE)
alice
$0
bob
$0
carol
$0
dave
$0
3 After undo
backstep undo-session
alice
$100
bob
$250
carol
$75
dave
$500
One command. Exact original state.
One command. Exact original state.
Demo
See it in one minute.
From a wiped table to an exact restore, plus a refusal and the benchmark results.
The problem
Agents can change real data now. They still make mistakes.
Agents have write access.
They run queries against real databases, file stores and cloud accounts.
They still get it wrong.
An UPDATE with the wrong WHERE clause. An insert loop that runs 200 times.
Two bad options.
Keep the agent read-only, or give it write access and hope for the best.
The third option
Let the agent act, keep an exact record of every change, and undo any of it with one command.
How it works
Five steps, on every tool call.
1
Intercept
Every tool call the agent makes passes through Backstep first.
Technical detail
A transparent MCP proxy aggregates tools from every configured upstream server and routes each call through an interceptor. It never alters or swallows the upstream response.
2
Classify
Each tool has a risk grade, from read-only to irreversible.
Technical detail
Looks up the tool's reversibility class (R0 to R4) and decides to pass it through, journal it, ask for confirmation or block it. No model is in the loop at runtime.
3
Capture
Before anything changes, it saves a copy of the rows about to change.
Technical detail
The Postgres adapter parses the statement with sqlglot, rewrites the INSERT, UPDATE or DELETE into a SELECT over the same rows, and runs it on its own connection.
4
Journal
It writes down what happened and what the data looked like before.
Technical detail
An append-only SQLite journal stores the call's arguments, the state before the change and a fingerprint of the state right after it.
5
Revert
One command puts it back. If someone else has changed the data since, it stops.
Technical detail
Compares the live rows with the stored fingerprint. No drift: restore the original rows. Any drift: refuse and show the difference.
Not every action can be undone. Backstep knows which.
Every tool gets a reversibility grade. It decides what is recorded, what needs your confirmation and what is refused.
SafeIrreversible
R0
Read-only
Passed through
R1
Fully reversible
Captured and journaled
R2
Reversible with loss
Captured, optional confirm
R3
Compensable only
Journaled, no snapshot
R4
Irreversible
Blocked unless allowed
Recovery benchmark
Measured, not just tested.
backstep eval runs six "agent gone wrong" scenarios against a real Postgres database and grades each recovery.
3full recoveries
1partial, reported as one
2correct refusals. Refusing is the pass.
Recovery benchmark: six scenarios, what the agent did, the result and the time to recover
Scenario
What the agent did
Result
Recovery
wrong_where_update
An UPDATE that hit far more rows than intended
Full recovery
28 ms
runaway_insert_loop
200 INSERTs from a stuck loop
Full recovery
2.7 s
mixed_session
3 updates, 1 delete, 1 insert, undone together
Full recovery
87 ms
cascading_delete
A DELETE whose child rows cascade
Partial recovery
37 ms
partial_drift
Another process edited the row afterwards
Correctly refused
3 ms
unparseable_sql
A CTE-heavy statement it cannot safely read
Correctly refused
17 ms
Why a refusal is a pass
In partial_drift, reverting would have destroyed someone else's edit. In unparseable_sql, Backstep could not safely read the statement, so it declined to guess.
The honest partial
In cascading_delete, Postgres removes child rows inside the same statement, before Backstep can see them. The parents come back; the children do not.
10 possible revert outcomes. Only 1 of them changes data.
Missing pre-state, no primary key, drift or an action already undone: any doubt ends in a refusal with a clear reason.
reverted
refused_not_committed
refused_already_reverted
refused_irreversible
refused_no_inverse
refused_no_pre_state
refused_no_adapter
refused_no_primary_key
refused_drift
failed
Real Postgres tests
110+
tests against a disposable Postgres container, with a fresh schema for each test.
Fully typed with mypy
Linted with ruff
Mutation guard tests
Each safety check is proven to matter.
drift check on refused
drift check off overwrites edit
Lossless serialization
Restored rows match byte for byte.
DecimalUUIDtimestampbytesrange
Append-only journal
History is never rewritten.
Once an action is recorded, only its status can change.
Getting started
Four commands.
Install with uv, then point your MCP client at the proxy instead of the upstream server.
terminal
$ uv sync # install dependencies$ uv run backstep init # optional: Claude grades each tool$ uv run backstep doctor # check config, journal, database$ uv run backstep serve # run the proxy
First copy backstep.example.toml to backstep.toml and add your upstream server and Postgres connection string. init needs ANTHROPIC_API_KEY.