$npx -y skills add simbajigege/book2skills --skill tool-permission-systemDesign and implement a layered, configurable permission/safety system for agent tools. Use this skill when building an agent that needs to control which tool calls are auto-allowed, which require user confirmation, and which are denied — especially when the system must be configu
| 1 | # Tool Permission System |
| 2 | |
| 3 | ## Core Idea |
| 4 | |
| 5 | 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. |
| 6 | |
| 7 | ``` |
| 8 | Tool call request |
| 9 | ↓ |
| 10 | [硬否决] Deny rules → immediate deny |
| 11 | ↓ |
| 12 | [强制确认] Ask rules → force prompt (even in bypass mode) |
| 13 | ↓ |
| 14 | [工具自身] Tool's checkPermissions() → tool-specific logic |
| 15 | ↓ |
| 16 | [安全绕过免疫] Safety checks (.git/, .claude/, shell configs) → prompt, immune to bypass |
| 17 | ↓ |
| 18 | [模式快速通过] Bypass / acceptEdits mode → immediate allow |
| 19 | ↓ |
| 20 | [白名单] Allow rules → immediate allow |
| 21 | ↓ |
| 22 | [默认] passthrough → prompt user (ask) |
| 23 | ``` |
| 24 | |
| 25 | **外层包装**(作用于整条流水线之后): |
| 26 | - `dontAsk` 模式:把所有 `ask` 转为 `deny`(用于无交互的后台 agent) |
| 27 | - `auto` 模式:把所有 `ask` 转给 AI 分类器判断,而不是打断用户 |
| 28 | - `headless` 模式:先跑 PermissionRequest hooks,hooks 没回应就自动 deny |
| 29 | |
| 30 | ## Workflow |
| 31 | |
| 32 | ### 1. 定义三种决策行为 |
| 33 | |
| 34 | ```typescript |
| 35 | type PermissionBehavior = 'allow' | 'deny' | 'ask' |
| 36 | |
| 37 | type PermissionDecision = |
| 38 | | { behavior: 'allow'; updatedInput?: unknown; decisionReason?: DecisionReason } |
| 39 | | { behavior: 'ask'; message: string; suggestions?: PermissionUpdate[] } |
| 40 | | { behavior: 'deny'; message: string; decisionReason: DecisionReason } |
| 41 | ``` |
| 42 | |
| 43 | ### 2. 建立分层规则来源 |
| 44 | |
| 45 | 规则来源按优先级从高到低排列: |
| 46 | |
| 47 | ``` |
| 48 | policySettings ← 企业管理员,用户不可覆盖 |
| 49 | userSettings ← 用户全局 (~/.agent/settings.json) |
| 50 | projectSettings ← 项目级 (.agent/settings.json,可提交 git) |
| 51 | localSettings ← 本地私有 (.agent/settings.local.json) |
| 52 | cliArg ← 启动参数 |
| 53 | command ← 运行时命令 |
| 54 | session ← 当次会话临时 |
| 55 | ``` |
| 56 | |
| 57 | 每条规则的格式:`ToolName` 或 `ToolName(content)`。 |
| 58 | |
| 59 | ### 3. 实现权限决策函数 |
| 60 | |
| 61 | ```typescript |
| 62 | async function hasPermission(tool, input, context): Promise<PermissionDecision> { |
| 63 | // Step 1: deny rules (优先级最高,含企业强制) |
| 64 | const denyRule = findMatchingRule(context.denyRules, tool, input) |
| 65 | if (denyRule) return { behavior: 'deny', message: '...', decisionReason: { type: 'rule', rule: denyRule } } |
| 66 | |
| 67 | // Step 2: ask rules (强制弹框,绕过模式也无法跳过) |
| 68 | const askRule = findMatchingRule(context.askRules, tool, input) |
| 69 | if (askRule) return { behavior: 'ask', message: '...' } |
| 70 | |
| 71 | // Step 3: 工具自身的 checkPermissions() |
| 72 | const toolResult = await tool.checkPermissions(input, context) |
| 73 | if (toolResult.behavior === 'deny') return toolResult |
| 74 | if (toolResult.behavior === 'ask' && toolResult.decisionReason?.type === 'rule') return toolResult // ask rule 免疫 bypass |
| 75 | if (toolResult.behavior === 'ask' && toolResult.decisionReason?.type === 'safetyCheck') return toolResult // 安全检查免疫 bypass |
| 76 | |
| 77 | // Step 4: bypass 模式快速通过 |
| 78 | if (context.mode === 'bypassPermissions') return { behavior: 'allow', updatedInput: input } |
| 79 | |
| 80 | // Step 5: allow rules 白名单 |
| 81 | const allowRule = findMatchingRule(context.allowRules, tool, input) |
| 82 | if (allowRule) return { behavior: 'allow', updatedInput: input } |
| 83 | |
| 84 | // Step 6: 默认转 ask |
| 85 | return { behavior: 'ask', message: `Agent requested to use ${tool.name}` } |
| 86 | } |
| 87 | ``` |
| 88 | |
| 89 | ### 4. 为每个工具定义安全属性接口(fail-closed 默认值) |
| 90 | |
| 91 | 工具与权限系统的接合点是工具接口上的一组**安全属性**。关键设计:所有属性都遵循**失败关闭(fail-closed)**——开发者不声明时,系统按"最保守"假设处理,必须主动声明"我是安全的"才放宽。 |
| 92 | |
| 93 | ```typescript |
| 94 | // 工厂函数用 TOOL_DEFAULTS 填充未声明的属性 |
| 95 | const TOOL_DEFAULTS = { |
| 96 | isEnabled: () => true, |
| 97 | isConcurrencySafe: () => false, // 默认不并发(怕数据竞争) |
| 98 | isReadOnly: () => false, // 默认假设会写入 |
| 99 | isDestructive: () => false, // 默认假设不可逆操作要谨慎 |
| 100 | checkPermissions: (input) => ({ behavior: 'allow', updatedInput: input }), // 默认交给中央权限系统 |
| 101 | } |
| 102 | function buildTool(def) { return { ...TOOL_DEFAULTS, ...def } } |
| 103 | ``` |
| 104 | |
| 105 | | 属性 | 返回 | 谁来问 / 影响什么 | |
| 106 | |---|---|---| |
| 107 | | `isReadOnly(input)` | boolean | 权限系统:只读操作可绕过部分限制 | |
| 108 | | `isDestructive(input)` | boolean | 权限系统:不可逆操作需更严格确认 | |
| 109 | | `isConcurrencySafe(input)` | boolean | Agent Loop:能否与其他工具并发执行(默认 false → 串行) | |
| 110 | | `checkPermissions(input, ctx)` | PermissionResult | 权限系统:工具专属权限逻辑(流水线 1c) | |
| 111 | | `validateInput(input, ctx)` | ValidationResult | Agent Loop:执行前的输入合法性校验 | |
| 112 | |
| 113 | **`checkPermissions` 在流水线里的位置是"夹心结构"**:通用 deny/ask 规则在它**之前**(且 bypass 也拦不住),通用 allow 白名单在它**之后**。所以工具自检既挡不住企业 deny,也不必重复实现通用 allow——只管工具特有的逻辑: |
| 114 | |
| 115 | ```typescript |
| 116 | class MyTool implements Tool { |
| 117 | isReadOnly = () => false |
| 118 | isConcurrencySafe = () => false |
| 119 | |
| 120 | async checkPermissions(input, context): Promise<PermissionResult> { |
| 121 | // 检查工具特定规则(如 Bash 检查具体命令前缀) |
| 122 | const allowRules = getRuleContentsForTool(context, this, 'allow') |
| 123 | if (allowRules.has(getCommandPrefix(input.command))) { |
| 124 | return { behavior: 'allow' } |
| 125 | } |
| 126 | |
| 127 | // 检查危险路径 |