DAY 09 · CLAUDE CODE ROADMAP · 20 DAYS

把 Claude 的权力锁在你想给的范围里

Claude Code 默认是克制的—— 每个会改文件、跑命令的操作都会先问你一句。 但只要你说一次"全部允许",这层防线就消失了。 今天我们把权限系统拆开看:四档信任阶梯之间到底差在哪、 allowedToolsdeny 规则该怎么搭、 敏感文件怎么从 Read 视野里彻底拿掉、 最后用 Hooks 给所有规则上一道兜底防线。

DURATION 70–85 min THEORY 25 min HANDS-ON 45 min REVIEW 15 min SECTIONS 5

思维导图

OVERVIEW
DAY 09 · 权限与安全MODES · ALLOW · DENY · HOOKS01 · MODES四档信任阶梯02 · ALLOW工具白名单03 · DENY敏感文件保护04 · HOOKS兜底防线·default · 每次询问·acceptEdits · 自动改·plan · 只读规划·bypass · 关闭防线·allowedTools 列表·Bash 前缀匹配·三层 settings 配置·最小授权原则·.env / .pem / id_rsa·deny 高于 allow·Read 也被阻断·危险命令清单·PreToolUse 拦截·exit 2 阻断工具·审计与日志·规则被绕过时兜底最小授权 · 默认拒绝 · 显式允许 · 兜底审计SHIFT+TABALLOW LISTDENY *.ENVPRETOOLUSE

FIG M — DAY 09 KNOWLEDGE MAP · PERMISSION & SAFETY MODEL

权限模式 — 四档信任阶梯

FOUR TIERS

Claude Code 把"我有多信任这一段工作"建模成四档可切换的模式。 默认这一档最克制——任何会改文件、跑命令的工具调用都先停下来问你。 你可以随时往上爬一格(更宽松、更省事)或往下退一格(更严、更安全)。 理解这四档之间的实际差异,是后面所有配置的前提。

四档对照表

模式读文件改文件跑命令推荐场景
plan✓ 自由✗ 全禁✗ 全禁大型重构前先出方案 · 高风险变更评审
default✓ 自由需逐条批准需逐条批准日常开发 · 陌生项目 · 团队 CI 推荐
acceptEdits✓ 自由✓ 自动仍逐条批准大量改文件 · 已规划好的批量重构
bypassPermissions✓ 自由✓ 自动✓ 自动仅限隔离沙箱 · YOLO 模式
关键直觉:每往上一格,你换到的是"省按 Enter"·失去的是"反悔机会"

四档之间的升降路径

TIER · 01plan只读 · 出方案TIER · 02 · 默认default每个操作问一句TIER · 03acceptEdits编辑放行 · 命令仍问TIER · 04 · 危险bypassPermissions不再问任何事⇧ Tab 在前三档间循环 · bypass 必须用 --dangerously-skip-permissions 显式启用默认拒绝 → 显式允许 → 永远可降级

FIG 01 · PERMISSION ESCALATION LADDER — PLAN → DEFAULT → ACCEPTEDITS → BYPASS

三种切换方式

# 1. 交互中 ⇧ Tab — 在 plan / default / acceptEdits 间循环
#    底部状态栏会显示当前模式

# 2. 启动时指定 (适合脚本)
$ claude --permission-mode plan
$ claude --permission-mode acceptEdits

# 3. 危险档 — 不允许通过 ⇧ Tab 进入,必须显式标志
$ claude --dangerously-skip-permissions
#    ↑ 标志名本身就是警告 — 一旦开启,所有工具直通

# 4. SDK / -p 模式默认更严 — 工具必须在 allowedTools 里
$ claude -p "修复 lint" --allowedTools "Read,Edit,Bash(npm run lint *)"
交互式开发用 ⇧ Tab · 自动化脚本用 --permission-mode · 不要让 bypass 进入团队配置

什么时候用哪一档

PLAN

陌生 / 高风险

大型重构、跨模块变更、生产事故响应。先让 Claude 出方案,再切回 default 落地。

