Skip to content

onPreToolUse 是 Copilot SDK 最重要的安全控制点,在工具执行前拦截并决定允许/拒绝/请求确认。可以修改工具参数、添加执行上下文、限制文件访问路径、记录审计日志。保持 Hook 逻辑快速,拒绝时给出清晰原因。

GitHub Copilot SDK onPreToolUse Hook:精细控制工具调用权限

基本结构

typescript
const session = await createSession({
  hooks: {
    onPreToolUse: async (context) => {
      const { toolName, arguments: args, sessionId } = context
      
      // 决策逻辑
      return null  // 允许(默认)
    }
  }
})

context 包含:

  • toolName:被调用的工具名称
  • arguments:工具参数对象
  • sessionId:当前会话 ID
  • agentName:发起调用的 Agent 名称(如果是 subagent)

三种决策方式

允许执行

typescript
return null  // 简洁写法
// 或
return { action: 'allow' }

拒绝执行

typescript
return {
  action: 'deny',
  reason: '该文件在安全保护列表中,禁止修改'  // 告知 AI 为什么被拒绝
}

AI 收到拒绝时会根据 reason 决定下一步,所以 reason 要清晰有用。

请求用户确认

typescript
return {
  action: 'ask',
  message: `Agent 要删除以下文件,是否确认?\n${args.paths.join('\n')}`
}

应用层需要监听对应事件,展示确认界面,然后调用 session API 传回用户决定。

实用模式

限制文件访问范围

typescript
onPreToolUse: async ({ toolName, arguments: args }) => {
  if (['read_file', 'write_file', 'delete_file'].includes(toolName)) {
    const allowedPaths = ['/workspace', '/tmp']
    const targetPath = args.path as string

    const isAllowed = allowedPaths.some(p => targetPath.startsWith(p))
    if (!isAllowed) {
      return {
        action: 'deny',
        reason: `文件访问限制在以下目录:${allowedPaths.join(', ')}`
      }
    }
  }
  return null
}

修改工具参数

typescript
onPreToolUse: async ({ toolName, arguments: args }) => {
  if (toolName === 'execute_command') {
    // 自动设置超时,防止命令无限运行
    return {
      action: 'allow',
      modifiedArguments: {
        ...args,
        timeout: Math.min(args.timeout ?? 30000, 30000)  // 最多 30 秒
      }
    }
  }
  return null
}

审计日志(非阻塞)

typescript
onPreToolUse: async ({ toolName, arguments: args, sessionId }) => {
  // 非阻塞写日志(不 await,不影响工具执行速度)
  auditLog.append({
    timestamp: new Date().toISOString(),
    sessionId,
    tool: toolName,
    args
  }).catch(err => console.error('日志写入失败:', err))
  
  return null  // 不影响工具执行
}

为特定工具添加上下文

typescript
onPreToolUse: async ({ toolName, arguments: args }) => {
  if (toolName === 'execute_sql') {
    return {
      action: 'allow',
      additionalContext: `数据库规则:
- 只允许 SELECT 和有 WHERE 条件的 UPDATE
- 禁止 DROP、TRUNCATE、DELETE without WHERE
- 当前用户:${process.env.DB_USER}(只读权限)`
    }
  }
  return null
}

最佳实践

1. 拒绝时说清楚原因:AI 会基于 reason 决定是否换一种方式完成任务,模糊的 reason 会导致 AI 反复尝试。

2. 快速决策:Hook 阻塞工具执行,避免在 Hook 中做耗时操作(数据库查询、网络请求)。

3. 修改参数要保守modifiedArguments 只改需要改的字段,不要改变参数的语义含义。

常见问题

Q: onPreToolUse 能看到 MCP 服务器的工具调用吗?

A: 可以,MCP 工具调用也会经过 Hook,toolName 会是 MCP 工具的名称(如 mcp:filesystem:read_file)。

Q: 同一个工具调用,Hook 被调用多次正常吗?

A: 如果 Agent 重试了工具调用(如第一次失败后),Hook 会被再次调用。这是正常行为,确保 Hook 的逻辑是幂等的。

Q: 如果 Hook 本身抛出异常会怎样?

A: Hook 异常会被视为工具调用失败,Agent 会收到错误信息。建议在 Hook 内部 try-catch,确保控制权始终在你手中。