Claude Code 安全实战:用 Hooks、权限、Git Worktrees 防误删(2026)
TL;DR — Claude Code 出过真实的 rm -rf 数据丢失事故(2025-10 Mike Wolak 案,全机用户文件被删,他甚至没开 --dangerously-skip-permissions)。本文给出三层防御的最小可用配置:settings.json 里的 permissions.deny 兜命令前缀、PreToolUse hook 用正则二次过滤、Git worktrees 给每个 session 独立工作区。三层都加上,才算把”AI 编辑器”变成你能放心在生产分支上跑的工具。
为什么 Claude Code 需要专门的安全配置
Claude Code 默认配置很乐观。Edit、Write、Bash 这些破坏性工具,开箱就在 ask 列表里——意味着你会一直被弹窗烦,烦到大多数人最终会点开 --dangerously-skip-permissions(社区俗称 “Safe YOLO mode”),然后再也不烦了。
代价是这些:
- 2025-10 Mike Wolak 案:他让 Claude Code 从一份干净的 checkout 重建 Makefile 项目,结果模型决定先
rm -rf一个它认为是临时文件的路径,递归一路上溯到/。GitHub bug report 的日志里有几千行 “Permission denied for /bin、/boot、/etc”。他没有开--dangerously-skip-permissions——是权限系统本身没拦住。 - eesel AI 的统计:在用
--dangerously-skip-permissions的开发者里,32% 遇到过非预期的文件修改,9% 报告了真实的数据丢失或损坏。 - Anthropic 自己的研究员 Nicholas Carlini 在 2026-02 跑 16 个并行 Claude Code agent 用 Rust 写一个能编译 Linux 内核的 C 编译器时,专门在博客里加了一句括号注释:“Run this in a container, not your actual machine.”
权限弹窗的疲劳是真实问题。但解法不是关掉它,是把”该挡的硬挡死、该放的全自动放过”——这正是 deny 规则 + hooks 要做的事。
第一道墙:settings.json 权限规则
Claude Code 在 settings.json 里有一个 permissions 对象,三个数组:allow / deny / ask。评估顺序是 deny → ask → allow,第一个匹配的规则赢——所以 deny 永远盖过其他两个。
关键:
permissions是由 Claude Code 本体强制的,不是模型决定的。CLAUDE.md 里写”不要 rm -rf”完全没用,模型记不住也可以选择忽略;写在settings.json的 deny 里才是硬约束。
最小可用 deny 列表(放在项目根的 .claude/settings.json):
{
"permissions": {
"deny": [
"Bash(rm -rf *)",
"Bash(rm -fr *)",
"Bash(sudo *)",
"Bash(chmod 777 *)",
"Bash(curl * | sh)",
"Bash(curl * | bash)",
"Bash(git push --force *)",
"Bash(git push -f *)",
"Bash(git reset --hard *)",
"Bash(git clean -fd*)",
"Write(.env)",
"Write(.env.*)",
"Edit(.env)"
],
"ask": [
"Bash(git push *)",
"Bash(npm publish *)"
],
"allow": [
"Bash(npm test*)",
"Bash(pnpm test*)",
"Read",
"Glob",
"Grep"
]
}
}
三档分工:
- deny 是绝对禁止线。
rm -rf、sudo、curl | sh(远程执行未审计代码的最常见路径)、操作.env之类的凭证文件,统统拒。 - ask 留给”我自己也想看一眼再决定”的命令,比如 push、publish。
- allow 把测试、读文件这些纯增量、纯读取的操作全自动放过,把弹窗预算留给真正重要的事。
配置好后用 /permissions 命令在 Claude Code 里检查规则加载情况:它会列出所有规则以及来源的 settings.json 路径。
配置文件优先级(从下往上覆盖):
| 文件 | 作用范围 | 是否进 Git |
|---|---|---|
~/.claude/settings.json | 全局所有项目 | 否(个人配置) |
.claude/settings.json | 单个项目 | 是(团队共享) |
.claude/settings.local.json | 单个项目 | 否(gitignored) |
实战建议:deny 列表写在项目级 .claude/settings.json 里 commit 进仓库,让全团队共享;个人偏好(比如某些命令你想 ask 而别人想 allow)放 settings.local.json。
deny 规则的盲区
deny 规则只匹配你写出来的字面命令前缀。模型很容易绕:
# 这个会被 Bash(rm -rf *) 挡住
rm -rf /tmp/cache
# 但这个不会
sh -c "rm -rf /tmp/cache"
# 这个也不会
RM=rm; $RM -rf /tmp/cache
# 写进脚本再执行的,更不会
echo "rm -rf /tmp/cache" > /tmp/x.sh && bash /tmp/x.sh
deny 是第一道粗滤网,挡 80% 的常规 footgun。剩下的留给第二道墙。
第二道墙:PreToolUse Hook 主动拦截
Hook 是 Claude Code 的生命周期回调。每次工具调用前后,Claude Code 会把一个 JSON 上下文 pipe 给你的脚本,由脚本决定放行还是拦截。PreToolUse 是其中最关键的——它在工具真正执行前触发。
PreToolUse 拿到的 JSON 长这样(Bash 工具示例):
{
"session_id": "abc123",
"cwd": "/Users/marvin/myproject",
"permission_mode": "default",
"hook_event_name": "PreToolUse",
"tool_name": "Bash",
"tool_input": {
"command": "rm -rf /tmp/some/path",
"description": "Clean up cache",
"timeout": 120000
},
"tool_use_id": "unique-id"
}
你可以用任何能读 stdin + 写 stdout/stderr 的脚本处理它。退出码决定行为:
| 退出码 | 行为 |
|---|---|
| 0 | 放行(可在 stdout 输出 JSON 补充指令) |
| 1 | 不阻塞——命令照样执行,stderr 只进 debug 日志 |
| 2 | 阻塞——工具调用被取消,stderr 反馈给模型 |
反直觉的坑:Unix 惯例里 exit 1 是失败,但 Claude Code 的 hook 协议把 1 当成”非致命警告”。要 block,必须 exit 2。这是社区最常见的 hook 失效原因。
下面是一个真正能拦住 rm -rf / 类灾难命令的脚本:
#!/bin/bash
# .claude/hooks/guard-bash.sh
COMMAND=$(jq -r '.tool_input.command')
# 危险命令正则集合
DANGER='(^|[[:space:];&|])rm[[:space:]]+(-[rRf]+[[:space:]]+)?(/|~|\$HOME)([[:space:]]|/|$)'
if echo "$COMMAND" | grep -qE "$DANGER"; then
jq -n --arg cmd "$COMMAND" '{
hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: ("拦截:命令试图删除 / 或 ~:" + $cmd)
}
}'
exit 2
fi
exit 0
注册到 .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/guard-bash.sh"
}
]
}
]
}
}
记得 chmod +x .claude/hooks/guard-bash.sh。
matcher 字段支持精确匹配(Bash)、多选(Edit|Write)、正则(^Notebook)、MCP 通配(mcp__memory__.*)。
Hook 能做的其他事
- PostToolUse:工具执行之后触发,能拿到
tool_response。常见用法:每次 Edit/Write 之后自动跑 prettier、eslint,把 fix 结果通过 stdout 反馈给模型。 - UserPromptSubmit:用户回车之前拦一道。可以做敏感关键词检测(“线上数据库”、“prod 表”),让模型先确认。
- SessionStart:在会话开始时注入额外上下文(比如最近的 commit log、当前分支)。
Hook 的官方事件列表挺长——session、turn、tool、subagent、worktree 都有钩子。完整清单看 Anthropic 官方文档。
第三道墙:Git Worktrees 隔离工作区
前两道墙拦的是”模型干了不该干的事”。worktree 拦的是另一类问题:多个 Claude session 互相覆盖文件。
git worktree 是 Git 本身的功能——允许同一个仓库 checkout 到多个不同的工作目录,各自指向不同分支,共享同一份对象库。Claude Code 官方推荐用它跑并行 session:
# 假设你的主仓库在 ~/myproject
cd ~/myproject
# 给三个并行任务各开一个 worktree
git worktree add ../myproject-feature-auth feature/auth
git worktree add ../myproject-bugfix-cors bugfix/cors
git worktree add ../myproject-refactor-db refactor/db
# 在每个目录开一个 Claude Code session
cd ../myproject-feature-auth && claude
# 另一个 terminal
cd ../myproject-bugfix-cors && claude
每个 session 看到的文件状态完全独立——A session 改的文件不会出现在 B session 的 Read 结果里,更不会被 B session 的 Edit 覆盖。Git 历史和 remote 还是共享的,merge / rebase 照常。
实际限制:
- 主流团队(截至 2026 年中)在用 4–8 个并行 worktree 每开发者。超过这个数,瓶颈通常不是 Claude,而是你 review 不过来。
- Anthropic 自己的建议是 2–4 个并行 session 起步。
- 大仓库上 5 个以上 session 会撞 API 速率限制,并且 review 体验会塌。
worktree 不解决的事
worktree 只隔离文件系统。下面这些是共享的:
- 环境变量:所有 session 读同一份 shell env。一个 session
export DATABASE_URL=...会污染其他 session。 - 本地数据库:两个 session 跑 migration 会互相踩,写测试数据会污染对方的 fixtures。这是实践里最容易翻车的点。
- 网络端口:两个 session 都
npm run dev抢 3000 端口,第二个起不来。 - 全局 npm/pnpm cache:装包时可能互相覆盖 lockfile。
要彻底隔离,要么每个 worktree 配独立的 .env 指向独立的数据库实例(推荐 Docker compose 给每个 worktree 起一份),要么把每个 session 跑在容器里(社区在用 Devcontainer + Claude Code 的方案)。
auto mode:比 --dangerously-skip-permissions 更安全的 YOLO
Anthropic 2026 推出了 auto mode 作为 --dangerously-skip-permissions 的替代。它跳过权限询问让 Claude 一路跑到底,但仍然受 deny 规则约束——这意味着即使在”一路自动”模式下,你写的 Bash(rm -rf *) deny 规则照样会拦住模型。
实战推荐:
- 日常开发:默认模式,深 deny + 几条 ask,依靠 hook 兜底。
- 长任务(比如让 Claude 跑一晚上重构):auto mode + 完整 deny 列表 + hook,外加跑在容器里。
- 真正需要无人值守的探索性任务:
--dangerously-skip-permissions+ 容器 + 容器里没挂宿主机敏感目录。
不要直接在生产代码所在的工作目录上开 --dangerously-skip-permissions。
三层防御的组合套路
把上面三件事拧成一个最小可用模板:
项目根 .claude/settings.json:
{
"permissions": {
"deny": [
"Bash(rm -rf *)",
"Bash(sudo *)",
"Bash(curl * | sh)",
"Bash(curl * | bash)",
"Bash(git push --force *)",
"Bash(git reset --hard *)",
"Write(.env)",
"Write(.env.*)"
],
"ask": ["Bash(git push *)"],
"allow": ["Read", "Glob", "Grep", "Bash(pnpm test*)", "Bash(npm test*)"]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/guard-bash.sh"
}
]
}
]
}
}
项目根 .claude/hooks/guard-bash.sh(前面给的脚本)。
工作流:每个独立任务开一个 worktree(git worktree add ../proj-<task> <branch>),在 worktree 里启动 Claude Code,长任务加 auto mode。
这套配置不会让 Claude Code 慢下来——allow 列表把大部分高频操作直接放过;deny 列表是 O(1) 字符串匹配;hook 只在 Bash 调用时跑一次几毫秒的脚本。但它会真的拦住模型脑抽时干出来的破坏性操作。
国内开发者:用 ofox API 跑 Claude Code
Claude Code 默认连 Anthropic 官方 API,国内网络不稳。把 ANTHROPIC_BASE_URL 指向 ofox 后即可在国内直连,所有 hooks、permissions、worktrees 配置完全不受影响——它们都在客户端。详细的国内接入步骤、模型可用性和定价对比,看 Claude Code 国内使用 + Opus 4.6 编程体验 和 Claude Opus 4.7 完全指南。
横向对比 Claude Code 与 Cursor、Windsurf 等同类工具的取舍,参考 Vibe Coding 工具横评。
总结
权限弹窗烦人是真的,但解法不是关掉它。三层加起来才稳:
settings.json权限规则——粗滤网,挡 80% 的常规误操作。deny 永远赢,写进项目根 commit 进 Git。- PreToolUse hook——细滤网,用脚本做正则匹配兜住 deny 漏掉的变种。记住用 exit 2 才能 block。
- Git worktrees——并行 session 不互相踩文件,但环境变量/数据库要单独想办法。
配完之后你才能在生产分支上放心开 Claude Code,而不是开完一晚上心惊胆战地查 git status。

