SuperAPI AI Directory

tool-permission-system

General & other

View repository

Updated 10-16 05:41

About this skill

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)

Outer wrapper(applied after the entire pipeline):

  • dontAsk Mode: convert all ask to deny(for non-interactive background agents)
  • auto Mode: pass all ask to the AI classifier for judgment, instead of interrupting the user
  • headless Mode: 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:

  1. Which operations never need confirmation? → put them in allow rules (such as read-only operations)
  2. Which operations always need confirmation and cannot be bypassed? → use decisionReason.type === 'safetyCheck' to mark them
  3. 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