Consider a fictional staging configuration service. Claude Code changes a routing rule through an MCP tool. The service accepts the write and returns revision 41.
An engineer spots a separate problem and repairs the same rule. The current revision becomes 42.
Then the agent’s validation fails. Its recovery instruction says to restore the snapshot from before its change.
A blind restore removes the engineer’s repair too. The rollback reports success, but the system has lost a fix that somebody deliberately made after the agent acted.
I would rather see a recovery conflict than a reassuring “rollback complete” message in this situation. The conflict tells the operator that ownership of the state has changed. Automatic recovery should stop there.
The old value does not belong to the current state
A rollback plan usually records what to restore. That is necessary, but it leaves out an awkward question: is the agent still allowed to replace what is there now?
For a single configuration resource, the recovery record needs the revision produced by the forward operation, as well as the earlier value:
# Illustrative service-owned record, not a native Claude Code setting.
operation: staging-route-change-17
resource: staging/routing/payments
before_state_ref: restricted://snapshots/route-before-17
written_revision: "41"
recovery:
action: restore_recorded_before_state
condition: current_revision_equals_written_revision
on_conflict: stop_and_escalate
The resource identifier must include the environment and any tenant boundary. “Restore the payments route” is too vague if the same tool can reach staging and production.
Keep the before-state snapshot in restricted storage. The review packet can carry its reference without carrying credentials or sensitive configuration. A caller should not be able to replace that reference with arbitrary values and call the result a rollback.
Compare and restore in one service operation
A separate read does not solve this problem:
read current revision -> observe 41
another writer commits -> revision becomes 42
restore old snapshot -> erase the other writer's change
The service that owns the resource must compare the expected revision and perform the restore atomically. An API might expose this through a conditional request, a database transaction, or a compare-and-swap operation. The syntax matters less than the guarantee: no writer can slip between the successful comparison and the mutation.
Use a revision token that changes on every committed write and is not reused. Comparing only a value hash can miss a change-away-and-back sequence. The value may look identical while another operator has taken responsibility for it.
If the service offers no reliable conditional mutation, disable automatic snapshot restoration for shared resources. A lock held only by the agent does not help when people and other services can write without taking it.
This is a resource-service control. Putting expected_revision: 41 in a prompt or MCP argument has no protective effect unless the receiving service enforces it.
Run the failure before shipping the recovery tool
The accompanying conditional rollback fixture is a runnable Python example with six regression tests. It uses only the standard library and makes no network calls:
python3 conditional-rollback.py
The fixture keeps a service-owned forward record and checks it during recovery. Under one lock, it validates that record, looks for an existing receipt, compares revisions, and either restores or records a conflict.
Its tests cover these cases:
| Case | Required result |
|---|---|
| Nobody edits after the agent | Restore the recorded value |
| A human repairs the resource | Reject recovery and preserve the repair |
| The value changes away and back | Reject recovery because the revision changed |
| Recovery authority is invalid | Refuse the mutation |
| The same recovery request arrives again | Return the original receipt without another write |
| A caller substitutes a different before-state | Reject the forged recovery record |
The duplicate-request case deserves attention. Suppose recovery succeeded, its response disappeared, and someone subsequently edited the resource. Repeating the same request must not restore the old snapshot again. A durable operation receipt should answer what the earlier request did. It is not a statement that the resource still has that value now.
The fixture demonstrates the admission logic, not a production MCP server. It stores records in memory and accepts an authorised boolean from its test caller. A deployed service must verify the caller’s identity, target scope, and authority expiry itself. Store the receipt and mutation atomically in durable storage, and ensure every relevant writer participates in the revision scheme.
The local tests do not prove distributed concurrency behaviour, restart recovery, or network-timeout handling. Before production, add an integration test that places a barrier between admission and mutation, races two writers against the real service, and checks its committed revision history. Test restart and duplicate delivery against the durable receipt store too.
Recovery authority needs a narrower job description
A failed forward operation does not justify handing the agent broad production credentials.
Before it starts, decide which inverse effect the recovery path may attempt, which resource it may touch, and how long that permission lasts. Bind it to the original operation. Recovery should not become a second route for general configuration editing.
Forward approval and recovery approval can have different lifetimes. That is a design decision to make before the failure, not permission for the agent to renew its own access. If valid recovery authority is unavailable, freeze further agent writes and hand the case to the incident owner.
Some actions cannot be undone by restoring a value. A sent message, an external payment, or a downstream consumer acting on a changed route may need a compensating action. Treat that as a separate effect with separate approval. The original snapshot does not make the wider system forget what happened.
A conflict is not an invitation to fetch a newer revision
The easiest way to defeat conditional recovery is to handle a conflict by reading revision 42 and retrying the old snapshot against it.
That makes the conditional write pass while discarding the reason for the condition. The revision must remain bound to the state produced by the original operation.
After a conflict, preserve the evidence and stop automatic mutation. An operator can inspect the intervening edit and propose a targeted repair. That repair is a new decision, not another attempt at the original rollback.
For a multi-resource workflow, record each outcome separately. “Two restored, one conflicted” is a useful status. “Rolled back” is misleading if one resource still needs attention. A timeout is also not a conflict or a failure: it is an unknown outcome until the authoritative operation receipt resolves it.
Put the conflict where the reviewer can see it
A recovery review packet should connect the original operation to its recorded before-state, the revision it wrote, the recovery authority, and the service’s outcome receipt. Include the current revision observed during the atomic conflict check, not an unrelated later read presented as if it caused the rejection.
Give an unresolved conflict an owner. Otherwise it can sit in an apparently finished agent run while operators assume that validation failure triggered a successful restore.
The same distinction matters for incident replay. Replaying the decision should use the recorded evidence without executing the restore again. The run’s flight recorder should show what the service accepted, refused, or left unresolved.
Start with one shared configuration resource and make the intervening-writer test fail safely. That is a better recovery milestone than watching the agent restore a snapshot in a sandbox where nobody else can touch it.
For the wider operating model around permissions, review, and recovery, see the Claude Code book and Kindle edition. The fixture here is a companion teaching example, not a claimed excerpt from the book.