●FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtask●LIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its own●MCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS●PLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callback●SONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October●FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtask●LIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its own●MCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS●PLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callback●SONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
Guard Your Agent's Destructive Operations with Pre- and Post-condition Contracts
A design for wrapping an autonomous agent's writes in deterministic pre- and post-condition checks. A contract gate stops the destructive operations that better prompts can never reliably prevent.
The scariest thing about running an autonomous agent in production is not when it crashes. A crash, at least, is honest. The truly dangerous failure is when a language model like Claude fails plausibly: it produces wrong content, confidently, in a perfectly well-formed shape. The schema validates. The types line up. The logs say success. And yet what got written is broken. That is the failure mode that keeps me up at night.
I have been running several apps and sites solo since 2014, and for the past couple of years I have handed parts of my publishing and deployment to autonomous agents. My first naive setup let the agent push to git directly, and once or twice a week something broken would reach production: a link pointing at a page that did not exist, a mismatch between the number of Japanese and English articles, a config file whose internal consistency had quietly drifted. Every one of these is the kind of failure you think you can fix by writing a more careful prompt. But no matter how much I polished the instructions, the leak rate never reached zero.
Here is the conclusion up front: what worked was not better prompting. It was physically wrapping the agent's write operations in deterministic gates. This article generalizes that design as a "contract gate" and walks all the way to an implementation you can bolt onto Claude Agent SDK tool calls.
Why "Ask the AI to Be Careful" Designs Break Down
Most agent implementations delegate safety to the prompt: "Do not write broken data," "Verify consistency before you push." It seems to work, but it is fundamentally a probabilistic defense. The model decides slightly differently every time. Even at 99% reliability, if you run it dozens of times a day, that remaining 1% will surface, reliably.
This is where an old idea from software engineering helps: Design by Contract. You give a function a precondition (the states in which it may be called) and a postcondition (what it guarantees afterward), and if either is violated you refuse to run. An autonomous agent's tool call is exactly the place that needs such a contract. Whatever the model is "thinking," the machine checks the invariants before and after the write, and a state that violates an invariant never reaches production. You move the locus of judgment from the model to deterministic code.
Official docs often present permission hooks like canUseTool as a way to insert human approval. But in real operation, the most effective thing to interpose is not a human and not the model — it is a deterministic check with neither in the loop. Human approval does not work in a nightly batch, and the model's self-check is, as noted, probabilistic.
The Shape of a Contract Gate: tryGuard, apply, commitGuard, rollback
A contract gate wraps a single destructive operation in four phases.
tryGuard (precondition) — Check whether the state permits the operation. If violated, refuse without executing.
snapshot — Record the pre-operation state for reversal.
apply — Perform the actual write.
commitGuard (postcondition) — Check whether the post-write state satisfies the invariants. If violated, roll back to the snapshot.
The key is that the postcondition phase follows an "apply, then check, then revert if bad" flow. Preconditions alone cannot verify the result of applying. For example, the invariant "the Japanese and English article counts are equal" can only be confirmed after the files are written. That is precisely why apply, check, and reversal must be treated as one unit.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦A complete, dependency-free TypeScript implementation of a contract gate that wraps tool calls in tryGuard, apply, commitGuard, and rollback
✦Clear criteria for what belongs in pre- and post-conditions: only invariants that resolve deterministically against external state
✦Concrete patterns for safe rollback via snapshots and idempotent reversal, plus a real breakdown of what the gate actually caught in production
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
The code below is a minimal implementation that wraps any side-effecting function in a contract gate. It layers onto Claude Agent SDK tool definitions, has no dependencies, and runs as-is.
// contract-gate.tsexport type GuardResult = { ok: true } | { ok: false; reason: string };export interface Contract<Ctx> { name: string; // Invariants checked before applying. May return several. pre: (ctx: Ctx) => Promise<GuardResult[]> | GuardResult[]; // Capture the state needed to reverse. snapshot: (ctx: Ctx) => Promise<unknown> | unknown; // The actual destructive operation. apply: (ctx: Ctx) => Promise<void> | void; // Invariants checked after applying. post: (ctx: Ctx) => Promise<GuardResult[]> | GuardResult[]; // Revert to the snapshot when a postcondition fails. Must be idempotent. rollback: (ctx: Ctx, snapshot: unknown) => Promise<void> | void;}export class ContractViolation extends Error { constructor( public phase: "pre" | "post", public contract: string, public reasons: string[], ) { super(`[${contract}] ${phase}-condition violated: ${reasons.join("; ")}`); this.name = "ContractViolation"; }}export async function runWithContract<Ctx>( contract: Contract<Ctx>, ctx: Ctx,): Promise<void> { // 1. Preconditions const preResults = await contract.pre(ctx); const preFailed = preResults.filter((r) => !r.ok) as { reason: string }[]; if (preFailed.length > 0) { throw new ContractViolation("pre", contract.name, preFailed.map((r) => r.reason)); } // 2. Snapshot const snap = await contract.snapshot(ctx); // 3. Apply await contract.apply(ctx); // 4. Postconditions -> roll back on violation const postResults = await contract.post(ctx); const postFailed = postResults.filter((r) => !r.ok) as { reason: string }[]; if (postFailed.length > 0) { await contract.rollback(ctx, snap); throw new ContractViolation("post", contract.name, postFailed.map((r) => r.reason)); }}
When you wrap a Claude Agent SDK tool in this gate, then even if the agent decides to "push," a violated contract throws ContractViolation and the tool returns an error. From the agent's point of view, the tool answers: "I can't do that, and here is why." The model reads the reason and tries something else — but even if the model goes off the rails, a state that breaks an invariant never survives in production.
// Wrapping a publish tool in a contract gateimport { runWithContract, type Contract } from "./contract-gate";import { execSync } from "node:child_process";import { readdirSync } from "node:fs";interface PublishCtx { repoPath: string; jaFile: string; enFile: string;}const publishContract: Contract<PublishCtx> = { name: "publish-article", pre: (ctx) => { const results = []; // Precondition 1: both JA and EN files are present results.push( ctx.jaFile && ctx.enFile ? { ok: true as const } : { ok: false as const, reason: "Japanese or English version is missing" }, ); // Precondition 2: the working tree is clean of unexpected changes const dirty = execSync(`git -C ${ctx.repoPath} status --porcelain`).toString().trim(); const unexpected = dirty.split("\n").filter((l) => l && !l.includes("content/articles/")); results.push( unexpected.length === 0 ? { ok: true as const } : { ok: false as const, reason: `Unexpected changes: ${unexpected.join(", ")}` }, ); return results; }, snapshot: (ctx) => execSync(`git -C ${ctx.repoPath} rev-parse HEAD`).toString().trim(), apply: (ctx) => { execSync(`git -C ${ctx.repoPath} add content/`); execSync(`git -C ${ctx.repoPath} commit -m "Add: article (JA+EN)"`); }, post: (ctx) => { const ja = readdirSync(`${ctx.repoPath}/content/articles/ja`, { recursive: true }) .filter((f) => String(f).endsWith(".mdx")).length; const en = readdirSync(`${ctx.repoPath}/content/articles/en`, { recursive: true }) .filter((f) => String(f).endsWith(".mdx")).length; // Postcondition: after commit, JA and EN counts match return [ ja === en ? { ok: true as const } : { ok: false as const, reason: `JA/EN count mismatch ja=${ja} en=${en}` }, ]; }, rollback: (ctx, snap) => { // Undo the commit back to the original HEAD (idempotent) execSync(`git -C ${ctx.repoPath} reset --hard ${snap}`); },};// Call this when the tool runsawait runWithContract(publishContract, { repoPath, jaFile, enFile });
What to Put in Pre- and Post-conditions
This is the part that takes the most design judgment. The only things you may put in a contract are invariants whose truth resolves deterministically by querying external state. Subjective questions like "is this article valuable to the reader?" cannot go in a contract. That is the model's job, not the gate's.
The invariants I actually place are things like: the Japanese and English article counts are equal; every internal link points at a file that exists; the config file has no contradiction between its redirects and its real entries. Each of these resolves to a single truth value by asking the filesystem or git. Naturalness or originality of prose, on the other hand, I leave to a different layer — the model itself, or a separate judging model.
The guideline is simple: put in a contract only what a human reviewer could call pass/fail by eye, beyond dispute, in under five seconds. The moment you add something open to interpretation, false positives stall the agent and you start wanting to remove the gate. A gate only survives if it is precise enough that you never want to take it out.
Here is a point the official material rarely stresses: postconditions should check the integrity of the whole repository, not just the range you wrote. An agent's writes frequently break assumptions in places it never touched. You meant to add one article, and you quietly broke sitemap consistency. Widening postconditions to the whole repo catches this kind of "ripple damage." The check costs more, but that is negligible against the cost of a broken production.
Making Rollback Safe: Snapshots and Idempotent Reversal
The weak point of a contract gate is rollback. If that code is buggy, you detect the anomaly in the postcondition but fail to revert, leaving a half-finished state behind. This is the worst pattern: the safety device itself causes the accident.
Two principles protect the implementation. First, rollback must be idempotent — calling it any number of times with the same snapshot yields the same final state. The example uses git reset --hard <sha>, which gives the same result however many times it runs. Second, the snapshot must be captured before apply, always. It sounds obvious, but I have seen implementations that try to take the snapshot only after an exception fires mid-apply. By then it is too late.
When you mutate the filesystem directly, you may not have a reversal mechanism like git. There, a "staging" approach works well: perform apply in a temporary directory, and only after the postcondition passes, swap it into the production directory with an atomic rename. On most filesystems rename is atomic, so if the postcondition fails you just discard the temp directory and production stays untouched.
Numbers and Pitfalls from Real Operation
With the naive setup, as noted, something broken reached production once or twice a week. In roughly three months after introducing the contract gate, the gate caught a total of 47 cases across pre- and post-conditions, with zero destructive states slipping through. The breakdown: 30 precondition refusals, 17 postcondition rollbacks.
What is interesting is that fewer than half of those 47 catches were the model clearly "getting it wrong." The rest were external causes: a previous job that ended half-finished, a remote that had moved ahead, a filesystem in a transient inconsistent state. In other words, the contract gate caught not only model errors but also the race conditions that come with any distributed system — a side benefit I had not designed for.
A pitfall worth recording: the biggest trap is an implementation that only logs the postcondition failure instead of feeding it back into the agent's loop. The agent then has no idea what happened and retries the same operation endlessly. Always return ContractViolation's reason as the tool's result so the model can correct based on the cause. Design the gate as a conversational partner, not a wall.
Retrofitting onto an Existing Agent
If you are adding a contract gate to an agent already in operation, you do not need to wrap every tool at once. The safe order is gradual. First, pick only the single most destructive tool — usually push, deploy, or a database write — and add just a postcondition. Postconditions, more than preconditions, give you "awareness" without disturbing existing behavior. Next, watch the postcondition logs for a week or two to observe how things actually break. Promote the patterns you see into preconditions, and the number of rollbacks themselves drops. Finally, add the heavier machinery — staging swaps, whole-repo checks — only where it is needed.
This order matters because a contract cannot be written perfectly on paper. Only by running it and watching how the agent errs do you learn which invariants belong there.
Both of my grandfathers were temple carpenters. I was told that good joinery is not forced together with nails; it is cut in advance so that, when load comes, it tightens on its own. A contract gate feels the same to me. Rather than scolding the agent into correctness, you carve the structure in advance so that it does not break even when it errs. Whether you can run an autonomous agent overnight with peace of mind comes down to this "carved in advance" design.
If you want to try it next, pick the single scariest tool in your agent and write just one postcondition. If you can state "after this operation, what should be true?" in one line, your contract gate is already half built.
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.