Every tool call passes through one gate: setting.HasPermissionToUseTool.
This page documents the inputs, the decision pipeline, and how the
foreground TUI, subagents, and plan-mode differ.
For the Claude-Code-compatible rule syntax see
reference/claude-permission-compat.md.
| Term | Meaning |
|---|---|
| Behavior | The intent: allow, deny, or ask. |
| Decision | Behavior + reason + suggested rule edits. The runtime returns one of these per call. |
| Mode | Session-wide policy: default, acceptEdits, bypassPermissions, plan. |
| Rule | A pattern matched against toolName + args. E.g. Bash(git status:*), Write(./src/**). |
| Session permissions | Per-session rules accumulated from hook responses and approval modals. Reset on session end. |
Five sources are consulted in this order; the first to produce a
non-ask behavior wins:
- Settings rules —
settings.jsonpermissions.allow / deny / ask, merged across user and project tiers. - Session permissions — rules added during the run (via approval modals or hook updates).
- Hook responses —
PermissionRequesthook (if any) may forceallowordeny, or rewrite the request. - Mode policy —
bypassPermissionsforces allow;planforces deny for any write-class tool. - Default —
ask.
The pipeline lives in internal/setting/permission.go. Bash gets special
treatment: bash_ast.go parses the command and matches per-argv patterns
(Bash(git status:*) allows git status -uall but not git push).
The decision pipeline is identical for the foreground agent and for
subagents. The single point of divergence is: when behavior is ask, can
we surface a modal to the user?
- Foreground: yes.
agent.PermissionBridgesynchronously waits for the TUI approval, then routes the answer back into the running tool call. - Subagent: no. There's no user attached to the subagent's loop.
Subagents resolve
askaccording to theirpermission_mode:bypassPermissions(the explore agent) — treat as allowplan— treat as deny + emit a "would have asked" record- default — treat as deny
This is implemented as one flag on the request, not by duplicating logic on the subagent side.
A read-only conversation. Write-class tools (Write, Edit,
NotebookEdit, certain Bash patterns) are auto-denied with a synthetic
reason "plan mode: read-only". The user can exit plan mode with
/plan-off or by approving an ExitPlanMode tool call.
Plan mode is a policy filter, not a separate code path. The same
HasPermissionToUseTool returns deny(plan-mode) early.
The PermissionRequest hook fires before the modal is shown (or before
the auto-deny in subagent mode). The hook can:
- Force
allowordenyfor this single call. - Append session-scope rules to be applied to subsequent calls.
- Switch the mode (e.g. flip to
acceptEditsfor the rest of the session). - Rewrite the tool args (e.g. canonicalize a path).
See packages/hook.md for the request/response
shape, and PermissionUpdate in internal/hook/types.go for the
mutation payload.
- Decision gate:
internal/setting/permission.go→HasPermissionToUseTool. - Rule parser + Bash AST:
internal/setting/bash_ast.go. - Approval modal flow:
internal/agent/permission.go(PermissionBridge). - Subagent permission resolution:
internal/subagent/executor.go. - Hook integration:
internal/hook/engine.go→getPermissionRequestOutcome.