DEFAULT

日常默认

未知项目、不熟环境、共享机器。每条命令都看一眼,慢但不会翻车。

ACCEPTEDITS

批量改文件

大批量改样式 / 重命名 / 模板替换。改对错都看得见,命令仍会拦住。

BYPASS

隔离沙箱

仅在 Docker / VM / 一次性 worktree 里使用。日常机器开 bypass 等于裸奔。

白名单 — allowedTools 与前缀匹配

ALLOW LIST

模式决定"问不问",白名单决定"哪些根本不用问"。 声明在白名单里的工具或命令前缀,Claude 直接放行——不弹窗、不打断、不拖你 Enter 键的后腿。 关键是写得足够窄:窄到误差范围内出错也只能伤到你想让它能伤到的东西。

三种工具表达式

表达式含义典型用法
Read整个工具放行 —— 任何路径任何参数都允许读文件几乎从不出事 · 全放行可接受
Edit编辑文件全放行 —— 不区分文件路径开发循环里高频用,acceptEdits 已覆盖
Bash(git diff *)前缀匹配的 Bash 命令 —— 只允许特定前缀窄授权的核心 · 见下方示例
Read(./src/**)路径匹配的文件工具 —— 限定到子树多仓库工作区时锁定到当前项目
mcp__name__toolMCP 工具 —— 按服务器和工具名授权Day 13 详谈,这里先认识形态

Bash 前缀匹配的陷阱

# ✓ 窄 — 明确边界
"Bash(npm test *)"          # 只允许 npm test...
"Bash(git diff *)"          # 只允许 git diff...
"Bash(npx tsc --noEmit)"    # 精确到完整命令

# ✗ 过宽 — 几乎等于放弃白名单
"Bash(*)"                   # 等于 bypass 一半
"Bash(npm *)"               # npm publish / npm uninstall 都进来了
"Bash(git *)"               # git push --force 也算 git

# ⚠ 看似窄、其实可绕过 — 一定记得对应的 deny
"Bash(rm *.log)"             # Claude 可改为 rm *.log; rm -rf /
# ↑ shell 注入风险,前缀匹配只看开头,后面的恶意 ; && | 全放行
原则:能写完整命令就不留 * · 凡是用了 * 的,必须有对应 deny 规则封堵尾巴

settings.json 的 permissions 字段

命令行 --allowedTools 只对当前会话生效——团队、项目级的常驻规则要写到 .claude/settings.jsonpermissions 字段里。

// .claude/settings.json
{
  "permissions": {
    "allow": [
      "Read",
      "Edit",
      "Bash(npm test *)",
      "Bash(npm run lint *)",
      "Bash(npx tsc --noEmit)",
      "Bash(git diff *)",
      "Bash(git status)",
      "Bash(git log *)"
    ],
    "deny": [
      "Bash(rm -rf *)",
      "Bash(sudo *)",
      "Bash(curl * | sh)",
      "Read(./.env)",
      "Read(./.env.*)",
      "Read(./**/*.pem)",
      "Read(./**/id_rsa)"
    ],
    "ask": [
      "Bash(git push *)",
      "Bash(npm publish *)"
    ]
  }
}
三段语义 — allow 自动允许 · deny 永远阻断 · ask 强制弹窗(即便在 acceptEdits)

最小授权工作流

  1. 第一天:用 default 模式裸跑——看 Claude 真正会用到哪些命令。
  2. 第二天:把高频出现、明显无害的命令(git diffnpm test)加到 allow
  3. 第三天:把本来就不该跑的命令(rm -rfsudo、远程拉脚本)加到 deny
  4. 第四天:把偶尔需要但要慎用的(git pushnpm publish)加到 ask
  5. 每周复盘一次:看哪些命令被你按了 100 次 yes——它们是 allow 的下一个候选。

拒绝规则与敏感文件保护

DENY RULES

