Idea In 30 Seconds
Allowlist in runtime defines which tools the agent is allowed to call. Blocklist only forbids part of tools from an already large set.
When you need it: when an agent has access to real APIs, write actions, or regularly receives new tools in production.
Problem
Many teams start with blocklist: "we forbid dangerous tools." At first this looks fast and convenient. In production this model breaks quickly.
The reason is simple: every new tool automatically becomes available unless explicitly blocked. So access grows by default, not by explicit decision.
Analogy: it is like an office with no list of allowed doors, only a list of "do not enter here". As soon as a new room appears, it is open for everyone until someone remembers to forbid it.
Solution
The solution is to set default deny in policy layer and allow tools only through allowlist.
Keep blocklist as an additional emergency brake for incident mode, not as the primary access model.
Policy layer returns technical decision for each call: allow, deny, or approval_required.
This decision is made on every step, not only at the end of run.
This is a separate system layer, not part of prompt or model logic.
Allowlist β Blocklist
These are different control approaches:
- Allowlist: only what is explicitly added to policy is allowed.
- Blocklist: everything is allowed except what is explicitly forbidden.
One without the other is not enough:
- without allowlist, access expands across releases almost invisibly
- without blocklist, it is harder to emergency-disable a specific tool during incident
Example:
- allowlist:
search.read,kb.read,refund.lookup - incident blocklist: temporarily deny
browser.runbecause of unstable vendor
Tool Access Control Metrics
These metrics and signals work together on every agent step.
| Metric | What it controls | Key mechanics | Why |
|---|---|---|---|
| Default deny | Baseline access rule | default = denydeny by default | New tool does not become available automatically |
| Allowlist | Which tools are allowed | explicit tool names capability scoping | Creates explicit boundaries for runtime execution |
| Incident blocklist | Temporary emergency blocking | incident mode deny list time-bound rules | Allows fast risk reduction without release |
| Write escalation | Risky write actions | approval_requiredseparate write policy | Prevents irreversible actions without control |
| Policy observability | Visibility into policy decisions | audit logs alerts on deny spikes | Does not directly limit actions, but shows where and why access is blocked |
How It Looks In Architecture
Policy layer (tool gateway) sits between runtime and tools and is the single access-control point before each step.
Each decision (allow, deny, approval_required) is recorded in audit log.
Each agent step passes through this flow before execution: runtime does not execute action directly β first policy check, then execution.
For write actions policy may return approval_required β this is a separate confirmation flow not shown in this diagram (see Human approval).
Flow summary:
- Runtime forms tool call
- Policy layer applies
default deny, then allowlist and incident blocklist allow-> tool call is executeddeny-> stop reason is returned- every decision is written to audit log
In runtime, deny is converted to explicit stop reason visible in logs and run response.
Example
A support agent receives a request for bulk ticket closing.
Tool ticket.close_bulk is not in allowlist, so call is immediately blocked.
During a separate incident, team adds browser.run to incident blocklist.
Even if this tool exists in allowlist, policy layer temporarily returns deny("incident_deny").
Allowlist constrains baseline access, and blocklist provides fast emergency control.
In Code It Looks Like This
In the simplified diagram above, the main control flow is shown. In practice, checks are executed in one place β policy layer before each step.
Example allowlist config:
policy:
default: deny
allowlist:
- search.read
- kb.read
- refund.lookup
incident_blocklist:
- browser.run
decision = policy.evaluate(tool, user_context, mode="normal")
if decision.outcome == "approval_required":
if not approval.ok():
audit.log(run_id, decision.outcome, reason=decision.reason, tool=tool)
return stop(decision.reason)
else:
audit.log(run_id, "approval_granted", reason="human_approved", tool=tool)
elif decision.outcome == "deny":
audit.log(run_id, decision.outcome, reason=decision.reason, tool=tool)
alerts.notify_if_needed(run_id, decision.reason, tool=tool)
return deny(decision.reason)
result = tool.execute(args)
decision = Decision.allow(reason="policy_ok")
audit.log(run_id, decision.outcome, reason=decision.reason, tool=tool, result=result.status)
return result
How It Looks During Execution
Scenario 1: tool not in allowlist (deny)
1. Runtime forms ticket.close_bulk call.
2. Policy layer checks allowlist.
3. Decision: deny (tool_not_allowed).
4. Audit: decision=deny, tool=ticket.close_bulk, reason=tool_not_allowed.
5. Action is not executed.
---
Scenario 2: incident block (incident_deny)
1. Runtime forms browser.run call.
2. Tool is in allowlist, but incident mode is active.
3. Policy layer checks blocklist and returns deny (incident_deny).
4. Audit: decision=deny, tool=browser.run, reason=incident_deny.
5. Run is stopped without calling tool.
---
Scenario 3: allowed call (allow)
1. Runtime forms kb.read call.
2. Policy layer checks rules: allow.
3. Tool is executed.
4. Audit: decision=allow, tool=kb.read, result=ok.
5. Result is returned to runtime.
Common Mistakes
- making blocklist the primary access model
- not using
default denyfor new tools - allowing wildcard like
*without narrow boundaries - keeping "universal" tools like
workflow.run_anything - logging only allow but not deny/approval_required
- checking access in prompt or UI, not in policy layer
As a result, access looks controlled, but in reality expands with each release.
Self-Check
Quick allowlist-policy check before production launch:
Progress: 0/8
β Baseline governance controls are missing
Before production, you need at least access control, limits, audit logs, and an emergency stop.
FAQ
Q: Can we run production using only blocklist?
A: For production β no. Blocklist is useful as emergency mechanism, but baseline model should be default deny + allowlist.
Q: Where do we start with allowlist?
A: Start with minimal read-only tool set. Add write actions separately, with explicit conditions and approval.
Q: Is wildcard allowlist (crm.*) okay?
A: Only in narrow cases with regular review. Otherwise wildcard quickly turns into hidden default-allow. For example, crm.* will allow crm.delete_all_contacts if such tool appears in the next release.
Q: Where should these checks be implemented?
A: In centralized policy layer (tool gateway), not in prompt and not in UI logic.
Q: What if we need to disable a tool urgently?
A: Add it to incident blocklist, record reason in audit log, then return to permanent allowlist policy.
Where It Fits In The Whole System
Allowlist/blocklist is one of Agent Governance layers. Together with RBAC, budgets, approval, and audit, it forms a unified execution-control system.
Related Pages
Next on this topic:
- Agent Governance Overview β overall production control model for agents.
- Access Control (RBAC) β how to combine roles, permissions, and tenant scope.
- Budget Controls β how to limit run spend at runtime level.
- Human approval β how to add confirmation for risky actions.
- Audit Logs For Agents β how to debug policy decisions in incidents.