Core Idea
Every time an agent calls a tool, a permission pipeline runs before execution. This pipeline is the single place that decides: auto-allow, ask the user, or deny. The pipeline is layered — different stakeholders (enterprise admin, user, project team, session) can each contribute rules, with higher layers overriding lower ones.
Tool call request
↓
[硬否决] Deny rules → immediate deny
↓
[强制确认] Ask rules → force prompt (even in bypass mode)
↓
[工具自身] Tool's checkPermissions() → tool-specific logic
↓
[安全绕过免疫] Safety checks (.git/, .claude/, shell configs) → prompt, immune to bypass
↓
[模式快速通过] Bypass / acceptEdits mode → immediate allow
↓
[白名单] Allow rules → immediate allow
↓
[默认] passthrough → prompt user (ask)
Outer wrapper(applied after the entire pipeline):
dontAskMode: convert allasktodeny(for non-interactive background agents)autoMode: pass allaskto the AI classifier for judgment, instead of interrupting the userheadlessMode: run PermissionRequest hooks first; if the hooks do not respond, automatically deny
Workflow
1. Define three decision behaviors
type PermissionBehavior = 'allow' | 'deny' | 'ask'
type PermissionDecision =
| { behavior: 'allow'; updatedInput?: unknown; decisionReason?: DecisionReason }
| { behavior: 'ask'; message: string; suggestions?: PermissionUpdate[] }
| { behavior: 'deny'; message: string; decisionReason: DecisionReason }
2. Establish layered rule sources
Rule sources are ordered from highest to lowest priority:
policySettings ← 企业管理员,用户不可覆盖
userSettings ← 用户全局 (~/.agent/settings.json)
projectSettings ← 项目级 (.agent/settings.json,可提交 git)
localSettings ← 本地私有 (.agent/settings.local.json)
cliArg ← 启动参数
command ← 运行时命令
session ← 当次会话临时
Format of each rule:ToolName or ToolName(content)。
3. Implement the permission decision function
async function hasPermission(tool, input, context): Promise<PermissionDecision> {
// Step 1: deny rules (优先级最高,含企业强制)
const denyRule = findMatchingRule(context.denyRules, tool, input)
if (denyRule) return { behavior: 'deny', message: '...', decisionReason: { type: 'rule', rule: denyRule } }
// Step 2: ask rules (强制弹框,绕过模式也无法跳过)
const askRule = findMatchingRule(context.askRules, tool, input)
if (askRule) return { behavior: 'ask', message: '...' }
// Step 3: 工具自身的 checkPermissions()
const toolResult = await tool.checkPermissions(input, context)
if (toolResult.behavior === 'deny') return toolResult
if (toolResult.behavior === 'ask' && toolResult.decisionReason?.type === 'rule') return toolResult // ask rule 免疫 bypass
if (toolResult.behavior === 'ask' && toolResult.decisionReason?.type === 'safetyCheck') return toolResult // 安全检查免疫 bypass
// Step 4: bypass 模式快速通过
if (context.mode === 'bypassPermissions') return { behavior: 'allow', updatedInput: input }
// Step 5: allow rules 白名单
const allowRule = findMatchingRule(context.allowRules, tool, input)
if (allowRule) return { behavior: 'allow', updatedInput: input }
// Step 6: 默认转 ask
return { behavior: 'ask', message: `Agent requested to use ${tool.name}` }
}
4. Define a safety attribute interface for each tool (fail-closed defaults)
The junction between tools and the permission system is a set ofsafety attributeson the tool interface. Key design: all attributes followfail-closed—when the developer does not declare them, the system treats them under the "most conservative" assumption, and must actively declare "I am safe" to relax them.
// 工厂函数用 TOOL_DEFAULTS 填充未声明的属性
const TOOL_DEFAULTS = {
isEnabled: () => true,
isConcurrencySafe: () => false, // 默认不并发(怕数据竞争)
isReadOnly: () => false, // 默认假设会写入
isDestructive: () => false, // 默认假设不可逆操作要谨慎
checkPermissions: (input) => ({ behavior: 'allow', updatedInput: input }), // 默认交给中央权限系统
}
function buildTool(def) { return { ...TOOL_DEFAULTS, ...def } }
| Attribute | Returns | Who asks / what it affects |
|---|---|---|
isReadOnly(input) |
boolean | Permission system: read-only operations can bypass some restrictions |
isDestructive(input) |
boolean | Permission system: irreversible operations require stricter confirmation |
isConcurrencySafe(input) |
boolean | Agent Loop: whether it can execute concurrently with other tools (default false → serial) |
checkPermissions(input, ctx) |
PermissionResult | Permission system: tool-specific permission logic (pipeline 1c) |
validateInput(input, ctx) |
ValidationResult | Agent Loop: input validity check before execution |
checkPermissions 's position in the pipeline is a "sandwich structure": general deny/ask rules arebeforeit (and bypass cannot stop them either), and the general allow whitelist isafterit. Therefore tool self-checks can neither block enterprise deny nor need to reimplement general allow—they only handle tool-specific logic:
class MyTool implements Tool {
isReadOnly = () => false
isConcurrencySafe = () => false
async checkPermissions(input, context): Promise<PermissionResult> {
// 检查工具特定规则(如 Bash 检查具体命令前缀)
const allowRules = getRuleContentsForTool(context, this, 'allow')
if (allowRules.has(getCommandPrefix(input.command))) {
return { behavior: 'allow' }
}
// 检查危险路径(命中后 type:'safetyCheck' → bypass 也拦不住,见下方说明)
if (isDangerousPath(input.path)) {
return {
behavior: 'ask',
message: '...',
decisionReason: { type: 'safetyCheck', reason: '...', classifierApprovable: false }
}
}
return { behavior: 'passthrough', message: '...' } // 没意见 → 交给外层
}
}
Dangerous path blacklist(safetyCheck) is a hardcoded list of sensitive files/directories that forces a prompt even if bypass / acceptEdits / allow rules are configured, preventing two types of attacks: ① code execution (.git/ hooks、.bashrc/.zshrc and other shell startup scripts,.vscode/.idea task configuration); ② AI modifying its own guardrails (.claude/、.mcp.json、.claude.json — the agent cannot escalate its own privileges through "normal file editing"). See the full list in references/dangerous-patterns.ts。
5. Implement the Hook system (optional but recommended)
Hooks let users/enterprises insert custom logic at various points in the tool lifecycle:
// 配置格式(settings.json)
{
"hooks": {
"PreToolUse": [{
"matcher": "MyTool", // 可选,工具名过滤
"hooks": [{
"type": "command", // command | prompt | agent | http
"command": "check-safety.sh $TOOL_INPUT"
}]
}],
"PostToolUse": [{
"matcher": "FileEdit",
"hooks": [{ "type": "command", "command": "prettier --write $FILE_PATH" }]
}]
}
}
Hook execution results affect permission decisions:
- exit 0 → pass
- exit 2 → block (tool does not execute)
- stdout contains JSON
{"action": "allow"}→ override decision
6. Implement Denial tracking (AI classifier scenario)
When using an AI classifier to automatically determine permissions, a circuit breaker is needed to prevent the classifier from being too strict:
// 连续拒绝 3 次或累计拒绝 20 次 → 回退到人工确认
const DENIAL_LIMITS = { maxConsecutive: 3, maxTotal: 20 }
function shouldFallback(state: DenialTrackingState): boolean {
return (
state.consecutiveDenials >= DENIAL_LIMITS.maxConsecutive ||
state.totalDenials >= DENIAL_LIMITS.maxTotal
)
}
Required Decisions
Three questions that must be clarified during design:
- Which operations never need confirmation? → put them in allow rules (such as read-only operations)
- Which operations always need confirmation and cannot be bypassed? → use
decisionReason.type === 'safetyCheck'to mark them - How to handle unattended scenarios (CI/background agents)? →
shouldAvoidPermissionPrompts = true+ run hooks + automatically deny
Minimal Pattern
// 最简实现:三层规则 + 工具自检
type Rule = { toolName: string; content?: string; behavior: 'allow' | 'deny' | 'ask' }
type PermissionContext = {
mode: 'default' | 'bypassPermissions' | 'acceptEdits'
allowRules: Rule[]
denyRules: Rule[]
askRules: Rule[]
}
async function checkPermission(toolName: string, input: unknown, ctx: PermissionContext) {
if (ctx.denyRules.some(r => matches(r, toolName, input))) return 'deny'
if (ctx.askRules.some(r => matches(r, toolName, input))) return 'ask'
if (ctx.mode === 'bypassPermissions') return 'allow'
if (ctx.allowRules.some(r => matches(r, toolName, input))) return 'allow'
return 'ask' // default: prompt
}
Boundaries
This skill owns:
- Design and implementation of the permission decision pipeline
- Priority architecture of layered rule sources (policySettings → session)
- Tool-level security attribute interface design (
isReadOnly/isDestructive/isConcurrencySafe/checkPermissions+ fail-closed default values) - Hook system configuration format and lifecycle events
- Circuit breaker pattern with AI classifier + Denial tracking
- Design principles of hardcoded blacklist for dangerous operations
This skill does not own:
- Business logic of specific tools (focus only on permission interface)
- Implementation of UI confirmation dialog (focus only on decision result)
- User authentication/identity verification (different from tool permissions)
- Specific prompt engineering of the AI classifier
When More Detail Is Needed
- Complete TypeScript type definitions →
references/permission-types.ts - Detailed implementation of decision pipeline with comments →
references/permission-pipeline.md - Complete code of Denial tracking circuit breaker →
references/denial-tracking.ts - Hook system architecture and all event types →
references/hook-system.md - Complete example of settings.json configuration →
references/settings-examples.json