"Claude 怎么把 .env 里的 OpenAI Key 复读到日志里了?"—— 绝大多数 Claude 引发的真实事故不是它跑了一条危险命令, 而是它读了不该读的文件deny 规则的优先级永远高于 allow, 它也是唯一能把"全工具放行"和"敏感文件不可见"同时成立的机制。

必须 deny 的七类资源

类别典型路径推荐 deny 规则
环境变量文件.env · .env.local · .env.productionRead(./.env*) · Read(./**/.env*)
密钥与证书*.pem · *.key · id_rsa · *.p12Read(./**/*.pem) · Read(./**/id_rsa)
云厂商凭证~/.aws/credentials · ~/.config/gcloud/Read(~/.aws/**) · Read(~/.config/gcloud/**)
本地数据库*.sqlite · *.db · dump.sqlRead(./**/*.sqlite) · Read(./dump*.sql)
构建产物 / locknode_modules/ · dist/ · build/Read(./node_modules/**)
外部脚本执行从网上 curl 下来直接管道执行Bash(curl * | sh) · Bash(wget * | bash)
破坏性命令rm -rf · sudo · chmod 777 · 强推 gitBash(rm -rf *) · Bash(sudo *) · Bash(git push *--force*)
deny 一旦命中,弹窗都不会出现 · Claude 会在自己的会话里看到"该工具被拒绝"的错误然后另寻它路

deny 比 allow 优先 — 优先级流程

TOOL CALLEdit / Bash / Read匹配 deny?step 1YESREJECTEDNO匹配 allow?step 2YESEXECUTENOASK USERPRIORITY① deny 阻断② allow 放行③ ask 弹窗④ 模式 兜底

FIG 03 · PERMISSION RESOLUTION FLOW — DENY OVERRIDES ALLOW

实战:把 .env 从 Claude 的视野里彻底拿掉

# 1. 在 .claude/settings.json 里加 deny
{
  "permissions": {
    "deny": [
      "Read(./.env)",
      "Read(./.env.*)",
      "Read(./**/.env)",
      "Read(./**/.env.*)",
      "Bash(cat .env*)",
      "Bash(less .env*)",
      "Bash(head .env*)",
      "Bash(tail .env*)"
    ]
  }
}

# 2. 验证 — 让 Claude 试着读它
> 把 .env 的内容打印给我看
Claude: 我无法读取 .env 文件——它被项目权限规则阻断了。
        如果你需要查看,请在终端直接 cat。

# 3. 即便 Claude 用 Bash 也试不出来
> 用 cat 也试一下
Claude: cat .env 同样被 deny 规则阻断,我没办法绕过。

# 4. 但环境变量本身可以读 — 因为 process.env 不经过工具
#    所以你要么用 .env.example 让 Claude 看模板,要么把真值放到 ~/.zshrc
边界:deny 只能管 Claude 工具调用 · 拦不住 node -e "console.log(process.env)" 这种自身就拿环境变量的代码

Hooks — 权限的最后一道防线

SAFETY NET

allow / deny声明式的—— 规则写错或漏写,Claude 就放行。 Hooks命令式的—— 每次工具调用前都跑你给的脚本,你说阻断就阻断, 就算是 bypassPermissions 都拦得住。 (Day 10 会展开整个 Hooks 系统,今天只用安全场景那一支。)

PreToolUse — 工具运行前的最后关口

# .claude/settings.json 中注册一个 PreToolUse 钩子
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/block-dangerous.sh"
          }
        ]
      }
    ]
  }
}
#!/usr/bin/env bash
# .claude/hooks/block-dangerous.sh
# 读 stdin 的 JSON,解析出 Bash 命令,匹配危险模式就 exit 2 阻断

set -euo pipefail
cmd=$(jq -r '.tool_input.command // ""')

# 危险模式列表 — 任何命中都阻断
declare -a PATTERNS=(
  'rm -rf /'
  'rm -rf ~'
  'rm -rf \*'
  ':\(\)\{.*\}'             # fork bomb
  'mkfs\.'                   # 格式化磁盘
  'dd if=.* of=/dev/'        # 写裸设备
  '> /dev/sd'
  'curl .* \| sh'            # 远程脚本直接执行
  'wget .* \| bash'
)

