Tool Allowlist Policy
The tool-allowlist policy category enforces a positive allowlist of tools an agent may invoke. The absence of a tool from the allowlist blocks it.
This differs from safety.blocked_tools (deny-only): with the allowlist, you don't have to enumerate every dangerous tool -- you only have to enumerate the safe ones. Critical for Codex and third-party agents whose tool surface is unbounded.
Rules
| Rule | Type | Default | Description |
|---|---|---|---|
allowed_tools | string[] | [] | Positive allowlist. Empty = no allowlist (all tools allowed unless in blocked_tools) |
blocked_tools | string[] | [] | Always-deny list. Wins over allowlist (deny overrides allow) |
action_on_violation | string | block | Action on violation: block or warn |
How It Works
The tool-allowlist handler runs at mid_execution and after_workflow (as a fallback for batched tools-used updates). before_workflow is a no-op since no tools have been invoked yet.
Semantics:
- Empty
allowed_tools-> no positive allowlist; onlyblocked_toolsapplies - Non-empty
allowed_tools-> ONLY those tools are permitted; any other tool is a violation blocked_toolsalways wins (deny overrides allow)
Context Attributes Read
| Attribute | Phase | Purpose |
|---|---|---|
context.tools_used | mid, after | List of tool names invoked this run |
Example Policy
Strict allowlist for Codex-style agents
{
"name": "Codex tool allowlist",
"category": "tool-allowlist",
"rules": {
"allowed_tools": ["read_file", "list_directory", "search_code"],
"blocked_tools": ["shell_exec", "write_file"],
"action_on_violation": "block"
},
"scope": {
"agents": ["codex-*"]
},
"enabled": true
}
Warn-first rollout
{
"rules": {
"allowed_tools": ["http_get", "summarize"],
"action_on_violation": "warn"
}
}
Roll out as warn first, watch the governance trace for what tools your agents actually use, then promote to block.
SDK Integration
import waxell_observe as waxell
waxell.init()
@waxell.observe(agent_name="codex-assistant", enforce_policy=True)
async def codex_agent(query: str) -> str:
return await run_agent(query)
The handler reads context.tools_used -- the runtime / observe SDK populates this as the agent invokes tools. Mid-execution checks fire after each tool batch.
Observability
| Field | Example (BLOCK) |
|---|---|
| Category | tool-allowlist |
| Action | block |
| Reason | Tool 'shell_exec' is on the deny list or Tool 'fetch_url' is not on the allowlist |
| Metadata | {"blocked_tool": "shell_exec", "allowed_tools": [...]} |
Common Gotchas
- Exact name match.
"shell"does not block"shell_exec". Use the full tool name. - Empty allowlist = NO allowlist. Setting
allowed_tools: []does not block all tools -- it disables the allowlist and falls back toblocked_toolsonly. To block all tools, use a single never-matching name like["__none__"]. blocked_toolsoverridesallowed_tools. Listing the same tool in both = blocked.- Differs from
safety.blocked_tools. Safety's blocked_tools is checked viacheck_tool_allowed()(a standalone API). The allowlist handler checkscontext.tools_usedautomatically at mid_execution. Use both for layered defense. - Mid-execution fires only if the runtime calls the handler between steps.
after_workflowis the fallback to catch batched updates. - First violation short-circuits the run. The handler returns BLOCK on the first non-allowed tool; subsequent tools aren't checked.
Next Steps
- Safety Policy --
blocked_tools(deny-only) + content filters - Code Execution Policy -- Sandbox controls for
shell_exec-style tools - MCP Server Allowlist -- Same shape, but for MCP servers
- Prompt Allowlist -- Same shape, but for prompt templates
- Policy Categories & Templates -- All categories