Problem
This blocks all agent execution. When agents are spawned with --print --output-format stream-json, the Claude CLI still prompts for tool permissions. Since agents run as background subprocesses with no TTY, they block indefinitely waiting for input that never comes.
Discovered during first UAT: Archie spawned but accumulated 0 tokens and 0.69s CPU time over 8+ minutes — blocked waiting for permission to call get_project_context.
Design: Three-Layer Permission System
Layer 1: --permission-mode acceptEdits
All agents (Archie + workers) should spawn with --permission-mode acceptEdits. This auto-approves standard file operations: Read, Edit, Write, Glob, Grep, NotebookEdit. These are foundational dev tools that every agent needs.
Layer 2: --allowedTools whitelist
Pre-approve specific tools per agent role via arch.yaml config + sensible defaults.
Default allowed tools for ALL agents:
mcp__arch__send_message
mcp__arch__get_messages
mcp__arch__update_status
mcp__arch__save_progress
mcp__arch__report_completion
Bash(git status)
Bash(git diff *)
Bash(git add *)
Bash(git commit *)
Bash(git log *)
Bash(git branch *)
Bash(git checkout *)
Additional defaults for Archie:
mcp__arch__spawn_agent
mcp__arch__teardown_agent
mcp__arch__list_agents
mcp__arch__escalate_to_user
mcp__arch__request_merge
mcp__arch__get_project_context
mcp__arch__close_project
mcp__arch__update_brief
mcp__arch__gh_create_issue
mcp__arch__gh_list_issues
mcp__arch__gh_close_issue
mcp__arch__gh_update_issue
mcp__arch__gh_add_comment
mcp__arch__gh_create_milestone
mcp__arch__gh_list_milestones
User-configurable overrides in arch.yaml:
archie:
persona: "personas/archie.md"
model: "claude-opus-4-5"
permissions:
allowed_tools: # extends defaults
- "Bash(npm *)"
- "Bash(python *)"
agent_pool:
- id: frontend
persona: "personas/frontend.md"
permissions:
allowed_tools: # extends defaults
- "Bash(npm *)"
- "Bash(npx *)"
- "Bash(node *)"
- id: backend
persona: "personas/backend.md"
permissions:
allowed_tools:
- "Bash(python *)"
- "Bash(pip *)"
- "Bash(pytest *)"
- id: qa
persona: "personas/qa.md"
permissions:
allowed_tools:
- "Bash(npm test *)"
- "Bash(pytest *)"
The allowed_tools list in config extends the defaults — users only need to add project-specific patterns, not repeat the base set.
Layer 3: --permission-prompt-tool for runtime delegation
For any tool NOT covered by layers 1 & 2, delegate to a new MCP tool that surfaces the request through the dashboard.
New MCP tool: handle_permission_request
This is NOT called by agents directly — it's called by the Claude CLI permission system when an agent attempts to use a tool that isn't pre-approved.
claude --print \
--permission-mode acceptEdits \
--allowedTools "mcp__arch__send_message" "Bash(git *)" ... \
--permission-prompt-tool mcp__arch__handle_permission_request \
...
Flow:
- Agent tries to use a tool not in
--allowedTools (e.g., Bash(curl https://api.example.com))
- Claude CLI calls
mcp__arch__handle_permission_request with the tool name and details
- MCP server receives the request, stores it as a pending decision (same pattern as
escalate_to_user)
- Dashboard shows:
⚠ frontend-dev-1 requests: Bash(curl https://api.example.com) [y]once [a]lways [n]o
- User responds with one of three options
- MCP tool returns the response to Claude CLI
- Agent continues (or gets denial)
MCP tool schema:
handle_permission_request
description: "Handle permission prompt from Claude CLI. Called automatically, not by agents directly."
params:
tool_name: string # e.g., "Bash"
tool_input: object # tool-specific input (e.g., {"command": "curl ..."})
agent_id: string # which agent is requesting (from SSE path)
returns:
{ allow: bool, message: string? }
access: system_only # Not visible to agents
"Always allow" runtime rules
When the user approves a permission request, they have three options:
y (once) — allow this one request, prompt again next time
a (always for this agent) — auto-approve this tool pattern for this agent for the rest of the session
n (deny) — deny the request
Implementation:
The MCP server maintains an in-memory dict[str, set[str]] mapping agent_id to approved tool patterns. A wildcard key "*" applies to all agents.
# In MCPServer
_runtime_allowed: dict[str, set[str]] = {} # agent_id -> {tool_patterns}
def _check_runtime_allowed(self, agent_id: str, tool_name: str) -> bool:
"""Check if tool was previously approved via 'always allow'."""
# Check agent-specific rules
if agent_id in self._runtime_allowed:
if tool_name in self._runtime_allowed[agent_id]:
return True
# Check wildcard rules
if "*" in self._runtime_allowed:
if tool_name in self._runtime_allowed["*"]:
return True
return False
When handle_permission_request is called:
- Check
_runtime_allowed first — if match, return {allow: true} immediately (no prompt)
- Otherwise, create pending decision and block until user responds
- If user chose "always", add
tool_name to _runtime_allowed[agent_id]
Rules are session-scoped (in-memory only). A fresh arch up starts with a clean slate. This is intentional — persistent permission grants belong in arch.yaml's allowed_tools config, not in runtime state. If a user finds themselves always-allowing the same tool, that's a signal to add it to their config.
Dashboard UI:
Permission requests appear in the escalation bar with three response options:
⚠ PERMISSION: frontend-dev-1 wants to use Bash(curl https://api.example.com) [y]once [a]lways [n]o: _
The existing answer_escalation() method needs to pass through the user's choice (y/a/n) so the MCP handler can distinguish "allow once" from "always allow".
Implementation
Changes required:
1. Config (orchestrator.py)
- Add
allowed_tools: list[str] to PermissionsConfig
- Add
allowed_tools to ArchieConfig (Archie also gets configurable permissions)
- Parse
permissions.allowed_tools from arch.yaml
- Define
DEFAULT_ALLOWED_TOOLS_ALL, DEFAULT_ALLOWED_TOOLS_ARCHIE constants
2. Session spawn (session.py)
- Add
allowed_tools: list[str] and permission_prompt_tool: Optional[str] to AgentConfig
- In
Session.spawn(), build the --allowedTools args from config
- Add
--permission-mode acceptEdits to all agent commands
- Add
--permission-prompt-tool mcp__arch__handle_permission_request to all agent commands
3. MCP server (mcp_server.py)
- Add
handle_permission_request tool definition (with system_only access — not visible to agents)
- Add
_runtime_allowed: dict[str, set[str]] for session-scoped "always allow" rules
- Implement handler: check
_runtime_allowed first → auto-approve if match → otherwise create pending decision and block
- On user response: if "always", add tool to
_runtime_allowed[agent_id]
- Re-use the
escalate_to_user blocking pattern with asyncio.Event
4. Dashboard (dashboard.py)
- Permission requests appear via the pending decisions mechanism
- Change escalation UI to show
[y]once [a]lways [n]o for permission-type decisions
- Pass user choice (y/a/n) through
answer_escalation() so MCP handler can distinguish
- Differentiate permission requests from general escalations (e.g.,
⚠ PERMISSION: prefix vs ⚠ ARCHIE ASKS:)
5. Orchestrator (orchestrator.py)
_spawn_archie(): Build allowed_tools list from defaults + archie config
_handle_spawn_agent(): Build allowed_tools list from defaults + pool entry config
- Pass
allowed_tools and permission_prompt_tool through to AgentConfig
CLI command changes:
The claude subprocess command changes from:
claude --model MODEL --output-format stream-json --mcp-config CONFIG --print PROMPT
To:
claude --model MODEL --output-format stream-json --mcp-config CONFIG --print \
--permission-mode acceptEdits \
--allowedTools "mcp__arch__send_message" "mcp__arch__get_messages" ... "Bash(git *)" \
--permission-prompt-tool mcp__arch__handle_permission_request \
PROMPT
Testing
- Test that
allowed_tools are correctly built from defaults + config overrides
- Test that
--allowedTools flags are properly formatted in the subprocess command
- Test that
--permission-prompt-tool is included in the command
- Test
handle_permission_request MCP handler creates pending decision and blocks
- Test that permission request + user response flows correctly
- Test
_runtime_allowed auto-approves after "always allow" without creating a pending decision
- Test that runtime rules are agent-scoped (agent A's "always" doesn't affect agent B)
- Test that
skip_permissions still works and overrides everything (uses --dangerously-skip-permissions)
Priority
P0 — This blocks all agent execution. Without this fix, no agent can do any work.
Problem
This blocks all agent execution. When agents are spawned with
--print --output-format stream-json, the Claude CLI still prompts for tool permissions. Since agents run as background subprocesses with no TTY, they block indefinitely waiting for input that never comes.Discovered during first UAT: Archie spawned but accumulated 0 tokens and 0.69s CPU time over 8+ minutes — blocked waiting for permission to call
get_project_context.Design: Three-Layer Permission System
Layer 1:
--permission-mode acceptEditsAll agents (Archie + workers) should spawn with
--permission-mode acceptEdits. This auto-approves standard file operations: Read, Edit, Write, Glob, Grep, NotebookEdit. These are foundational dev tools that every agent needs.Layer 2:
--allowedToolswhitelistPre-approve specific tools per agent role via
arch.yamlconfig + sensible defaults.Default allowed tools for ALL agents:
Additional defaults for Archie:
User-configurable overrides in
arch.yaml:The
allowed_toolslist in config extends the defaults — users only need to add project-specific patterns, not repeat the base set.Layer 3:
--permission-prompt-toolfor runtime delegationFor any tool NOT covered by layers 1 & 2, delegate to a new MCP tool that surfaces the request through the dashboard.
New MCP tool:
handle_permission_requestThis is NOT called by agents directly — it's called by the Claude CLI permission system when an agent attempts to use a tool that isn't pre-approved.
Flow:
--allowedTools(e.g.,Bash(curl https://api.example.com))mcp__arch__handle_permission_requestwith the tool name and detailsescalate_to_user)⚠ frontend-dev-1 requests: Bash(curl https://api.example.com) [y]once [a]lways [n]oMCP tool schema:
"Always allow" runtime rules
When the user approves a permission request, they have three options:
y(once) — allow this one request, prompt again next timea(always for this agent) — auto-approve this tool pattern for this agent for the rest of the sessionn(deny) — deny the requestImplementation:
The MCP server maintains an in-memory
dict[str, set[str]]mapping agent_id to approved tool patterns. A wildcard key"*"applies to all agents.When
handle_permission_requestis called:_runtime_allowedfirst — if match, return{allow: true}immediately (no prompt)tool_nameto_runtime_allowed[agent_id]Rules are session-scoped (in-memory only). A fresh
arch upstarts with a clean slate. This is intentional — persistent permission grants belong inarch.yaml'sallowed_toolsconfig, not in runtime state. If a user finds themselves always-allowing the same tool, that's a signal to add it to their config.Dashboard UI:
Permission requests appear in the escalation bar with three response options:
The existing
answer_escalation()method needs to pass through the user's choice (y/a/n) so the MCP handler can distinguish "allow once" from "always allow".Implementation
Changes required:
1. Config (
orchestrator.py)allowed_tools: list[str]toPermissionsConfigallowed_toolstoArchieConfig(Archie also gets configurable permissions)permissions.allowed_toolsfromarch.yamlDEFAULT_ALLOWED_TOOLS_ALL,DEFAULT_ALLOWED_TOOLS_ARCHIEconstants2. Session spawn (
session.py)allowed_tools: list[str]andpermission_prompt_tool: Optional[str]toAgentConfigSession.spawn(), build the--allowedToolsargs from config--permission-mode acceptEditsto all agent commands--permission-prompt-tool mcp__arch__handle_permission_requestto all agent commands3. MCP server (
mcp_server.py)handle_permission_requesttool definition (withsystem_onlyaccess — not visible to agents)_runtime_allowed: dict[str, set[str]]for session-scoped "always allow" rules_runtime_allowedfirst → auto-approve if match → otherwise create pending decision and block_runtime_allowed[agent_id]escalate_to_userblocking pattern withasyncio.Event4. Dashboard (
dashboard.py)[y]once [a]lways [n]ofor permission-type decisionsanswer_escalation()so MCP handler can distinguish⚠ PERMISSION:prefix vs⚠ ARCHIE ASKS:)5. Orchestrator (
orchestrator.py)_spawn_archie(): Buildallowed_toolslist from defaults + archie config_handle_spawn_agent(): Buildallowed_toolslist from defaults + pool entry configallowed_toolsandpermission_prompt_toolthrough toAgentConfigCLI command changes:
The
claudesubprocess command changes from:To:
Testing
allowed_toolsare correctly built from defaults + config overrides--allowedToolsflags are properly formatted in the subprocess command--permission-prompt-toolis included in the commandhandle_permission_requestMCP handler creates pending decision and blocks_runtime_allowedauto-approves after "always allow" without creating a pending decisionskip_permissionsstill works and overrides everything (uses--dangerously-skip-permissions)Priority
P0 — This blocks all agent execution. Without this fix, no agent can do any work.