Skip to content

Commit c901845

Browse files
anthhubclaude
andcommitted
feat: Chapter 7 — Permission system with danger command blocking
Demo code: - Add utils/permissions.ts with DEFAULT_RULES (13 rules), checkPermission(), createPermissionContext() - Three-tier rules: read-only allow, dangerous deny, write ask - Three modes: default (strict), auto (read-only auto-allow), bypassPermissions (skip all) - Integrate into query.ts via optional checkPermission callback - Permission checked before every tool execution in Agentic Loop - Update main.ts with 4 permission test cases (allow/ask/deny) Documentation: - Add "Hands-on: Permission Check Integration" to Ch7 (zh-CN/en) - Explain decision flow, rule hierarchy, mode comparison Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 7744648 commit c901845

5 files changed

Lines changed: 377 additions & 42 deletions

File tree

demo/main.ts

Lines changed: 11 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,10 @@ import type {
3030
ContentBlock,
3131
Tool,
3232
AppConfig,
33-
PermissionRule,
34-
PermissionContext,
35-
PermissionDecision,
3633
} from "./types/index.js";
3734

35+
import { createPermissionContext, createCheckPermissionFn, DEFAULT_RULES } from "./utils/permissions.js";
36+
3837
import { buildTool } from "./Tool.js";
3938
import { allTools, findToolByName, getToolsForAPI } from "./tools.js";
4039
import { query } from "./query.js";
@@ -79,44 +78,14 @@ const messages: Message[] = [userMsg, assistantMsg];
7978

8079
// ─── 验证权限系统 ──────────────────────────────────────────────────────────
8180

82-
// 定义权限规则
83-
const permissionRules: PermissionRule[] = [
84-
{ toolName: "Read", behavior: "allow", source: "default", reason: "只读操作无风险" },
85-
{ toolName: "Bash", pattern: "rm -rf", behavior: "deny", source: "default", reason: "危险的删除操作" },
86-
{ toolName: "Bash", behavior: "ask", source: "default", reason: "Shell 命令可能有副作用" },
87-
];
81+
const permCtx = createPermissionContext("default");
82+
const checkPerm = createCheckPermissionFn(permCtx);
8883

89-
// 构建权限上下文
90-
const permissionCtx: PermissionContext = {
91-
mode: "default",
92-
cwd: process.cwd(),
93-
rules: permissionRules,
94-
};
95-
96-
// 简单的权限检查函数(模拟真实 Claude Code 的 canUseTool)
97-
function checkPermission(
98-
toolName: string,
99-
input: Record<string, unknown>,
100-
rules: PermissionRule[]
101-
): PermissionDecision {
102-
for (const rule of rules) {
103-
if (rule.toolName !== "*" && rule.toolName !== toolName) continue;
104-
if (rule.pattern) {
105-
const command = String(input.command ?? "");
106-
if (!command.includes(rule.pattern)) continue;
107-
}
108-
if (rule.behavior === "allow") return { behavior: "allow" };
109-
if (rule.behavior === "deny") return { behavior: "deny", message: rule.reason ?? "Denied" };
110-
return { behavior: "ask", message: rule.reason ?? "需要确认" };
111-
}
112-
return { behavior: "ask", message: "默认需要确认" };
113-
}
114-
115-
// 测试权限检查
11684
const permTests = [
11785
{ tool: "Read", input: { file_path: "main.ts" } },
11886
{ tool: "Bash", input: { command: "ls -la" } },
11987
{ tool: "Bash", input: { command: "rm -rf /" } },
88+
{ tool: "Write", input: { file_path: "test.txt", content: "hello" } },
12089
];
12190

