# Useful but Little-Known Features of Claude Agent SDK
🌍 Permission evaluation is a 5-stage pipeline. Understanding what happens where makes security design dramatically clearer.
Hooks, deny rules, permission mode, allow rules, and callback -- evaluated strictly in order.
📌 Title: 5-Stage Permission Evaluation Order
🔗 URL:
🧩 Overview
Claude Agent SDK tool permissions are evaluated in a 5-stage pipeline: (1) Hooks: PreToolUse hooks run first and can deny. However, returning "allow" does NOT skip subsequent evaluation stages. (2) Deny rules: checks disallowed_tools. Bare tool names (e.g., "Bash") remove the tool definition from context entirely. Scoped rules (e.g., "Bash(rm *)") block even in bypassPermissions mode. (3) Permission mode: bypassPermissions approves everything reaching this stage. acceptEdits approves file operations. (4) Allow rules: matched allowed_tools entries are approved. (5) canUseTool callback: called for anything not resolved above. Priority order: deny > defer > ask > allow.
🛠 How to Use
```python
import asyncio
from claude_agent_sdk import (
ClaudeSDKClient,
ClaudeAgentOptions,
HookMatcher,
)
# Stage 1: Hook to block dangerous operations
async def block_dangerous_commands(input_data, tool_use_id, context):
if input_data.get("tool_name") == "Bash":
command = input_data["tool_input"].get("command", "")
if "rm -rf" in command:
return {
"hookSpecificOutput": {
"hookEventName": input_data["hook_event_name"],
"permissionDecision": "deny",
"permissionDecisionReason": "rm -rf is prohibited",
}
}
return {}
async def main():
options = ClaudeAgentOptions(
# Stage 1: Hooks
hooks={
"PreToolUse": [
HookMatcher(matcher="Bash", hooks=[block_dangerous_commands])
],
},
# Stage 2: Deny rules (bare name = removed from context entirely)
disallowed_tools=["WebFetch"],
# Stage 3: Permission mode
permission_mode="acceptEdits", # Auto-approve file operations
# Stage 4: Allow rules
allowed_tools=["Read", "Glob", "Grep", "Edit", "Write"],
)
# Stage 5: canUseTool is configured separately via query() or ClaudeSDKClient
async with ClaudeSDKClient(options=options) as client:
await client.query("Improve the code")
async for message in client.receive_response():
print(message)
```
Lockdown configuration example:
```python
# Principle of least privilege: only allowed tools, everything else denied
options = ClaudeAgentOptions(
allowed_tools=["Read", "Glob", "Grep"],
permission_mode="dontAsk", # Deny unapproved tools without prompting
)
```
🏗 Integration into Production Systems
- allowed_tools does NOT constrain bypassPermissions. Even with only "Read" in allowed_tools, bypassPermissions approves all tools
- Use disallowed_tools to completely block specific tools (effective even in bypassPermissions)
- For least privilege, combine allowed_tools with permission_mode="dontAsk"
- A hook returning "allow" means "this hook is OK with it" -- it does not override subsequent deny rules or mode evaluation
💡 Use Cases
🔒 Least-privilege agent: allow only Read/Glob/Grep with dontAsk to deny everything else
🛡 Progressive trust: start in default mode, escalate to acceptEdits after review
🚫 Eliminate dangerous ops: remove Bash entirely via disallowed_tools, or scope-block with Bash(rm *)
🔍 Audited approvals: log all requests via hooks while auto-deciding via rules
⚠️ Caveats
- Tools not in allowed_tools are "unresolved," not "denied" -- they proceed to the next stage
- Bare tool names in disallowed_tools remove the tool definition from context; the agent won't even know the tool exists
- bypassPermissions is inherited by subagents, which may have different system prompts -- use with caution
- When multiple hooks register for the same event, the most restrictive decision wins
✨ Master the 5-stage permission pipeline for robust security design!
#
ClaudeAgentSDK# #
AIAgent#