An undo button for AI agents.

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.

  • Any MCP client
  • No agent changes
  • Python 3.11+
  • MIT
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.
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.

  1. R0

    Read-only

    Passed through
  2. R1

    Fully reversible

    Captured and journaled
  3. R2

    Reversible with loss

    Captured, optional confirm
  4. R3

    Compensable only

    Journaled, no snapshot
  5. 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
ScenarioWhat the agent didResultRecovery
wrong_where_updateAn UPDATE that hit far more rows than intendedFull recovery28 ms
runaway_insert_loop200 INSERTs from a stuck loopFull recovery2.7 s
mixed_session3 updates, 1 delete, 1 insert, undone togetherFull recovery87 ms
cascading_deleteA DELETE whose child rows cascadePartial recovery37 ms
partial_driftAnother process edited the row afterwardsCorrectly refused3 ms
unparseable_sqlA CTE-heavy statement it cannot safely readCorrectly refused17 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.

Raw results in eval_results.json
Engineering approach

A safety tool has to be trustworthy itself.

Refuse instead of guess

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.