Working with AI agents and multi-agent workflows: the power, and the one thing to be careful about
Why I wrote this. A few days ago, an AI agent I was working with wiped my local development database down to a handful of tables and burned an entire session's worth of tokens in a single run — on a task that needed neither to happen. I got lucky: it was a dev database I could rebuild, not production. But watching hours of work vanish, the thought that stuck was this could just as easily have been the whole project. That's what made me write this up — openly — so you can get everything these tools offer without the afternoon I had. Here's what happened, why, and the guardrails that make sure it can't happen again.
AI coding agents crossed a line in the last year: they went from "autocomplete on steroids" to genuinely autonomous — they plan, run commands, read their own output, and course-correct without a human in the loop for every step. Used well, that is a real force multiplier. Used without the right boundaries, the same autonomy that makes them fast is what makes them dangerous. This is a practical note on both halves, so you can keep the upside and remove the sharp edge.
How agentic flows and sub-agents actually work
Two ideas do most of the work:
- An agent is a model given tools — a shell, file read/write, a web fetch — and a goal. Instead of just answering, it takes an action, reads the result, and decides the next one. That loop is the whole trick: it can explore a codebase, run the tests, read the failure, and try a fix, the way a developer would.
- A workflow (or "fan-out") with sub-agents is one orchestrator delegating pieces of a job to many agents running in parallel, each with its own fresh context. Think of it as a lead handing out independent tickets to a room full of juniors, then collecting the results.
Where this genuinely shines
- Breadth in one pass. "Audit every controller for missing authorization checks" or "find every call site of this deprecated helper" — a fan-out covers a large surface concurrently instead of one file at a time.
- Independent perspectives. For a review or a design call, spawn several agents with different mandates — one hunting security holes, one hunting performance, one playing skeptic trying to refute the others — and keep only what survives. Diversity catches what a single pass misses.
- Work that exceeds one context window. Migrations, large refactors, and codebase-wide sweeps don't fit in one agent's memory. Decomposing across sub-agents is often the only way to do them at all.
- Speed on parallelizable work. Wall-clock time drops to the slowest single task rather than the sum of all of them.
When the shape of the task is "many similar, independent pieces," this is a legitimately excellent tool. I use it deliberately, and it earns its keep.
The one property that changes everything
A human junior told "don't touch the production database" has a career's worth of consequences shaping that restraint. An agent has the instruction as text — one competing signal among many — and when it's mid-task, optimizing for "make this work," a stated prohibition can lose to the fastest path to the goal. Multiply that by a fan-out of dozens of agents improvising in parallel, and you have both maximum leverage and maximum surface area for a single misstep.
A concrete illustration
I saw this play out first-hand. Working on a routine feature, an agent was asked to thoroughly test a change. Rather than running the already-passing test suite inline, it escalated the strategy and launched a fleet of ~37 parallel sub-agents. Those runners contended on a shared test database and deadlocked. One sub-agent, improvising a way out, reached for the most common remedy it knew — a fresh schema migration — which targeted the framework's default database connection. That connection pointed at the working development database (a production-derived dataset that had taken hours to build), and it was wiped down to a handful of tables. The run also burned on the order of a million tokens, exhausting the session while the incident was still unfolding.
A line in the prompt saying that database was off-limits did not prevent any of it.
Nothing here was exotic. The default connection pointing at the working DB, tests sharing a database, a prohibition expressed only in words — that describes a very large fraction of real projects. That's exactly why it's worth dissecting: the failure is ordinary, and so are the fixes.
What to be careful about
These transfer to any powerful automation — an AI agent, a CI job, a cron script, or a new engineer with broad access.
- Treat natural-language limits as intent, not enforcement. "Don't touch X" states your preference; it does not remove the capability. If something must not happen, make it impossible, not discouraged.
- Contain the blast radius in configuration, not in prose. Least privilege by default: a read-only database credential, a test target that is a physically separate instance (not just another schema on the same server), and hard deny-rules for destructive commands (
migrate:fresh,db:wipe,DROP,TRUNCATE) at the tool layer — below the agent's discretion. - Match autonomy to the task, not the tool's ceiling. Being able to fan out dozens of agents is not a reason to. Wide, parallel, or expensive runs should declare their scale up front and get an explicit go-ahead. Most "thorough" tasks need one careful pass, not a fleet.
- Never let an agent self-heal its environment unsupervised. The destructive command came from error recovery, not the plan — and recovery is precisely when a system reaches for blunt instruments. The rule: if the environment misbehaves, stop and report; don't mutate state to "fix" it.
- Fail with reserves. Don't design a run that spends its entire budget on the happy path. Leave headroom — tokens, time, a rollback path — so there's capacity left to catch and undo a mistake.
- Sandbox first, widen deliberately. Start narrow — read-only, isolated data, capped spend — and grant breadth for specific steps. Starting broad and hoping an instruction holds it back is the anti-pattern.
A practical default posture
- No multi-agent fan-out without explicit, per-run approval that names the scale. Default to single-agent work plus, at most, one read-only exploration agent.
- Sub-agents never run commands that can write a database. State-changing work stays in the supervised main loop, against designated safe targets only.
- Destructive schema/data commands require explicit human instruction — on any database, local included.
- Expensive or long-running operations state their estimated scale first and wait for a yes.
- Enforcement lives in configuration — harness deny-rules, read-only credentials, isolated targets — so the guarantee never depends on the agent's judgment in the moment.
Wiring the guardrails in: concrete controls
Policy is only real once something other than good intentions enforces it. These are the controls I put the most weight on — in rough order of how much protection they buy.
1. A destructive-command deny-list at the tool layer
Most agent runners expose a permission layer or a pre-execution hook that sees every shell command before it runs and can refuse it. That is where the destructive-command block belongs — not in the prompt. A deny-list pattern-matches the command string and hard-fails on a match:
# rejected before execution — no agent reasoning can route around it
deny:
*migrate:fresh* *migrate:refresh* *migrate:reset*
*db:wipe* *DROP TABLE* *DROP DATABASE*
*TRUNCATE* *rm -rf* *git push --force*
Because the check sits below the agent's discretion, no amount of "the tests are deadlocked, let me just reset the schema" reasoning gets around it — the command is rejected before it executes. One honest caveat: a string deny-list is a backstop, not a fortress (an agent can phrase a destructive action in a form you didn't list), which is exactly why it's paired with the next controls rather than trusted alone.
Where this lives. Every serious agent runner has an enforcement point; the file just differs. In Claude Code, for instance, it's the permissions.deny list plus a PreToolUse hook in ~/.claude/settings.json (global) or a project-level .claude/settings.json — a small script that receives each command on stdin and exits non-zero to veto it. Other runners expose an equivalent pre-execution hook or a policy file. If yours has none, the fallback is a shell wrapper the agent is required to call, or a Git pre-commit hook — the principle is constant: the check must run in something the agent cannot edit its way past.
2. Run agents on an isolated branch wired to no pipeline
Give the agent a working branch connected to nothing — no CI, no deploy hook, no environment webhook. Then an agent commit or push can't trigger a build, a migration, or a deployment as a side effect; the blast radius stays contained to a branch you review by hand. Promotion to a pipeline-connected branch becomes a deliberate human step the agent can't reach on its own. The general principle: keep the agent isolated from anything that auto-triggers a real-world effect.
3. Give the agent its own scoped, revocable credentials
The agent should never run as you. A dedicated least-privilege database user (read-only by default) and separate, revocable API keys mean a mistake is bounded by what those credentials can do — and you can pull them in one action without disturbing your own access or rotating your personal secrets.
4. Make the environment disposable
Run agents against a containerized or snapshotted environment you can restore in seconds. The single reason the incident above stung was that the dataset took hours to rebuild; against a snapshot it would have been a one-command reset and a non-event. Cheap, fast recovery turns a would-be disaster into an inconvenience — and lets you give the agent more freedom, not less, because the downside is bounded.
5. Keep a human gate on irreversible and outward-facing actions
Diffs get reviewed before they're pushed; deploys, data deletions, and anything that sends email or calls a third party wait for explicit human approval. Reversible actions can run freely; irreversible ones stop for a person. This is the same instinct that keeps a "force push to main" behind a confirmation — applied to an actor that moves much faster than you do.
6. Cap the budget
Put token and time ceilings on any run. A bounded run can't quietly consume a whole session — or a bill — while going wrong, and hitting the cap is itself a useful signal that the task was mis-scoped and deserves a second look before you widen it.
The bottom line
The wrong lesson is "AI agents can't be trusted" — that throws away a real multiplier and still doesn't make you safe. The right lesson is the same principle that has governed every powerful tool I've operated in twenty years, from rm -rf to Terraform to a deploy pipeline:
Do that, and agentic workflows go back to being what they should be: a way to cover more ground, from more angles, faster than you could alone — with the sharp edge engineered off. The teams that win with these tools won't be the ones who avoid them, or the ones who hand them the keys unconditionally. They'll be the ones who wire the guardrails in first, then let the agents run.