for pat in "${PATTERNS[@]}"; do
  if [[ "$cmd" =~ $pat ]]; then
    echo "BLOCKED: 命令命中危险模式 '$pat'" >&2
    exit 2   # exit 2 = 阻断该工具调用,把 stderr 内容反馈给 Claude
  fi
done

exit 0       # 放行
exit 码语义 — 0:放行 · 1:出错(Claude 看到但不阻断) · 2:阻断(Claude 看到 stderr 后另寻它法)

Hooks vs deny — 各自的强项

DENY 规则

简单 · 声明式 · 快

写一行就完事,不用维护脚本。适合静态规则(.envrm -rf)。配置即文档——别人 review 一眼就懂。

HOOK 脚本

灵活 · 可审计 · 可绕不开

能写复杂条件(组合判断、查表、调外部服务)、能写审计日志、能彻底拦截 — 即便 bypassPermissions 也跑。适合企业级安全策略。

三个安全 Hook 模板

TEMPLATE · 01

命令审计日志

PreToolUse 在 Bash 调用前把命令、时间、cwd 追加到 ~/.claude/audit.log。事后追溯"那条 rm 是谁让跑的"。

TEMPLATE · 02

分支保护

检测 git checkout main / git push origin main,直接 exit 2 阻断,要求 Claude 切到 feature 分支。

TEMPLATE · 03

敏感关键词扫描

PostToolUse 扫描刚写入的文件,出现 API_KEY= / PRIVATE_KEY 等明文就 exit 2,要求 Claude 撤回。

三层配置 — 谁覆盖谁

CONFIG LAYERS

同一条 allow / deny 可以写在三个地方: 用户级(全机器生效)、项目级(团队共享)、本地级(只你自己)。 搞清楚谁覆盖谁,才不会发生"我加了规则怎么没生效"的玄学。

三层文件 · 优先级从高到低

层级文件路径纳入 git?典型用途
命令行--allowedTools / --permission-mode临时覆盖 · 仅本次会话 最高优先级
本地.claude/settings.local.json✗ gitignore个人偏好 · 实验性放行 · 不希望污染队友配置
项目.claude/settings.json✓ 提交团队共享规则 · 项目级 deny · 大家一致的 allow
用户~/.claude/settings.json个人通用偏好 · 跨项目的 deny(防 rm -rf 之类)
企业/etc/claude-code/managed-settings.json组织强制策略 · 不可被覆盖 · 管理员部署
合并语义 — allow / deny 数组是并集,不是覆盖 · 子层不能"撤销"父层的规则 · 但本地 deny 可以加在已有 allow 之上

团队配置 vs 个人配置的分工

提交到仓库 · settings.json

团队级规则

