Allowlist vs Blocklist (Why Default-Deny Wins) + Code

Blocklists rot. Allowlists scale. A practical tool policy model that doesn’t accidentally permit the next dangerous tool you add.
On this page
  1. Problem-first intro
  2. Why this fails in production
  3. 1) You can’t enumerate “all dangerous tools” in advance
  4. 2) Blocklists fail on naming and indirection
  5. 3) Default-allow is how blast radius grows quietly
  6. 4) Allowlists force an explicit decision
  7. Implementation example (real code)
  8. Real failure case (incident-style, with numbers)
  9. Trade-offs
  10. When NOT to use
  11. Copy-paste checklist
  12. Safe default config snippet (JSON/YAML)
  13. FAQ (3–5)
  14. Related pages (3–6 links)
Interactive flow
Scenario:
Step 1/3: Execution

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”
PYTHON
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"
JAVASCRIPT
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:

  1. default-deny allowlist
  2. write tools behind approvals
  3. 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)

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)

Are blocklists ever useful?
Yes: for incident mode and temporary disables. They’re a brake, not a permission model.
Can I use wildcard allowlists?
Be careful. Wildcards drift into default-allow. If you need patterns, make them narrow and review regularly.
What about ‘workflow.run’ tools?
They hide side effects. Prefer explicit tools for explicit actions so policy can reason about them.
What’s the minimal safe policy?
Default-deny allowlist + read-only tools. Add writes later behind approvals.

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.

Not sure this is your use case?

Design your agent ->
⏱️ 5 min readUpdated Mar, 2026Difficulty: ★★★
Implement in OnceOnly
Budgets + permissions you can enforce at the boundary.
Use in OnceOnly
# onceonly guardrails (concept)
version: 1
budgets:
  max_steps: 25
  max_tool_calls: 12
  max_seconds: 60
  max_usd: 1.00
policy:
  tool_allowlist:
    - search.read
    - http.get
writes:
  require_approval: true
  idempotency: true
controls:
  kill_switch: { enabled: true }
Integrated: production controlOnceOnly
Add guardrails to tool-calling agents
Ship this pattern with governance:
  • Budgets (steps / spend caps)
  • Tool permissions (allowlist / blocklist)
  • Kill switch & incident stop
  • Idempotency & dedupe
  • Audit logs & traceability
Integrated mention: OnceOnly is a control layer for production agent systems.
Author

This documentation is curated and maintained by engineers who ship AI agents in production.

The content is AI-assisted, with human editorial responsibility for accuracy, clarity, and production relevance.

Patterns and recommendations are grounded in post-mortems, failure modes, and operational incidents in deployed systems, including during the development and operation of governance infrastructure for agents at OnceOnly.