如果你已经在用 Claude Code 的 Hooks 做守门人,这篇文章是进阶篇。前一篇讲的是「怎么在工具调用前后插一段逻辑」,这篇要讲的是「权限被拒绝后怎么办」——这是生产级治理最容易被忽视但也最关键的环节。

2026年4月初的更新给 Hooks 系统补上了两块重要拼图:

  1. PermissionDenied Hook(4月1日):Auto Mode 拒绝一个操作后触发的回调
  2. Deferred Permission Decision(4月1日):把权限决策 defer 留到后续再处理

这两块拼图补上之后,Hooks 系统才真正从「做点小自动化」升级为「生产级治理层」。这篇文章就是专门讲这两块怎么用、什么时候用、以及如何和现有权限体系搭起来。


一、先理解 PermissionDenied Hook

1.1 触发时机

用户 Prompt → Claude Code 分析 → Auto Mode 判断可不可做 → (拒绝) → PermissionDenied Hook 触发

当 Auto Mode(.claude/settings.jsonautoApprove: true)决定拒绝一个操作时,这个 Hook 就会触发。它不是在工具执行之前卡住(那是 PreToolUse 的活),而是在已经决定拒绝之后给你一个回调。

1.2 返回值语义

这个 Hook 的关键在于它的返回值。最重要的字段是 retry

// Hook 返回示例
{
  retry: true  // 告诉模型:你可以重试这个操作
}

如果返回 retry: true,模型会重新评估这个操作,可能换一种方式再做一次。如果不返回或者返回 retry: false,模型就会放弃这个操作,继续往下走。

1.3 典型使用场景

场景一:降级处理

一个命令被 Auto Mode 拒绝,但你知道某些情况下可以绕过去:

// permission-denied.js
module.exports = {
  hooks: {
    PermissionDenied: async ({ tool, input, reason }) => {
      // 数据库操作被拒,可能是权限不够
      if (tool === 'Bash' && input.command.includes('DROP TABLE')) {
        // 检查是否是测试环境
        const env = await getEnvContext();
        if (env === 'test') {
          return { retry: true };  // 测试环境允许
        }
      }
      // 其他情况正常拒绝
      return { retry: false };
    }
  }
}

场景二:升级人工介入

拒绝只是一个中间状态,真正的意图是让更高级别的权限来审批:

// permission-escalation.js
module.exports = {
  hooks: {
    PermissionDenied: async ({ tool, input, reason }) => {
      // 敏感操作被拒,转发给人肉审批
      if (isSensitiveOperation(tool, input)) {
        await notifySecurityTeam({
          tool,
          input: sanitize(input),  // 脱敏
          reason,
          session: getSessionId()
        });
        return { retry: false };  // 不重试,等人工审批
      }
      return { retry: true };  // 其他可以重试
    }
  }
}

二、Deferred Permission Decision:把决策权留到后面

2.1 核心设计

Deferred Permission Decision 的核心思想是:不在运行时卡住,而是在后续会话中处理

当你在 PreToolUse Hook 里返回 { defer: true } 时:

  1. 当前工具调用被暂停
  2. 会话记录里留下一个「待决标记」
  3. 用户下次用 -p --resume 恢复会话时,这个 Hook 会再次触发,但这次带上了 deferred 上下文

2.2 工作流

第一轮:
User: "把这个表删掉" → Claude: 分析 → PreToolUse: 检测到 DROP TABLE → Hook 返回 { defer: true } → 操作暂停

第二轮(用户重新接入):
User: "确认删,这是测试环境" → Claude: 分析 → PreToolUse: 检测到 deferred 标记 → Hook 再次触发,这次看到上下文 → 这次返回 { continue: true } → 执行

2.3 代码示例

// deferred-decision.js
module.exports = {
  hooks: {
    PreToolUse: async ({ tool, input }) => {
      // 需要确认的操作
      if (tool === 'Bash' && isDestructiveCommand(input.command)) {
        // 第一次遇到 defer,等后续确认
        if (!input._deferred) {
          return {
            defer: true,
            message: "这个操作有点危险,需要你确认一下环境"
          };
        }
        // 第二次遇到(用户已确认),检查确认上下文
        if (input._deferred?.confirmed) {
          return { continue: true };
        }
        // 用户没有确认,取消
        return { cancel: true };
      }
      return { continue: true };
    }
  }
}

