-
Notifications
You must be signed in to change notification settings - Fork 142
Description
Summary
Add a per-tool permission policy system that enables orchestrating agents to control which tools are auto-approved, auto-denied, or escalated — using acpx's existing session persistence for the escalation flow.
This addresses the Permission policies item from the ACP coverage roadmap — per-tool rules beyond the current all-or-nothing modes. The escalation flow naturally enables multi-agent orchestration as a downstream use case.
Problem
Today, session/request_permission supports three modes: approve-all, approve-reads, deny-all. When no TTY is available (agent-to-agent, CI/CD), permission requests are either silently denied or cause the session to crash (nonInteractivePermissions: fail).
This forces orchestrators to use approve-all — losing all safety guarantees. There is no way to:
- Auto-approve safe tools (Read, Glob, Grep) while blocking dangerous ones (rm, Write)
- Let an orchestrating agent review specific tool calls before allowing them
- Have the sub-agent pause and let the orchestrator decide
Relationship with #49
#49 proposes --allowed-tools for tool availability — controlling which tools the agent can see. This proposal is about tool approval — controlling which available tools require permission before execution.
They compose naturally:
--allowed-tools ["Read","Glob","Bash"] → agent can only use these 3 tools
--policy '{"autoApprove":["Read","Glob"],"escalate":["Bash"]}' → of those 3, Bash needs approval
Complementary, not overlapping.
Proposed Solution
1. Per-tool policy (new permission mode)
A new --policy flag accepts a JSON file or inline JSON:
# Via policy file
acpx --policy ./policies/read-only.json claude -s task "Check this repo"
# Via inline JSON
acpx --policy '{"autoApprove":["Read","Glob","Grep"],"autoDeny":["Write"],"escalate":["Bash"]}' claude -s task "Check this repo"Policy schema:
{
"autoApprove": ["Read", "Glob", "Grep", "WebSearch"],
"autoDeny": ["Write", "WebFetch"],
"escalate": ["Bash", "Edit"],
"defaultAction": "escalate"
}autoApprove— allow without prompting (scoped version ofapprove-all)autoDeny— reject without promptingescalate— gracefully deny with structured escalation info (see below)defaultAction— fallback for tools not in any list ("escalate"|"approve"|"deny")
Tool matching should support both tool names (e.g., "Bash", "Read") and tool kinds (e.g., "read", "execute"), building on acpx's existing tool kind inference.
2. Session-based escalation flow
When a tool call matches escalate and no TTY is available, acpx:
- Gracefully denies the tool call with a structured message indicating escalation
- Includes escalation details in the session output so the caller knows what was blocked
- The turn ends — control returns to the orchestrator
- The orchestrator reviews the escalation, decides to approve or deny
- If approved, the orchestrator resumes the session with a new prompt instructing the agent to retry, and updates the policy to allow the tool
This leverages acpx's existing session persistence (session/load, -s named sessions) — no new IPC mechanism needed.
Example flow
# Step 1: Orchestrator spawns session with restrictive policy
$ acpx --policy '{"autoApprove":["Read","Glob"],"escalate":["Bash"]}' \
claude -s task "Run the integration tests"
# Claude reads files (auto-approved), then wants to run bash...
# Output includes:
# [escalation] Bash denied: "bash infra/scripts/check-services.sh"
# Tool kind: execute
# Session paused. Resume with updated policy to approve.
# Step 2: Orchestrator reviews, decides to approve bash for infra/scripts/*
$ acpx --policy '{"autoApprove":["Read","Glob","Bash"],"escalate":[]}' \
claude -s task "The previously blocked Bash command is now approved. Please continue."
# Claude retries, Bash is now auto-approved, work continues...Structured escalation output
When a tool is escalated, acpx includes structured JSON in the output (when --format json):
{
"type": "escalation",
"tool": "Bash",
"kind": "execute",
"title": "bash infra/scripts/check-services.sh",
"input": {"command": "bash infra/scripts/check-services.sh"},
"sessionId": "abc-123",
"sessionName": "task"
}This gives orchestrators everything they need to make a decision and resume.
3. TTY fallback preserved
When a TTY is available, escalate tools prompt interactively via stdin (existing behavior). The session-based flow only activates when no TTY is detected.
Use Cases
Agent-to-agent orchestration:
Orchestrator spawns: acpx --policy read-and-test.json claude -s task "Run tests"
Claude wants Read → autoApprove → ✅ instant
Claude wants Bash → escalate → ⏸ turn ends, escalation returned
Orchestrator reviews → approves → resumes session with updated policy
Claude retries Bash → autoApprove → ✅ continues
CI/CD with human gate:
GitHub Action runs: acpx --policy ci.json claude -s pr "Review PR #42"
Claude wants Edit → escalate → CI posts to Slack for approval
Human approves → CI resumes session with Edit allowed
Local development (TTY available):
Dev runs: acpx --policy dev.json claude -s work "Implement feature"
Claude wants Bash → escalate → terminal prompt "Allow? (y/N)" → existing behavior
Progressive trust:
# Start restrictive
acpx --policy '{"autoApprove":["Read"],"defaultAction":"escalate"}' claude -s task "Fix bug"
# After reviewing first few escalations, loosen policy
acpx --policy '{"autoApprove":["Read","Bash","Edit"],"defaultAction":"deny"}' claude -s task "Continue"Non-goals
To keep this proposal focused, the following are explicitly out of scope:
- Path-based rules (e.g.,
allow reads to src/,deny writes to .env) — valuable but a separate layer of complexity. Can be added as a follow-up enhancement to the policy schema. - Runtime policy updates mid-session — policies are set at session start. Changing policy requires resuming with a new
--policyvalue, not hot-reloading. - Tool argument inspection — this proposal matches on tool name only, not on the arguments passed to the tool (e.g., cannot distinguish
bash lsfrombash rm -rf /). - Authentication/identity for escalation reviewers — the orchestrator is trusted. Access control for who can approve escalations is outside acpx's scope.
Design Decisions
-
Why session-based, not file polling? — Reuses existing acpx session persistence. No new IPC mechanism, no filesystem coordination, no polling. The orchestrator already knows how to send prompts to named sessions.
-
Why graceful denial, not pause? — ACP
requestPermissionis a synchronous request/response within a JSON-RPC stream. The agent blocks waiting for a response. True pause would require protocol changes. Graceful denial with structured escalation info achieves the same outcome using existing primitives. -
Why policy files, not just CLI flags? — Policies can be reusable presets (
read-only.json,full-dev.json,ci-safe.json). CLI inline JSON is supported for ad-hoc use. Both work. -
Why not extend
--approve-reads? —approve-readsis a single hardcoded rule. Per-tool policies are a generalization:{"autoApprove":["Read","Search"]}is equivalent to--approve-reads, but you can also express--approve-reads-and-bash-in-infrawhich isn't possible today.
Related
- #49 —
--allowed-toolsflag for tool availability (complementary) - #91 —
disableExecconfig option (precedent for tool-level restrictions) - #120 — permission denial stats accounting (existing permission infrastructure)
- #94 / #92 — CLI passthrough flags,
_meta.claudeCode.optionsplumbing - ACP coverage roadmap — lists permission policies as a known gap
Happy to implement this. Would appreciate feedback on:
- The policy schema — is the
autoApprove/autoDeny/escalatesplit right? - The escalation output format — what fields do orchestrators need?
- Whether
--policyshould be a global flag or per-agent subcommand flag
This proposal was drafted with AI assistance.