12291
// ─── 输出验证结果 ─────────────────────────────────────────────────────────
@@ -159,13 +128,13 @@ console.log(` 模型: ${DEFAULT_CONFIG.model}`);
159128
console.log(` 最大 Token: ${DEFAULT_CONFIG.maxTokens}`);
160129
console.log(` 权限模式: ${DEFAULT_CONFIG.permissionMode}`);
161130
console.log();
162-
console.log(`权限系统 (模式: ${permissionCtx.mode}):`);
163-
permTests.forEach((tc) => {
164-
const decision = checkPermission(tc.tool, tc.input, permissionCtx.rules);
131+
console.log(`权限系统 (模式: ${permCtx.mode}, 规则数: ${permCtx.rules.length}):`);
132+
for (const tc of permTests) {
133+
const decision = await checkPerm(tc.tool, tc.input);
165134
const icon = decision.behavior === "allow" ? "✅" : decision.behavior === "deny" ? "🚫" : "❓";
166135
const cmd = (tc.input.command ?? tc.input.file_path) as string;
167136
console.log(` ${icon} ${tc.tool}("${cmd}") → ${decision.behavior}`);
168-
});
137+
}
169138
console.log();
170139
// 实际执行工具
171140
console.log();
@@ -199,6 +168,7 @@ if (process.env.ANTHROPIC_API_KEY) {
199168
{
200169
model: DEFAULT_MODEL,
201170
maxTokens: 4096,
171+
checkPermission: checkPerm,
202172
onText: (text) => process.stdout.write(text),
203173
onToolUse: (name, input) => {
204174
console.log(`\n [工具调用] ${name}(${JSON.stringify(input)})`);
@@ -220,4 +190,4 @@ if (process.env.ANTHROPIC_API_KEY) {
220190
}
221191

222192
console.log();
223-
console.log("下一步: 第 5 章 - 完善工具实现(FileWrite、FileEdit、Glob)");
193+
console.log("下一步: 第 8 章 - REPL 交互式确认框");

demo/query.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* 或达到最大轮次限制。
1111
*/
1212

13-
import type { Message, ContentBlock, ToolUseBlock } from "./types/index.js";
13+
import type { Message, ContentBlock, ToolUseBlock, CheckPermissionFn } from "./types/index.js";
1414
import { createClient, streamMessage } from "./services/api/claude.js";
1515
import { buildSystemPrompt } from "./context.js";
1616
import { allTools, findToolByName, getToolsForAPI } from "./tools.js";
@@ -35,6 +35,8 @@ export interface QueryOptions {
3535
onToolUse?: (name: string, input: Record<string, unknown>) => void;
3636
/** 工具结果回调 */
3737
onToolResult?: (name: string, result: string, isError: boolean) => void;
38+
/** 权限检查函数(可选,不提供则跳过权限检查) */
39+
checkPermission?: CheckPermissionFn;
3840
}
3941

4042
/** 查询结果 */
@@ -74,6 +76,7 @@ export async function query(
7476
onText,
7577
onToolUse,
7678
onToolResult,
79+
checkPermission,
7780
} = options;
7881

7982
const client = createClient(apiKey);
@@ -200,6 +203,17 @@ export async function query(
200203
if (readOnlyTools.length > 0) {
201204
const results = await Promise.all(
202205
readOnlyTools.map(async (tu) => {
206+
// 权限检查
207+
if (checkPermission) {
208+
const decision = await checkPermission(tu.name, tu.input);
209+
if (decision.behavior === "deny") {
210+
return createToolResultBlock(tu.id, `Permission denied: ${decision.message}`, true);
211+
}
212+
if (decision.behavior === "ask") {
213+
// 在 CLI 环境中,默认允许(第 8 章的 REPL 才会真正弹出确认框)
214+
onToolUse?.(`[权限: ${decision.message}] ${tu.name}`, tu.input);
215+
}
216+
}
203217
const tool = findToolByName(tu.name);
204218
if (!tool) {
205219
return createToolResultBlock(tu.id, `Error: Unknown tool '${tu.name}'`, true);
@@ -214,6 +228,20 @@ export async function query(
214228

215229
// 串行执行读写工具
216230
for (const tu of writeTools) {
231+
// 权限检查
232+
if (checkPermission) {
233+
const decision = await checkPermission(tu.name, tu.input);
234+
if (decision.behavior === "deny") {
235+
toolResultBlocks.push(
236+
createToolResultBlock(tu.id, `Permission denied: ${decision.message}`, true)
237+
);
238+
continue;
239+
}
240+
if (decision.behavior === "ask") {
241+
// 在 CLI 环境中,默认允许(第 8 章的 REPL 才会真正弹出确认框)
242+
onToolUse?.(`[权限: ${decision.message}] ${tu.name}`, tu.input);
243+
}
244+
}
217245
const tool = findToolByName(tu.name);
218246
if (!tool) {
219247
toolResultBlocks.push(

demo/utils/permissions.ts

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/**
2+
* utils/permissions.ts - 权限检查实现
3+
*
4+
* 对应真实 Claude Code: src/utils/permissions.ts + src/hooks/toolPermission/
5+
*
6+
* 确保 AI 不会未经用户同意执行危险操作。
7+
* 每次工具调用前,根据权限规则决定:允许、拒绝、或询问用户。
8+
*/
9+
10+
import type {
11+
PermissionMode,
12+
PermissionRule,
13+
PermissionContext,
14+
PermissionDecision,
15+
CheckPermissionFn,
16+
} from "../types/index.js";
17+
18+
/**
19+
* 默认权限规则
20+
*
21+
* 真实 Claude Code 的规则更复杂,支持正则匹配、路径范围等。
22+
* 我们定义核心规则:只读工具允许,危险命令拒绝,其他询问。
23+
*/
24+
export const DEFAULT_RULES: PermissionRule[] = [
25+
// 只读工具:始终允许
26+
{ toolName: "Read", behavior: "allow", source: "default", reason: "只读操作" },
27+
{ toolName: "Grep", behavior: "allow", source: "default", reason: "只读搜索" },
28+
{ toolName: "Glob", behavior: "allow", source: "default", reason: "只读匹配" },
29+
{ toolName: "Echo", behavior: "allow", source: "default", reason: "只读回显" },
30+
31+
// 危险命令:始终拒绝
32+
{ toolName: "Bash", pattern: "rm -rf", behavior: "deny", source: "default", reason: "递归删除" },
33+
{ toolName: "Bash", pattern: "rm -r /", behavior: "deny", source: "default", reason: "删除根目录" },
34+
{ toolName: "Bash", pattern: "mkfs", behavior: "deny", source: "default", reason: "格式化磁盘" },
35+
{ toolName: "Bash", pattern: "> /dev/", behavior: "deny", source: "default", reason: "写入设备文件" },
36+
{ toolName: "Bash", pattern: ":(){ :|:& };:", behavior: "deny", source: "default", reason: "Fork 炸弹" },
37+
{ toolName: "Bash", pattern: "chmod -R 777", behavior: "deny", source: "default", reason: "不安全的权限修改" },
38+
39+
// 写操作:需要询问
40+
{ toolName: "Bash", behavior: "ask", source: "default", reason: "Shell 命令可能有副作用" },
41+
{ toolName: "Write", behavior: "ask", source: "default", reason: "文件写入操作" },
42+
{ toolName: "Edit", behavior: "ask", source: "default", reason: "文件编辑操作" },
43+
];
44+
45+
/**
46+
* 检查权限
47+
*
48+
* 按规则列表顺序匹配,返回第一个匹配的决策。
49+
* 如果没有规则匹配,根据权限模式决定默认行为。
50+
*/
51+
export function checkPermission(
52+
toolName: string,
53+
input: Record<string, unknown>,
54+
context: PermissionContext
55+
): PermissionDecision {
56+
// bypassPermissions 模式跳过所有检查
57+
if (context.mode === "bypassPermissions") {
58+
return { behavior: "allow" };
59+
}
60+
61+
// 遍历规则
62+
for (const rule of context.rules) {
63+
// 工具名匹配
64+
if (rule.toolName !== "*" && rule.toolName !== toolName) continue;
65+
66+
// 模式匹配(如果有)
67+
if (rule.pattern) {
68+
const command = String(input.command ?? input.content ?? "");
69+
if (!command.includes(rule.pattern)) continue;
70+
}
71+
72+
// 匹配成功,返回决策
73+
switch (rule.behavior) {
74+
case "allow":
75+
return { behavior: "allow" };
76+
case "deny":
77+
return { behavior: "deny", message: `Blocked: ${rule.reason ?? "policy violation"}` };
78+
case "ask":
79+
// auto 模式下,读操作自动放行
80+
if (context.mode === "auto") {
81+
// 简单的启发式:如果工具名暗示只读,自动放行
82+
if (["Read", "Grep", "Glob", "Echo"].includes(toolName)) {
83+
return { behavior: "allow" };
84+
}
85+
}
86+
return { behavior: "ask", message: rule.reason ?? "需要确认" };
87+
}
88+
}
89+
90+
// 无匹配规则,默认询问
91+
return { behavior: "ask", message: "未匹配任何规则,需要确认" };
92+
}
93+
94+
/**
95+
* 创建权限上下文
96+
*/
97+
export function createPermissionContext(
98+
mode: PermissionMode = "default",
99+
cwd: string = process.cwd(),
100+
extraRules: PermissionRule[] = []
101+
): PermissionContext {
102+
return {
103+
mode,
104+
cwd,
105+
rules: [...extraRules, ...DEFAULT_RULES], // 用户规则优先
106+
};
107+
}
108+
109+
/**
110+
* 创建权限检查函数
111+
*
112+
* 返回一个闭包,可以传递给 query() 使用。
113+
* 这对应真实 Claude Code 中 canUseTool 回调的模式。
114+
*/
115+
export function createCheckPermissionFn(
116+
context: PermissionContext
117+
): CheckPermissionFn {
118+
return async (toolName, input) => checkPermission(toolName, input, context);
119+
}

docs/en/07-permission-system.md

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
8. [Sandbox Integration](#8-sandbox-integration)
1717
9. [Hands-on: Build a Permission System](#9-hands-on-build-a-permission-system)
1818
10. [Key Takeaways & What's Next](#10-key-takeaways--whats-next)
19+
11. [Hands-on Build: Permission Check Integration](#hands-on-build-permission-check-integration)
1920

2021
---
2122

@@ -582,4 +583,112 @@ Every `ask` result carries a `decisionReason` explaining *why* the tool needs ap
582583

583584
---
584585

586+
## Hands-on Build: Permission Check Integration
587+
588+
> **This section marks another significant upgrade to the demo.** We add `utils/permissions.ts` — a permission check module — and integrate it into the tool execution flow in `query.ts`, enabling mini-claude to intercept or prompt before executing dangerous operations.
589+
590+
### Project Structure Update
591+
592+
```
593+
demo/
594+
├── utils/
595+
│ ├── messages.ts # Chapter 4
596+
│ └── permissions.ts # ← New: permission check implementation
597+
├── query.ts # Updated: permission check integration
598+
├── tools/
599+
│ ├── BashTool/
600+
│ ├── FileReadTool/
601+
│ ├── FileWriteTool/
602+
│ ├── FileEditTool/
603+
│ ├── GrepTool/
604+
│ └── GlobTool/
605+
├── main.ts
606+
├── Tool.ts
607+
├── context.ts
608+
├── services/api/
609+
└── types/
610+
```
611+
612+
### utils/permissions.ts Walkthrough
613+
614+
The permission check module implements the core decision flow of Claude Code's permission system:
615+
616+
```
617+
Tool call → Iterate rules → First matching rule determines behavior
618+
↓ ↓ ↓
619+
allow deny ask
620+
(execute) (reject+feedback to AI) (prompt user)
621+
```
622+
623+
**Default rule hierarchy:**
624+
625+
1. Read-only tools (Read, Grep, Glob) → always `allow`
626+
2. Dangerous command patterns (`rm -rf`, `mkfs`, `dd if=`, etc.) → always `deny`
627+
3. Write operations (Bash, Write, Edit) → `ask`
628+
629+
These three layers embody Claude Code's core security philosophy: **read-only operations pass freely, dangerous commands are firmly rejected, write operations are left to the user's judgment**.
630+
631+
### Three Permission Modes
632+
633+
| Mode | Behavior |
634+
|------|----------|
635+
| `default` | Strict rule execution — read-only allow, dangerous deny, writes ask |
636+
| `auto` | Read-only operations auto-approve, writes still require confirmation (simplified `acceptEdits`) |
637+
| `bypassPermissions` | Skip all checks (development/debugging only, never use in production) |
638+
639+
The real Claude Code has 7 modes (see Section 2 of this chapter); the demo simplifies to 3 to focus on core logic.
640+
641+
### Integration with query.ts
642+
643+
`checkPermission` is passed as an optional callback into `query()`, via a new field in `QueryOptions`:
644+
645+
```typescript
646+
export interface QueryOptions {
647+
// ...existing fields
648+
checkPermission?: (toolName: string, input: Record<string, unknown>) => Promise<PermissionResult>;
649+
}
650+
```
651+
652+
The integration point is before tool execution — after the Agentic Loop receives tool calls, it checks permissions first:
653+
654+
- **allow** → execute the tool normally
655+
- **deny** → skip tool execution and return an error message to the AI (e.g., `"Permission denied: rm -rf is blocked by safety rules"`), which lets the AI adjust its strategy
656+
- **ask** → log and proceed (the current demo has no interactive UI; Chapter 8's REPL will display a confirmation dialog, implementing true interactive user confirmation)
657+
658+
If no `checkPermission` callback is provided, behavior is identical to before — all tools execute unconditionally. This ensures backward compatibility.
659+
660+
### Running the Demo
661+
662+
```bash
663+
cd demo && bun run main.ts
664+
```
665+
666+
Try these interactions to verify the permission system:
667+
668+
```
669+
you> delete all files in the current directory
670+
# AI attempts rm -rf → denied → AI receives error and adjusts strategy
671+
672+
you> read package.json
673+
# Read is a read-only tool → allowed directly, no confirmation needed
674+
675+
you> create a test.txt file
676+
# Write is a write operation → ask → permission check info logged
677+
```
678+
679+
### Mapping to Real Claude Code
680+
681+
| Demo File | Real File | What's Simplified |
682+
|-----------|-----------|-------------------|
683+
| `utils/permissions.ts` | `src/utils/permissions/permissions.ts` | No multi-source rule priority, no wildcard matching engine |
684+
| `utils/permissions.ts` dangerous patterns | `src/utils/permissions/dangerousPatterns.ts` | Hardcoded few patterns, no tree-sitter AST analysis |
685+
| `utils/permissions.ts` mode switching | `src/utils/permissions/PermissionMode.ts` | 3 modes vs 7 modes |
686+
| `query.ts` (permission callback) | `src/hooks/toolPermission/` | No resolve-once racing, no classifier, no bridge |
687+
688+
### What's Next
689+
690+
Chapter 8 will implement an interactive terminal UI (React + Ink), including user input, message rendering, and permission confirmation dialogs. At that point, the `ask` permission will display a real dialog letting users choose "Allow" or "Deny", rather than merely logging.
691+
692+
---
693+
585694
*Source references verified against `anthhub-claude-code` commit tree.*

0 commit comments

Comments
 (0)