Building a Dry-Run Mode for Your Harness
See exactly what an agent would do before it touches production. An agent harness dry run runs the full loop, simulates mutating tools, and hands you the plan to review.
def dispatch(call, dry_run=False):
tool = TOOLS[call.name]
if dry_run and tool.mutates:
PLAN.record(call.name, call.args) # log the intent
return tool.simulate(call.args) # fake but plausible result
return tool.execute(call.args) # real executionPicture this: you've built an agent that manages cloud infrastructure, and you're about to let it run against production for the first time. You're fairly sure it'll do the right thing. "Fairly sure" is not a feeling you want about something that can delete databases. What you want is to watch it decide what it would do — every tool call, every argument — without any of it actually happening. That's an agent harness dry run, and it's one of the highest-value features you can add to a harness that takes real actions. This post covers what it is, why it changes how you ship agents, and how to build one that's actually faithful.
The idea is borrowed from infrastructure tooling, where
terraform planterraform applyWhat Is an Agent Harness Dry Run?
An agent harness dry run executes the full agent loop — the model reasons, decides on tool calls, produces arguments — but every tool that would change the world instead returns a simulated result and records what it would have done. The agent runs to completion; the world stays untouched. At the end, you have a complete transcript of intended actions to inspect.
The distinction that makes it useful is between read tools and write tools. Read tools (search, fetch, look up) can run for real in a dry run — they don't change anything, and their real results make the agent's decisions realistic. Write tools (delete, send, charge, deploy) are the ones that get intercepted, returning a plausible fake result so the agent can continue reasoning as if they'd succeeded.
[object Object], ,[object Object],(,[object Object],):
tool = TOOLS[call.name]
,[object Object], dry_run ,[object Object], tool.mutates:
PLAN.record(call.name, call.args) ,[object Object],
,[object Object], tool.simulate(call.args) ,[object Object],
,[object Object], tool.execute(call.args) ,[object Object],What this does: In dry-run mode, any tool marked as mutating records its intended call and returns a simulated result instead of executing, while read-only tools run normally. The agent proceeds through its whole loop believing its actions worked, and you collect a complete plan of everything it would have changed. One flag turns a live agent into a planner.
Why It Matters
A dry run collapses the risk of the first live run. Instead of "let's run it and watch nervously," you run it in dry mode, read the plan, confirm every intended action is sane, and then run for real with confidence. The scary first-contact-with-production moment becomes a document review.
It also changes how you develop. You can iterate on prompts and tools by running dry against real scenarios and reading what the agent decides, without side effects and without cleanup. A whole class of "run it, see what broke, undo the damage, try again" development loops disappears, replaced by "run dry, read the plan, adjust." That's faster and far less nerve-wracking.
⚠️ Common mistake: Building a dry run that skips mutating tools entirely instead of simulating them. If a delete tool just returns nothing in dry mode, the agent's next decision is based on a world where the delete didn't happen — which diverges from reality and produces a plan you can't trust. The simulation has to be plausible — return what a real success would have returned — so the rest of the run stays realistic.
Making Simulations Faithful
The value of an agent harness dry run is only as good as its simulations. A dry run whose fake results don't resemble real ones produces a plan that wouldn't match the real execution — worse than useless, because it looks authoritative while being wrong.
The best pattern is to have each tool define its own simulation alongside its execution, so the fake result mirrors the real one's shape and the person maintaining the tool maintains both together.
[object Object], ,[object Object],(,[object Object],):
mutates = ,[object Object],
,[object Object], ,[object Object],(,[object Object],):
n = db.delete(args[,[object Object],], args[,[object Object],])
,[object Object], {,[object Object],: n}
,[object Object], ,[object Object],(,[object Object],):
n = db.count(args[,[object Object],], args[,[object Object],]) ,[object Object],
,[object Object], {,[object Object],: n, ,[object Object],: ,[object Object],}What this does: The real
executesimulate⚡ Pro tip: Have simulations return real previews wherever they can — a count of affected rows, the actual recipients an email would go to, the real diff a change would apply. A dry run that says "would delete 340 rows from
ordersUsing Dry Runs Beyond Development
Once you have a faithful dry run, it powers three things beyond first-run confidence.
It powers approval previews. The plan a dry run produces is exactly what a human approver needs to see — "here's everything this run will do." Feeding the dry-run plan into an approval gate gives reviewers a concrete, complete picture instead of a single action out of context.
It powers testing. A dry run against a fixed scenario produces a deterministic plan you can assert against — "given this input, the agent should plan to call these tools with these arguments." That's a powerful regression test for agent behavior, catching when a prompt change alters what the agent decides to do.
It powers safe demos and audits. You can show stakeholders exactly what an agent would do in real situations without any risk, which is often what unblocks approval to run it live at all.
⚡ Pro tip: Diff the dry-run plan against the actual execution afterward, at least during rollout. If the agent planned to delete 340 rows but really deleted 900, your simulation is drifting from reality and the dry run is lying to you. A periodic plan-versus-reality diff keeps your simulations honest over time as the underlying tools evolve.
Structuring Tools So Dry-Run Is Cheap
The reason many teams never add a dry run is that retrofitting one onto tools designed without it is painful — you end up threading a
dry_runGive every tool two required methods,
executesimulatemutatessimulateexecute[object Object], ,[object Object],(,[object Object],):
mutates: ,[object Object], = ,[object Object],
,[object Object],
,[object Object], ,[object Object],(,[object Object],): ...
,[object Object], ,[object Object],(,[object Object],):
,[object Object], ,[object Object],.mutates:
,[object Object], NotImplementedError(,[object Object],)
,[object Object], ,[object Object],.execute(args) ,[object Object],What this does: Makes
simulatesimulate⚡ Pro tip: Make
mutatesTrueFalseFalseCommon Mistakes
The mistake that undermines the whole feature is inconsistent coverage — some mutating tools honor dry-run mode and others don't. A single tool that executes for real during a dry run makes the entire feature untrustworthy, because now a dry run might change something. Mark every mutating tool, and default unmarked tools to "assume mutating" so a forgotten flag fails safe.
The second is letting read tools with side effects slip through. A "search" tool that also logs analytics or increments a counter isn't purely read-only, and running it for real in a dry run leaks side effects. Audit your read tools for hidden writes.
The third is treating the dry-run plan as guaranteed. The real run happens later, against a world that may have changed — the 340 rows might be 350 by then. The plan is a high-confidence preview, not a contract, and irreversible actions still deserve their approval gates even after a clean dry run.
Conclusion
An agent harness dry run turns the riskiest moment — the first live run against real systems — into a document you read before anything happens. Simulate mutating tools faithfully, return real previews, mark every tool honestly, and the same feature powers approvals, behavior tests, and safe demos. It's a small amount of code for a large amount of confidence — and it's the kind of feature that, once you have it, you can't imagine having shipped an action-taking agent without.
Because faithful simulations and the tools they mirror have to evolve together, keep them versioned alongside your prompts in a library like PromptABCD, so the dry-run behavior you built stays in lockstep with the tools it previews — and every agent you ship can show its plan before it touches the world.
Continue Reading
Save the prompts from this post
PromptABCD is a free prompt manager. Paste, organize, and reuse your best AI prompts — no more hunting through chat history.
