tool_approval
Web-based approval for dangerous tools via WebSocket
Quick Start
Lifecycle
User sends prompt
↓
Agent calls LLM
↓
LLM returns tool_calls batch: [bash("npm install"), write("config.json"), bash("npm build")]
↓
tool_executor iterates sequentially:
↓
┌─ Tool #1: bash("npm install")
│ before_each_tool fires → check_approval()
│ → Is it safe? No (bash is DANGEROUS)
│ → Already approved for session? No
│ → Send to client:
│ {
│ "type": "approval_needed",
│ "tool": "bash",
│ "arguments": {"command": "npm install"},
│ "batch_remaining": [
│ {"tool": "write", "arguments": "{...}"},
│ {"tool": "bash", "arguments": "{\"command\": \"npm build\"}"}
│ ]
│ }
│ → BLOCK — wait for client response
│ ↓
│ Client responds: {"approved": true, "scope": "session"}
│ → Execute bash("npm install")
│ → Save "bash" as session-approved
│
├─ Tool #2: write("config.json")
│ before_each_tool fires → check_approval()
│ → Is it safe? No (write is DANGEROUS)
│ → Send approval_needed (batch_remaining: [bash(...)])
│ → Client responds: {"approved": false, "mode": "reject_soft"}
│ → Skip this tool, continue to next
│
├─ Tool #3: bash("npm build")
│ before_each_tool fires → check_approval()
│ → bash is session-approved → skip approval, execute immediately
│
└─ Done. Return results to LLM.Rejection Modes
When the client rejects a tool, the mode field determines what happens next:
reject_soft (Skip)
Skip this tool, agent loop continues. The LLM receives a hint to ask the user what they prefer.
- • Current tool is skipped (raises ValueError)
- • Next tool in the batch proceeds normally
- • LLM gets: "User rejected tool 'write'. Feedback: Don't write that file\n\n[System reminder: Ask the user...]"
reject_hard (Stop)
Skip this tool AND all remaining tools in the batch. The agent loop stops and waits for new user input.
- • Current tool is skipped (raises ValueError)
- •
stop_signalflag is set in session - • All remaining tools in the batch are auto-rejected
- • Agent loop stops — LLM does NOT get another turn
- • User must send a new message to continue
mode is not provided: reject_hardTool Classification
Safe Tools (No Approval)
Read-only operations that never modify state:
list_files, get_file_info, task, load_guide
enter_plan_mode, exit_plan_mode, write_plan
task_output, ask_user
Dangerous Tools (Require Approval)
Operations that can modify files or have side effects:
write, edit, multi_edit
run_background, kill_task
send_email, post, delete, remove
Unknown Tools
Tools not in either list are treated as safe (no approval needed).
Config-Based Auto-Approval
Auto-approve safe commands permanently via host.yaml configuration. Config permissions never expire and apply to all sessions.
Configuration Example
Pattern Types
Simple Tool Name
Matches any call to the tool
"read_file"Exact Bash Command
Only matches exact command
"Bash(git status)"Wildcard Bash Command
Matches command prefix
"Bash(git diff *)"Parameter Matching
Uses 'when' field for granular control
when: {file_path: "*.md"}Priority Order
- 1.Safe tools - Always approved (SAFE_TOOLS list)
- 2.Config permissions - Loaded from host.yaml (
source: config) - 3.Skill permissions - Temporary, turn-scoped (
source: skill) - 4.Session approvals - User approved for session (
source: user) - 5.Runtime approval - Ask user (if none of above match)
Bash Command Chain Permissions
Uses bashlex to parse and validate command chains - ALL commands must be permitted.
✅ All Permitted
❌ Partial Permission
Supported Syntax
| Syntax | Example | Commands Extracted |
|---|---|---|
&& | pwd && ls | ["pwd", "ls"] |
|| | test -f file || echo no | ["test", "echo"] |
| | cat file | grep test | ["cat", "grep"] |
; | echo a; echo b | ["echo", "echo"] |
Security
Whitelist-first: One dangerous command = whole chain rejected.
pwd && rm -rf /
Client Protocol
Server sends
Client responds
Approval Scopes
| Scope | Behavior |
|---|---|
once | Approve this call only |
session | Approve for rest of session (stored in memory) |
