Walkthrough | 模拟 Permission Prompt¶
难度:⭐⭐ 时间:~1h 目标:手写一个 permission prompt UI(mock)
1. Permission Prompt 是什么¶
Permission Prompt = Claude Code 询问用户是否允许某个工具调用的 UI。 - 4 选项:Yes / Yes (file) / Yes (session) / No - 3 mode 影响 - 可 deny 加 rule
2. 目标¶
手写一个简化版 permission prompt: - 4 选项 - 返回用户选择 - 加 rule - 集成 mock
3. 完整代码¶
// mock-permission-prompt.ts
import * as readline from 'readline/promises'
type Decision = 'allow-once' | 'allow-file' | 'allow-session' | 'deny'
interface PermissionPromptResult {
decision: Decision
rule?: string
}
interface PermissionContext {
toolName: string
toolInput: any
reason?: string
suggestedRule?: string
}
export class MockPermissionPrompt {
async ask(context: PermissionContext): Promise<PermissionPromptResult> {
this.display(context)
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout,
})
const answer = await rl.question('\nChoice: ')
rl.close()
return this.parse(answer.trim(), context)
}
private display(ctx: PermissionContext): void {
console.log('\n' + '='.repeat(60))
console.log(`Allow ${this.formatTool(ctx)}?`)
console.log('='.repeat(60))
if (ctx.reason) {
console.log(`Reason: ${ctx.reason}`)
}
if (ctx.toolInput) {
console.log('\nDetails:')
const input = JSON.stringify(ctx.toolInput, null, 2)
.split('\n')
.map((line) => ' ' + line)
.join('\n')
console.log(input)
}
console.log('\nOptions:')
console.log(' 1. Yes (allow this time)')
console.log(' 2. Yes, and don\'t ask again for this tool on this file')
console.log(' 3. Yes, and don\'t ask again for this tool in this session')
console.log(' 4. No')
if (ctx.suggestedRule) {
console.log(`\nSuggested rule: ${ctx.suggestedRule}`)
}
}
private parse(answer: string, ctx: PermissionContext): PermissionPromptResult {
switch (answer) {
case '1':
return { decision: 'allow-once' }
case '2':
return {
decision: 'allow-file',
rule: this.buildFileRule(ctx),
}
case '3':
return {
decision: 'allow-session',
rule: ctx.suggestedRule || this.buildSessionRule(ctx),
}
case '4':
return { decision: 'deny' }
default:
return { decision: 'deny' } // 默认 deny(保守)
}
}
private formatTool(ctx: PermissionContext): string {
const input = JSON.stringify(ctx.toolInput)
if (input.length > 60) {
return `${ctx.toolName}(${input.slice(0, 60)}...)`
}
return `${ctx.toolName}(${input})`
}
private buildFileRule(ctx: PermissionContext): string {
const file = ctx.toolInput?.file_path
if (file) {
return `${ctx.toolName}(${file})`
}
return ctx.suggestedRule || ctx.toolName
}
private buildSessionRule(ctx: PermissionContext): string {
return ctx.suggestedRule || ctx.toolName
}
}
~120 行。
4. 使用示例¶
const prompt = new MockPermissionPrompt()
const result = await prompt.ask({
toolName: 'Bash',
toolInput: { command: 'ls -la' },
reason: 'Listed directory',
suggestedRule: 'Bash(ls:*)',
})
console.log('Decision:', result.decision)
console.log('Rule:', result.rule)
2 步。
5. 4 个选项详解¶
5.1 Yes (allow this time)¶
单次允许。
5.2 Yes, file¶
文件级允许。
5.3 Yes, session¶
session 级允许。
5.4 No¶
拒绝。
6. 4 步集成¶
6.1 集成 1: Bash tool¶
const decision = await prompt.ask({
toolName: 'Bash',
toolInput: { command },
suggestedRule: `Bash(${extractPrefix(command)}:*)`,
})
Bash。
6.2 集成 2: Edit¶
const decision = await prompt.ask({
toolName: 'Edit',
toolInput: { file_path, old_string, new_string },
suggestedRule: `Edit(./${path.dirname(file_path)}/**)`,
})
Edit。
6.3 集成 3: WebFetch¶
const decision = await prompt.ask({
toolName: 'WebFetch',
toolInput: { url },
suggestedRule: `WebFetch(domain:${new URL(url).hostname})`,
})
WebFetch。
6.4 集成 4: Agent¶
const decision = await prompt.ask({
toolName: 'Agent',
toolInput: { name, prompt },
suggestedRule: `Agent(${name})`,
})
Agent。
7. 5 个扩展¶
7.1 加 fuzzy match¶
// "y" / "n" 也接受
if (answer === 'y') return { decision: 'allow-once' }
if (answer === 'n') return { decision: 'deny' }
fuzzy。
7.2 加 default¶
// 超时 → default
const timeout = setTimeout(() => rl.close(), 30000)
try {
const answer = await rl.question(...)
} finally {
clearTimeout(timeout)
}
default。
7.3 加 reason 输入¶
reason。
7.4 加 TUI¶
// 不用 readline,用 Ink
import { Box, Text, useInput } from 'ink'
function TUIPrompt({ onChoice }) {
useInput((input, key) => {
if (input === '1') onChoice('allow-once')
// ...
})
}
TUI。
7.5 加 history¶
// 记忆上次选择
const lastChoice = history.get(toolName)
if (lastChoice === 'allow-session') {
return { decision: 'allow-session' }
}
history。
8. 5 个最佳实践¶
- default deny —— 未指定 → deny
- timeout —— 30s 默认
- show tool input —— 让用户理解
- suggested rule —— 一键添加
- file / session —— 粒度选择
9. 5 个常见情况¶
9.1 单次允许¶
9.2 文件允许¶
9.3 session 允许¶
9.4 拒绝¶
9.5 超时¶
10. 5 个测试¶
// 1. test allow-once
const r1 = await prompt.ask({ toolName: 'Bash', toolInput: { command: 'ls' } })
// simulate input '1'
// expect r1.decision === 'allow-once'
// 2. test allow-file
// simulate '2'
// expect r1.decision === 'allow-file'
// expect r1.rule === 'Bash(...)'
// 3. test deny
// simulate '4'
// expect r1.decision === 'deny'
// 4. test default
// simulate 'invalid'
// expect r1.decision === 'deny'
// 5. test timeout
// (no input for 30s)
// expect r1.decision === 'deny'
5 测。
11. 总结¶
模拟 Permission Prompt = 4 选项 + suggested rule + 集成。
核心: - 120 行简化版 - 4 选项 - rule 生成 - default deny
下一步: - 看真实 Claude Code - 加 TUI - 加 history