Skip to content
源码分析手册

PermissionRequest:权限弹窗前的自定义裁决与远端旁路

在 Claude Code 的安全模型中,PermissionRequest 钩子是一个特殊的存在。它允许你介入“系统决定向用户发起权限询问”的那一刻,从而实现比内建模式更细粒度的自定义审批逻辑,甚至支持跨设备的远端审批。

它解决了什么问题

PermissionRequest 本质上是 审批 UI 的逻辑劫持器(Approval UI Hijacker)

当权限引擎(claude-code-opensource/src/utils/permissions/permissions.ts)发现一个操作既不属于自动放行范围,也不属于已记录的拒绝范围时,它通常会弹出一个终端对话框。该钩子允许开发者在弹窗“震动用户”前,先用代码逻辑跑一遍自定义审计。

它同时也是 MCP 审批旁路(MCP Approval Relay) 的入口:通过该钩子,你可以将本地 CLI 的权限弹窗外接到 Slack、Discord 或自定义的管理后台。

运行时的真相

该钩子的触发流程位于权限引擎的决策中枢:

  1. 拦截询问信号:在 permissions.ts 确定需要发起 Ask 决策时,它会首先检查是否存在订阅了该工具的 PermissionRequest 钩子。
  2. 提供深度元数据PermissionRequestHookInput 包含工具、参数,以及权限系统生成的 suggestedRules(建议添加的规则)。这让钩子可以感知到系统打算如何“学习”这一权限。
  3. 自定义裁决(Final Decision):钩子可以返回 HookJSONOutput 来改写系统的决定:
    • 提前放行(Approve):如果你的钩子分析后认为该调用(即使它看起来像越界)是安全的,系统将不再弹出对话框。
    • 提前拒绝(Deny):如果你的钩子检测到了黑名单敏感信息,用户甚至不会被打扰,操作会直接失败。
    • 继续询问(Passthrough):让系统继续走默认的终端弹窗流程。
  4. 远端审批旁路(Relay Prompts): 对于启用了 claude/channel/permission 能力的 MCP 客户端,系统会并行启动一条旁路:
    • 生成一个短请求 ID(如 abcde)并裁剪工具输入预览。
    • 向合格的 Channel 发送结构化的 permission_request
    • 竞速裁决:本地 UI、Bridge 和 Channel Relay 处于竞争状态,谁先给出 allow/deny,谁就赢得最终决策。

使用时的关键约束

  • DontAsk 模式的阻断:在 dontAsk 模式下,这个钩子会被完全跳过,因为系统会直接将所有询问收敛为拒绝。
  • 非交互式限制:Hook 脚本本身是在后台运行的。你不应该尝试在钩子代码里自己去读取 stdin 或弹出窗口,这会与系统的 Ink 渲染器发生冲突。
  • 规则库优先级:如果规则库(settings.json)中已经存了 allowdeny,系统会优先使用规则库,而不会触发此钩子。
  • Channel Relay 的 capability 依赖:能发 Channel 消息,不代表能批权限。只有显式声明了对应能力且在 allowlist 里的 Channel 客户端才会被激活。

推荐阅读路径

源码锚点

  • claude-code-opensource/src/utils/permissions/permissions.ts — 权限请求发起时的 Hook 触发点。
📄 src/utils/permissions/permissions.ts — 权限请求发起时的 Hook 触发点。L152-152 of 1487
typescript
          ? `Hook '${decisionReason.hookName}' blocked this action: ${decisionReason.reason}`
  • claude-code-opensource/src/services/mcp/channelPermissions.ts — Permission Relay 的短 ID、预览裁剪与 Map 映射。
📄 src/services/mcp/channelPermissions.ts — Permission Relay 的短 ID、预览裁剪与 Map 映射。L2-10 of 241
typescript
 * Permission prompts over channels (Telegram, iMessage, Discord).
 *
 * Mirrors `BridgePermissionCallbacks` — when CC hits a permission dialog,
 * it ALSO sends the prompt via active channels and races the reply against
 * local UI / bridge / hooks / classifier. First resolver wins via claim().
 *
 * Inbound is a structured event: the server parses the user's "yes tbxkq"
 * reply and emits notifications/claude/channel/permission with
 * {request_id, behavior}. CC never sees the reply as textapproval
  • claude-code-opensource/src/hooks/toolPermission/handlers/interactiveHandler.ts — 权限弹窗如何与本地 UI、Hooks、Channel 进行竞速。
📄 src/hooks/toolPermission/handlers/interactiveHandler.ts — 权限弹窗如何与本地 UI、Hooks、Channel 进行竞速。L417-426 of 537
typescript
      const hookDecision = await ctx.runHooks(
        currentAppState.toolPermissionContext.mode,
        result.suggestions,
        result.updatedInput,
        permissionPromptStartTimeMs,
      )
      if (!hookDecision || !claim()) return
      if (bridgeCallbacks && bridgeRequestId) {
        bridgeCallbacks.cancelRequest(bridgeRequestId)
      }

基于 Claude Code v2.1.88 开源快照的深度分析