用户在 --resume 时可以带参数:

claude -p --resume -- "确认删除,这是测试环境"

三、何时用即时拒绝,何时用延迟决策

场景 推荐模式 理由
明确危险(如 rm -rf / 即时拒绝 没有「绕过去」的可能
需要环境确认(如生产 vs 测试) Deferred 需要上下文判断
批量操作前需要人工审批 Deferred 审批链路长,不适合卡在运行时
权限不足但可能换账号 Deferred 切账号后再试
误触发的自动操作 PermissionDenied + retry 可能是误判,给模型重试机会

3.1 决策树

收到权限请求
    ↓
是否 100% 确定要拒绝?
    ├─ 是 → 直接拒绝
    └─ 否 → 进入判断流程
              ↓
        是否需要额外上下文?
              ├─ 是 → defer,存状态
              └─ 否 → 直接拒绝或放行

四、结合 forceRemoteSettingsRefresh 的企业治理

4月初还发布了另一个重要策略:forceRemoteSettingsRefresh。这个策略解决的是「启动时配置不生效」的问题。

4.1 场景

企业用托管配置(managed settings),但有时候 CI/CD 更新了配置,本地还在用旧配置。之前的做法是手动删缓存或者重启。

4.2 启用方式

// settings.json
{
  "policies": {
    "forceRemoteSettingsRefresh": {
      "description": "启动时强制拉取远程配置,失败则退出",
      "enabled": true
    }
  }
}

4.3 治理组合

forceRemoteSettingsRefresh + PermissionDenied Hook + Deferred Permission 组合起来,就是一个完整的企业级权限治理闭环:

  1. 启动:强制拉取最新托管配置(确保策略是最新的)
  2. 运行中:PreToolUse Hook 做第一道卡口(即时判断)
  3. 被拒后:PermissionDenied Hook 做第二道卡口(允许重试或升级)
  4. 需要确认:Deferred Decision 把决策权留给后续会话

五、团队落地推荐分层

Layer 1:基础防护(必选)

// base-security.js
module.exports = {
  hooks: {
    PreToolUse: [
      // 明确拒绝的清单
      { if: 'tool === "Bash" && command.includes("rm -rf /")', action: { cancel: true } }
    ]
  }
}

Layer 2:生产治理(推荐)

// production-guard.js
module.exports = {
  hooks: {
    PreToolUse: [
      // 生产环境操作需要 defer
      { if: 'env === "production" && isDestructive', action: { defer: true } }
    ],
    PermissionDenied: [
      // 被拒后记录日志,上报监控
      { action: ({ tool, reason }) => logPermissionDenied(tool, reason) }
    ]
  }
}

Layer 3:安全响应(可选)

// security-response.js
module.exports = {
  hooks: {
    PermissionDenied: [
      // 敏感操作被拒,触发安全告警
      { if: 'isSensitiveOperation', action: ({ tool, reason }) => alertSecurityTeam({ tool, reason }) }
    ]
  }
}

六、总结

新能力 解决的核心问题 适合场景
PermissionDenied Hook Auto Mode 被拒后的处理 需要重试、需要升级审批
Deferred Permission Decision 运行时无法判断,需要后续上下文 跨会话审批、环境切换确认
forceRemoteSettingsRefresh 配置不同步问题 企业托管配置、CI/CD 更新

PermissionDenied + Deferred Permission 这两块拼图,让 Hooks 系统真正从「脚本小技巧」升级为「生产级治理层」。如果你在做多 Agent 协作、或者在企业里推 AI 编程工具,这几个能力是绕不开的。


下篇预告:当你有几十个 Hooks 要管理时,怎么用 Settings 的分层配置来组织它们,而不是让它们变成一坨 spaghetti 代码。