Action is proposed as structured data (tool + args).
Problem-first intro
You start with a blocklist because it’s fast: “deny the dangerous stuff.”
Then you add a new tool. You forget to update the blocklist. The agent finds it.
And now you’re on-call because “deny the dangerous stuff” was a story you told yourself.
Blocklists are appealing because they feel like control. In agent systems, they’re usually a trap. They also rot fast. Two weeks later, nobody remembers what the list was “protecting”.
Why this fails in production
1) You can’t enumerate “all dangerous tools” in advance
Today it’s db.delete_user.
Tomorrow it’s crm.merge_accounts.
Next week it’s tickets.close_all.
The tool you forgot to block is the one that bites you.
2) Blocklists fail on naming and indirection
If you block db.write, someone ships db.patch.
If you block email.send, someone ships email.send_bulk.
Even worse: wrappers.
The agent calls workflow.run("close_ticket") and your “blocklist” never sees the real side effect.
3) Default-allow is how blast radius grows quietly
If the policy says “allow everything except…”, you’re shipping permission expansion by default. Every new tool is an incident waiting to happen.
4) Allowlists force an explicit decision
Allowlists are annoying. Good. They force you to say: “yes, the agent may call this tool under these conditions.”
Implementation example (real code)
This is a small policy evaluator:
- default-deny
- optional blocklist for emergency disables (incident mode)
- explicit “write tools require approval”
from dataclasses import dataclass
WRITE_TOOLS = {"email.send", "db.write", "ticket.close"}
@dataclass(frozen=True)
class Policy:
allow: set[str]
deny: set[str] = None # for incident mode
require_approval_for_writes: bool = True
class Denied(RuntimeError):
pass
def evaluate(policy: Policy, tool: str) -> str:
deny = policy.deny or set()
if tool in deny:
raise Denied(f"denied: {tool} (incident mode)")
if tool not in policy.allow:
raise Denied(f"not allowed: {tool}")
if policy.require_approval_for_writes and tool in WRITE_TOOLS:
return "approve"
return "allow"const WRITE_TOOLS = new Set(["email.send", "db.write", "ticket.close"]);
export class Denied extends Error {}
export function evaluate(policy, tool) {
const deny = new Set(policy.deny || []);
if (deny.has(tool)) throw new Denied("denied: " + tool + " (incident mode)");
if (!policy.allow.includes(tool)) throw new Denied("not allowed: " + tool);
if (policy.requireApprovalForWrites && WRITE_TOOLS.has(tool)) return "approve";
return "allow";
}Real failure case (incident-style, with numbers)
We saw a team ship an agent with: “deny dangerous tools” policy.
Then they added a new tool: ticket.close_bulk.
It wasn’t in the deny list.
The agent used it because it was the shortest path to “resolve”.
Impact:
- ~200 tickets closed incorrectly
- ~5 engineer-hours to reopen, explain, and patch
- the agent was disabled for a week because nobody trusted it
Fix:
- default-deny allowlist
- write tools behind approvals
- deny list reserved for incident mode only (temporary)
Blocklists are fine as an emergency brake. They’re bad as your steering wheel.
Trade-offs
- Allowlists slow “just ship it” velocity. That’s a feature in prod.
- You’ll need tooling to manage allowlists (don’t hardcode in 12 places).
- Developers will try to bypass it with wrappers. Don’t let them.
When NOT to use
- If you’re prototyping locally, you can start looser — but don’t carry that to prod.
- If all tools are read-only, a small allowlist is still worth it (it prevents accidental exposure).
- If your “tool” is a generic RPC that can do anything, fix that first. Split tools by capability.
Copy-paste checklist
- [ ] Default-deny allowlist (explicit tool names)
- [ ] Separate tools by capability (read vs write)
- [ ] Approvals for irreversible writes
- [ ] Deny list only for incident mode (temporary)
- [ ] Log denied tool attempts (it’s a signal)
- [ ] Don’t expose generic “do anything” tools
Safe default config snippet (JSON/YAML)
policy:
default: "deny"
allow: ["search.read", "kb.read", "http.get"]
require_approval_for_writes: true
incident_mode:
deny: ["browser.run"] # temporary brake
logging:
log_denies: true
FAQ (3–5)
Used by patterns
Related failures
Q: Are blocklists ever useful?
A: Yes: for incident mode and temporary disables. They’re a brake, not a permission model.
Q: Can I use wildcard allowlists?
A: Be careful. Wildcards drift into default-allow. If you need patterns, make them narrow and review regularly.
Q: What about ‘workflow.run’ tools?
A: They hide side effects. Prefer explicit tools for explicit actions so policy can reason about them.
Q: What’s the minimal safe policy?
A: Default-deny allowlist + read-only tools. Add writes later behind approvals.
Related pages (3–6 links)
- Foundations: How agents use tools · Planning vs reactive agents
- Failure: Prompt injection attacks · Tool spam loops
- Governance: Tool permissions · Kill switch design
- Production stack: Production agent stack