| name | tool-permission-system |
| description | Design 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 configurable across multiple scopes (project/user/enterprise) and extensible via hooks. Triggers on: "权限系统", "工具安全", "tool permission", "permission system", "tool safety", "allow/deny rules", "hook system", "构建安全机制". |
Tool Permission System
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)
外层包装(作用于整条流水线之后):
dontAsk 模式:把所有 ask 转为 deny(用于无交互的后台 agent)
auto 模式:把所有 ask 转给 AI 分类器判断,而不是打断用户
headless 模式:先跑 PermissionRequest hooks,hooks 没回应就自动 deny
Workflow
1. 定义三种决策行为
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. 建立分层规则来源
规则来源按优先级从高到低排列:
policySettings ← 企业管理员,用户不可覆盖
userSettings ← 用户全局 (~/.agent/settings.json)
projectSettings ← 项目级 (.agent/settings.json,可提交 git)
localSettings ← 本地私有 (.agent/settings.local.json)
cliArg ← 启动参数
command ← 运行时命令
session ← 当次会话临时
每条规则的格式:ToolName 或 ToolName(content)。
3. 实现权限决策函数
async function hasPermission(tool, input, context): Promise<PermissionDecision> {
const denyRule = findMatchingRule(context.denyRules, tool, input)
if (denyRule) return { behavior: 'deny', message: '...', decisionReason: { type: 'rule', rule: denyRule } }
const askRule = findMatchingRule(context.askRules, tool, input)
if (askRule) return { behavior: 'ask', message: '...' }
const toolResult = await tool.checkPermissions(input, context)
if (toolResult.behavior === 'deny') return toolResult
if (toolResult.behavior === 'ask' && toolResult.decisionReason?.type === 'rule') return toolResult
if (toolResult.behavior === 'ask' && toolResult.decisionReason?.type === 'safetyCheck') return toolResult
if (context.mode === 'bypassPermissions') return { behavior: 'allow', updatedInput: input }
const allowRule = findMatchingRule(context.allowRules, tool, input)
if (allowRule) return { behavior: 'allow', updatedInput: input }
return { behavior: 'ask', message: `Agent requested to use ${tool.name}` }
}
4. 为每个工具定义安全属性接口(fail-closed 默认值)
工具与权限系统的接合点是工具接口上的一组安全属性。关键设计:所有属性都遵循失败关闭(fail-closed)——开发者不声明时,系统按"最保守"假设处理,必须主动声明"我是安全的"才放宽。
const TOOL_DEFAULTS = {
isEnabled: () => true,
isConcurrencySafe: () => false,
isReadOnly: () => false,
isDestructive: () => false,
checkPermissions: (input) => ({ behavior: 'allow', updatedInput: input }),
}
function buildTool(def) { return { ...TOOL_DEFAULTS, ...def } }
| 属性 | 返回 | 谁来问 / 影响什么 |
|---|
isReadOnly(input) | boolean | 权限系统:只读操作可绕过部分限制 |
isDestructive(input) | boolean | 权限系统:不可逆操作需更严格确认 |
isConcurrencySafe(input) | boolean | Agent Loop:能否与其他工具并发执行(默认 false → 串行) |
checkPermissions(input, ctx) | PermissionResult | 权限系统:工具专属权限逻辑(流水线 1c) |
validateInput(input, ctx) | ValidationResult | Agent Loop:执行前的输入合法性校验 |
checkPermissions 在流水线里的位置是"夹心结构":通用 deny/ask 规则在它之前(且 bypass 也拦不住),通用 allow 白名单在它之后。所以工具自检既挡不住企业 deny,也不必重复实现通用 allow——只管工具特有的逻辑:
class MyTool implements Tool {
isReadOnly = () => false
isConcurrencySafe = () => false
async checkPermissions(input, context): Promise<PermissionResult> {
const allowRules = getRuleContentsForTool(context, this, 'allow')
if (allowRules.has(getCommandPrefix(input.command))) {
return { behavior: 'allow' }
}
if (isDangerousPath(input.path)) {
return {
behavior: 'ask',
message: '...',
decisionReason: { type: 'safetyCheck', reason: '...', classifierApprovable: false }
}
}
return { behavior: 'passthrough', message: '...' }
}
}
危险路径黑名单(safetyCheck)是一份硬编码的敏感文件/目录清单,即使 bypass / acceptEdits / 配了 allow 规则也强制弹框,防两类攻击:① 代码执行(.git/ hooks、.bashrc/.zshrc 等 shell 启动脚本、.vscode/.idea 任务配置);② AI 改自己的护栏(.claude/、.mcp.json、.claude.json —— agent 不能通过"正常编辑文件"给自己提权)。完整清单见 references/dangerous-patterns.ts。
5. 实现 Hook 系统(可选但推荐)
Hook 让用户/企业在工具生命周期各节点插入自定义逻辑:
{
"hooks": {
"PreToolUse": [{
"matcher": "MyTool",
"hooks": [{
"type": "command",
"command": "check-safety.sh $TOOL_INPUT"
}]
}],
"PostToolUse": [{
"matcher": "FileEdit",
"hooks": [{ "type": "command", "command": "prettier --write $FILE_PATH" }]
}]
}
}
Hook 执行结果影响权限决策:
- exit 0 → 通过
- exit 2 → block(工具不执行)
- stdout 包含 JSON
{"action": "allow"} → 覆盖决策
6. 实现 Denial 追踪(AI 分类器场景)
当使用 AI 分类器自动判断权限时,需要 circuit breaker 防止分类器过于严格:
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
设计时必须明确的三个问题:
- 哪些操作永远不需要确认? → 放入 allow rules(如只读操作)
- 哪些操作永远需要确认,不可绕过? → 用
decisionReason.type === 'safetyCheck' 标记
- 无人值守场景(CI/后台 agent)怎么处理? →
shouldAvoidPermissionPrompts = true + 跑 hooks + 自动 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'
}
Boundaries
This skill owns:
- 权限决策流水线的设计与实现
- 分层规则来源(policySettings → session)的优先级架构
- 工具级安全属性接口设计(
isReadOnly / isDestructive / isConcurrencySafe / checkPermissions + fail-closed 默认值)
- Hook 系统的配置格式和生命周期事件
- AI 分类器 + Denial 追踪的 circuit breaker 模式
- 危险操作硬编码黑名单的设计原则
This skill does not own:
- 具体工具的业务逻辑(只关注权限接口)
- UI 确认弹框的实现(只关注决策结果)
- 用户认证/身份校验(不同于工具权限)
- AI 分类器的具体 prompt 工程
When More Detail Is Needed
- 完整 TypeScript 类型定义 →
references/permission-types.ts
- 决策流水线带注释的详细实现 →
references/permission-pipeline.md
- Denial 追踪 circuit breaker 完整代码 →
references/denial-tracking.ts
- Hook 系统架构与所有事件类型 →
references/hook-system.md
- settings.json 配置完整示例 →
references/settings-examples.json