① 项目相关的 allow(Bash(npm test *)Bash(npm run lint *))
② 项目敏感文件的 deny(Read(./.env*)Read(./secrets/**))
③ 必装的 PreToolUse hook(分支保护、审计)

谁 clone 项目都自动受这些规则保护。

gitignore · settings.local.json

个人级规则

① 个人开发偏好的 allow(只你信任的命令)
② 实验性放行(临时打开某个工具试试)
③ 你自己的 hook 路径(本地审计日志)
④ 个人 ANTHROPIC_API_KEY 环境变量

不会污染队友,也不会被 review。

合并示例

# 用户级 ~/.claude/settings.json — 跨所有项目生效
{
  "permissions": {
    "deny": ["Bash(rm -rf *)", "Bash(sudo *)"]
  }
}

# 项目级 .claude/settings.json — 当前项目生效,提交到 git
{
  "permissions": {
    "allow": ["Read", "Edit", "Bash(npm test *)"],
    "deny": ["Read(./.env*)"]
  }
}

# 个人级 .claude/settings.local.json — 仅自己,gitignore
{
  "permissions": {
    "allow": ["Bash(git diff *)", "Bash(git status)"]
  }
}

# 合并后 Claude 看到的实际规则:
#   allow: Read, Edit, Bash(npm test *), Bash(git diff *), Bash(git status)
#   deny:  Bash(rm -rf *), Bash(sudo *), Read(./.env*)
无法在 local 里"取消"项目级的 deny — 想绕开必须在项目级显式调整规则,这是设计上的安全保证

Labs

4 EXERCISES
LAB 01 · 模式切换

给自己装一个安全的 plan → default 工作流

在一个你不熟的开源项目里:先用 claude --permission-mode plan 让 Claude 出方案, 切回 default 让它落地,中途用 ⇧ Tab 在 default 和 acceptEdits 之间切换, 感受三档模式各自的"问的频率"。不要试 bypass。

LAB 02 · 最小授权

为你的项目写一份合格的 permissions 块

创建 .claude/settings.json, 根据项目用到的命令写至少 5 条 allow(Bash(npm test *) 等)、 5 条 deny(Bash(rm -rf *)Bash(sudo *) 等), 2 条 ask(git push * 等)。提交到仓库。

LAB 03 · 敏感文件

把 .env 从 Claude 的视野里彻底抹掉

在你的项目里加 deny 规则(Read(./.env*)Bash(cat .env*) 等), 然后主动让 Claude 试着读 .env——确认它返回的是"被规则阻断",而不是文件内容。 再尝试用 cat / head / 拼接 shell 等方式让它绕,直到完全拦死为止。

LAB 04 · Hook 兜底

写一个 PreToolUse hook 拦截危险命令

写一个 Bash 脚本(参考 §04 模板),从 stdin 读取工具调用的 JSON, 匹配 rm -rf /mkfscurl | sh 等模式就 exit 2 阻断。 注册到 .claude/settings.json 的 hooks 字段。 然后故意让 Claude 跑一条 rm -rf ./tmp_test——确认 hook 工作。

常见问题

5 Q&A
Q1 acceptEdits 和 bypassPermissions 在实操上到底差多少? +
A

差一个数量级。acceptEdits 只放行 Edit / Write 这类"动文件"的工具,任何 Bash 调用——哪怕是 ls——依然会弹窗等你按 yes。bypassPermissions 是把所有工具全放行,包括 Bash(rm -rf /)

更关键的区别:acceptEdits 改错文件你能 git diff / git restore 救回来;bypass 让 Claude 跑了 rm 之后,文件在硬盘上就真的没了。除非你在隔离沙箱(Docker、一次性 VM、临时 worktree),否则永远不要用 bypass。

Q2 --allowedTools 命令行参数和 settings.json 的 permissions.allow 该用哪个? +
A

原则上:临时用命令行,长期用 settings.json

命令行 --allowedTools 只对当前会话生效,适合一次性脚本、CI 任务、-p 模式下精确控制权限边界。settings.json 适合写常驻规则——团队都能受益、commit 进 git 后下次启动 Claude 也自动加载。

反模式:把 settings.json 写得很宽,然后期待用 --disallowedTools 在 CI 里收紧——容易遗漏。正确做法是 settings.json 里就写最小集,有特殊场景再 --allowedTools 加点。

Q3 plan 模式真的不会修改任何文件吗?会不会有边界情况? +
A

plan 模式下 Claude 收到的工具集是被裁剪过的——Edit / Write / Bash 等写操作的工具根本不会出现在它的可调用列表里。所以即便它"想"改文件,也找不到对应工具调用。

边界情况:如果你装了 MCP 服务器,plan 模式不会自动限制 MCP 工具——某些 MCP 工具可能间接造成副作用(向数据库写、调远程 API 等)。关键 MCP 工具该在项目 settings 里另外配 deny,不要指望 plan 模式自动拦。

Q4 deny 规则能 100% 防止 Claude 读到 .env 吗? +
A

能拦住 Claude 自己的工具调用(ReadBash(cat .env) 等)。拦不住三种情况:

① 你让 Claude 写代码,代码运行时读 process.env——这是程序自身,不经过工具系统;② 你执行 npm run dev,dev server 启动时加载 .env——同样是程序行为;③ Claude 让你帮忙执行 echo $OPENAI_KEY 然后把输出贴给它——这是你自己泄漏的。

所以真正安全的做法:① 项目 .gitignore 排除 .env;② settings.json 的 deny 防 Claude 主动读;③ 写 PreToolUse hook 扫描即将写入的代码里有没有出现真实密钥模式(如 sk-[A-Za-z0-9]{20,});④ 重要密钥用密钥管理服务而不是 .env。四层叠起来才是合格的防御。

Q5 团队成员的权限配置怎么共享,又怎么允许个人差异? +
A

三层分工各司其职:

项目 settings.json(commit):全队共用的最小集——项目相关的命令 allow、敏感文件 deny、必装的 hook。这是底线,任何 clone 项目的人都自动受保护。

个人 settings.local.json(gitignore):每人的个性化偏好——你信任的额外 allow、你的本地 hook 路径、个人 API Key 环境变量。这部分别人看不到也用不到。

用户 ~/.claude/settings.json:跨项目通用——比如永远 deny rm -rf / sudo,无论在哪个项目都生效。

反模式:把团队规则写到 settings.local.json——结果新同事 clone 项目后没有,得手动配,体验很差。"该全员有的规则一定要进 commit"是底线。

复习题

5 QUESTIONS
  1. 四档权限模式按"会允许 Claude 做什么"从最少到最多排序,并说出每一档的典型场景。
  2. Bash(git diff *) 这种前缀匹配会匹配到 git diff && rm -rf / 吗?为什么这种通配符要慎用?
  3. 当一条工具调用同时匹配 allowdeny,Claude Code 会怎么处理?
  4. 项目级 .claude/settings.json、个人级 settings.local.json、用户级 ~/.claude/settings.jsonallow 数组是覆盖关系还是合并关系?
  5. PreToolUse hook 用什么 exit code 阻断工具调用?Claude 收到阻断后会做什么?

自检清单

8 ITEMS
  • 能脱口说出 plan / default / acceptEdits / bypassPermissions 四档的差异
  • 能用 ⇧ Tab 在交互中切换前三档,知道 bypass 必须用 --dangerously-skip-permissions
  • 能在 .claude/settings.json 写至少 5 条精确的 allow 规则(含 Bash 前缀匹配)
  • 能写至少 5 条 deny 规则覆盖危险命令和敏感文件
  • 理解 deny > allow > ask > 模式 的优先级顺序
  • 能区分项目 settings.json(团队共享)和 settings.local.json(个人)的使用边界
  • 能写一个 PreToolUse hook 拦截危险 Bash 命令并用 exit 2 阻断
  • 清楚 deny 拦不住"运行时读 process.env"这类边界情况,知道补救措施

延伸阅读

3 LINKS
DOCS

Claude Code — Permissions

官方权限文档:四档模式语义、permissions 字段全字段、allow/deny/ask 表达式语法。Day 09 的根参考。

REFERENCE

Settings Schema

.claude/settings.json 的完整 schema:三层文件位置、合并规则、企业级 managed settings 字段定义。

GUIDE

Hooks 安全模板

Day 10 的前置阅读:PreToolUse / PostToolUse 事件列表、stdin JSON schema、exit code 语义、社区安全 hook 仓库。

Day 10 预告

NEXT
COMING NEXT

Hooks 钩子系统 — 完整事件 · 自动格式化 · 自动测试 · Shell & HTTP

今天我们只用了 Hooks 的安全那一支——明天把它整个翻开。9 种钩子事件各自在什么时候触发、Shell 与 HTTP 两种处理器各自的取舍、自动 lint / 自动测试 / 自动 commit 怎么落地、hook 跑挂了怎么调试、阻断与撤销机制如何与 Claude 协作。规则一旦写好,记忆这件事就交还给机器。

"Least privilege is not paranoia — it's how you keep trust earnable."

DAY 09 · CLAUDE CODE 20-DAY ROADMAP · PERMISSION & SAFETY