用 Claude Code 久了,几乎每个人都会经历同一个过程:第一次弹出「是否允许执行这条命令」,为了不被打断,顺手点「Yes, and don't ask again」。三个月后再看,允许列表里躺着几十条规则,有些是 Bash(git:*),有些是 Read(**),还有几条是当初调试时临时放行的 Bash(curl:*)。这些规则不会过期,也不会提醒你,它们安静地留在配置文件里,成为一条条自动放行的通道。
这篇教程做一件事:把累积下来的白名单翻出来,一条条看清,删掉不该留的,把「全自动放行」改回「按命令授权」,最后加一道提交前检查兜底。
适用场景
适合已经用 Claude Code 跑过几个真实项目、允许规则明显堆积的开发者,也适合要把 Claude Code 引入团队仓库、需要先做一次安全收口的场景。如果团队里有人把 .claude/settings.json 提交进了仓库,或者有人习惯加 --dangerously-skip-permissions 启动,这套流程尤其值得走一遍。
环境与前置条件
- 操作系统:macOS / Linux / Windows(WSL 或 Git Bash 均可),下文命令以 bash/zsh 为准
- 已安装 Claude Code,并能正常启动交互会话;版本以官方文档当前版本为准,命令行为以本机
claude --help输出为准 jq:用来读 JSON 配置,brew install jq或apt install jqgitleaks或trufflehog:用来扫密钥,二选一即可,安装方式以各自官方仓库说明为准git:仓库类检查需要- 磁盘/内存:本流程本身几乎不吃资源,但如果要对大型仓库跑全量密钥扫描,建议留出 2GB 以上空闲内存,扫描时间与仓库历史长度成正比
先说一句重要的:下面所有操作都会改动配置文件,动手前先备份。
```bash
mkdir -p ~/claude-audit-backup
cp ~/.claude.json ~/claude-audit-backup/ 2>/dev/null || true
cp ~/.claude/settings.json ~/claude-audit-backup/user-settings.json 2>/dev/null || true
cp .claude/settings.json ~/claude-audit-backup/project-settings.json 2>/dev/null || true
cp .claude/settings.local.json ~/claude-audit-backup/project-local-settings.json 2>/dev/null || true
ls -la ~/claude-audit-backup
```
最后一条 ls 能看到备份文件,就说明备份这一步成功了。
分步骤部署
第 1 步:搞清楚这台机器上有几层配置
Claude Code 的权限规则不是只存在一个地方。按优先级从高到低,大致是:企业托管策略 → 命令行参数 → 项目本地设置 .claude/settings.local.json → 项目共享设置 .claude/settings.json(通常提交进仓库)→ 用户全局设置 ~/.claude/settings.json。具体层级名称与优先级以官方文档的说明为准。
另外还有一个历史遗留位置:~/.claude.json。早期版本里,交互会话中选「don't ask again」留下的允许项,会写进这个文件的 projects 节点下,按项目路径分组。不同版本、不同配置方式会让规则落在不同文件里,所以每个都要查。
```bash
ls -la ~/.claude.json ~/.claude/settings.json 2>/dev/null
ls -la .claude/settings.json .claude/settings.local.json 2>/dev/null
```
输出中存在的文件就是你要审计的对象。不存在的文件不用管,后面命令用 2>/dev/null 跳过即可。
顺便查一下有没有把「跳过全部权限确认」写死进 shell 配置:
```bash
grep -rn "dangerously-skip-permissions" ~/.zshrc ~/.bashrc ~/.bash_profile ~/.profile 2>/dev/null
```
有输出就要留意,这等于每次启动都关掉了整套权限系统。
第 2 步:把当前生效的允许规则全部导出
在会话里可以用 /permissions 斜杠命令查看和管理规则,这是最直观的方式。但要做批量比对,还是命令行更快。
用户全局设置:
```bash
jq '.permissions // {}' ~/.claude/settings.json
```
项目共享设置和本地设置:
```bash
jq '.permissions // {}' .claude/settings.json
jq '.permissions // {}' .claude/settings.local.json
```
输出里通常包含 allow、deny、ask、additionalDirectories、defaultMode 这几个键。如果 defaultMode 显示为 bypassPermissions,说明当前是「什么都不问」模式,这是本轮审计的重点对象。
再把 ~/.claude.json 里的历史遗留项捞出来:
```bash
jq -r '.projects // {} | to_entries[] | "\(.key)\t\((.value.allowedTools // []) | join(", "))"' ~/.claude.json 2>/dev/null | grep -v '^\s*$'
```
每行是「项目路径 + Tab + 逗号分隔的允许工具列表」。现在你有了三份清单,接下来做合并去重。
```bash
{
jq -r '.permissions.allow[]?' ~/.claude/settings.json 2>/dev/null
jq -r '.permissions.allow[]?' .claude/settings.json 2>/dev/null
jq -r '.permissions.allow[]?' .claude/settings.local.json 2>/dev/null
jq -r '.projects // {} | to_entries[] | .value.allowedTools[]?' ~/.claude.json 2>/dev/null
} | sort -u > /tmp/allowed.txt
wc -l /tmp/allowed.txt
cat /tmp/allowed.txt
```
wc -l 的数字就是规则总数。几十条以上基本可以确定有历史残留。
第 3 步:按风险给规则分级
把 /tmp/allowed.txt 从头看一遍,按下表对照。判断标准不是「工具名好不好听」,而是「这条规则能不能间接执行任意代码或读取任意文件」。
| 风险 | 典型规则 | 为什么危险 |
|---|---|---|
| 极高 | Bash、Bash(*)、Bash(python:*)、Bash(node:*)、Bash(bash:*) | 解释器类命令等于任意代码执行,后面的参数完全不受控 |
| 极高 | Read()、Read(//)、Write()、Edit() | 可读 .env、~/.ssh/id_rsa、~/.aws/credentials |
| 高 | Bash(curl:*)、Bash(wget:*)、WebFetch | 可把读到的内容发到外部地址,数据外带 |
| 高 | Bash(git push:*)、Bash(npm publish:*)、Bash(docker:*) | 影响远端、生产环境或发布流程 |
| 高 | Bash(sudo:*)、Bash(chmod:*)、Bash(rm:*) | 直接改动系统状态 |
| 中 | Bash(git:*)、Bash(npm:*)、Bash(make:*) | 覆盖面过宽,子命令里可能藏有副作用 |
| 低 | Bash(git status)、Bash(npm run lint)、Bash(pytest:*) | 参数固定、副作用可控 |
关于 :* 语法要特别说明:Bash(npm run test:*) 是前缀匹配,Bash(*) 是匹配一切,而写成 Bash(rm -rf *) 这种「空格 + 星号」的形式并不符合规则语法,会被当成普通字符串精确匹配,等于没写。正确写法是 Bash(rm -rf:*)。
还需要清醒一点:前缀匹配本质上是字符串匹配。遇到 &&、|、反引号、$(...) 这类复合结构时,具体怎么判定依赖当前版本的实现。所以不要把这些规则当成安全边界,它们的目标是减少打扰,不是防住有意为之的攻击。真正的兜底放在第 7 步的提交前检查上。
第 4 步:重写三份配置
原则是:全局只留通用低风险项,项目共享设置里写团队共识,个人临时放行一律进 .claude/settings.local.json。
先处理用户全局 ~/.claude/settings.json,只保留读类和查询类:
```json
{
"permissions": {
"defaultMode": "default",
"allow": [
"Read(./src/**)",
"Bash(git status)",
"Bash(git diff:*)",
"Bash(git log:*)"
]
}
}
```
再处理项目共享 .claude/settings.json,把危险操作显式列入 deny,需要人确认的列入 ask:
```json
{
"permissions": {
"defaultMode": "default",
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Read(./secrets/**)",
"Read(~/.ssh/**)",
"Read(~/.aws/**)",
"Bash(curl:*)",
"Bash(wget:*)",
"Bash(sudo:*)",
"Bash(rm -rf:*)",
"Bash(chmod 777:*)",
"WebFetch"
],
"ask": [
"Bash(git push:*)",
"Bash(npm install:*)",
"Bash(docker:*)"
],
"allow": [
"Bash(npm run lint)",
"Bash(npm run test:*)",
"Bash(pytest:*)"
]
}
}
```
这里的关键动作是:把 python、node、bash、sh 这类解释器从 allow 里彻底删掉。只要留了其中之一,前面所有 deny 规则都可以绕过——一句 python -c "..." 就够了。
defaultMode 的可选值以官方文档为准,常见的有 default(按规则询问)、acceptEdits(自动接受文件编辑)、plan(只读规划)、bypassPermissions(全部放行)。日常开发用 default 就够,配置改动频繁的探索阶段可以临时开到 plan。
~/.claude.json 里的历史 allowedTools 不建议手改整个文件(这个文件结构复杂、容易写坏)。更稳妥的做法是:先在 ~/.claude.json 里定位到对应项目的 allowedTools 数组,只删掉高风险项;或者干脆在会话里用 /permissions 逐条移除。改完后必须用 jq 校验一遍:
```bash
jq empty ~/.claude.json && echo "JSON OK"
```
输出 JSON OK 才算没写坏。
第 5 步:清理 shell 层面的绕过入口
```bash
grep -rn "skip-permissions\|bypassPermissions\|acceptEdits" \
~/.zshrc ~/.bashrc ~/.bash_profile ~/.profile \
.claude/settings.json .claude/settings.local.json ~/.claude/settings.json 2>/dev/null
```
对每一条命中项问自己:这是有意为之,还是当初图省事留下的?有意的保留并写进 README 说明;图省事的删掉。
第 6 步:扫描可能已经泄露的密钥
规则收紧只能管住「以后」。过去那些自动放行的命令,可能已经把密钥打进了终端、写进了日志、甚至提交进了仓库。
先扫工作区:
```bash
gitleaks detect --source . --verbose --redact
```
或者用 trufflehog:
```bash
trufflehog filesystem . --only-verified
```
(各工具的当前参数名以 --help 为准,不同大版本之间有过调整。)
再扫 Git 历史——很多人只扫当前文件,忘了历史提交里还留着三个月前的 .env:
```bash
gitleaks detect --source . --log-opts="--all" --verbose --redact
```
然后扫 Claude Code 自己的会话记录。这些会话文件里保存着完整的对话与工具输出,包括被打印过的环境变量、cat .env 的结果等等:
```bash
grep -rEl 'sk-[A-Za-z0-9_-]{16,}|AKIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}' ~/.claude/projects 2>/dev/null
```
有文件名输出,就说明会话记录里存在疑似密钥。处理方式:先把对应的真实密钥轮换掉(这一步不能省,因为文件可能已经被同步或备份),再清理记录。
确认 .gitignore 里有这些条目,避免下一次又把本地配置和密钥文件提交上去:
```bash
grep -qxF '.claude/settings.local.json' .gitignore || echo '.claude/settings.local.json' >> .gitignore
grep -qxF '.env' .gitignore || echo '.env' >> .gitignore
grep -qxF '.env.*' .gitignore || echo '.env.*' >> .gitignore
cat .gitignore | tail -5
```
第 7 步:加一道提交前检查
前面说过,前缀匹配的规则挡不住精心构造的命令。所以真正可靠的那道闸门放在 git 提交这一侧。
先写扫描脚本:
```bash
mkdir -p .githooks
cat > .githooks/pre-commit <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
1) 密钥扫描(只扫暂存区)
if command -v gitleaks >/dev/null 2>&1; then
if ! gitleaks protect --staged --redact --verbose; then
echo "[pre-commit] 暂存区检测到疑似密钥,提交已中止。" >&2
echo "[pre-commit] 请从代码中移除密钥并轮换,不要用 --no-verify 跳过。" >&2
exit 1
fi
fi
2) 禁止提交本地个人配置
if git diff --cached --name-only | grep -qx '.claude/settings.local.json'; then
echo "[pre-commit] .claude/settings.local.json 是个人的,不要提交。" >&2
exit 1
fi
3) 禁止把 bypassPermissions 带进共享配置
if git diff --cached --name-only | grep -qx '.claude/settings.json'; then
if git show :.claude/settings.json | grep -q 'bypassPermissions'; then
echo "[pre-commit] 共享设置里出现 bypassPermissions,已中止。" >&2
exit 1
fi
fi
exit 0
EOF
chmod +x .githooks/pre-commit
git config core.hooksPath .githooks
```
这几步分别在做什么:core.hooksPath 把钩子目录指到 .githooks,这样脚本能随仓库分发,不必每个人手动拷到 .git/hooks;脚本第 1 段扫暂存区密钥,第 2 段挡个人配置,第 3 段挡「全自动放行」被误提交。
如果团队已经在用 pre-commit 框架,把 gitleaks 作为一条 hook 写进 .pre-commit-config.yaml 也可以,效果等效。
最后,给 Claude Code 自己也加一道:用 PreToolUse 钩子在 Bash 命令执行前做一次拦截。在 .claude/settings.json 里加:
```json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": ".githooks/guard-bash.sh" }
]
}
]
}
}
```
钩子脚本从标准输入读 JSON,退出码 2 表示拦截:
```bash
cat > .githooks/guard-bash.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail
input="$(cat)"
cmd="$(printf '%s' "$input" | jq -r '.tool_input.command // empty')"
patterns=(
'rm[[:space:]]+-rf'
'git[[:space:]]+push'
'npm[[:space:]]+publish'
'curl[[:space:]]'
'wget[[:space:]]'
'>[[:space:]]*/etc/'
)
for p in "${patterns[@]}"; do
if printf '%s' "$cmd" | grep -Eq "$p"; then
echo "已拦截:命中禁止模式 [$p]。如确需执行,请自行在终端运行。" >&2
exit 2
fi
done
exit 0
EOF
chmod +x .githooks/guard-bash.sh
```
注意 jq 在钩子环境里可能不在 PATH 上,如果报错,把 jq 换成绝对路径(command -v jq 查到的那个)。
验证部署是否成功
逐条验证,每条都有明确的预期结果。
1. JSON 没写坏
```bash
jq empty ~/.claude.json ~/.claude/settings.json .claude/settings.json .claude/settings.local.json 2>/dev/null
echo "exit=$?"
```
预期输出 exit=0。任何非 0 都说明某个文件语法有误,回去改。
2. 高风险规则已经清掉
```bash
jq -r '.permissions.allow[]?' ~/.claude/settings.json .claude/settings.json .claude/settings.local.json 2>/dev/null \
| grep -E '^Bash$|^Bash\(\*\)|Bash\((python|node|bash|sh|sudo|curl|wget):\*\)'
```
预期:无任何输出。有输出就是还没清干净。
3. defaultMode 已回到按需授权
```bash
jq -r '.permissions.defaultMode // "default"' ~/.claude/settings.json .claude/settings.json .claude/settings.local.json 2>/dev/null
```
预期:全部输出 default(或你刻意设置的 plan),不出现 bypassPermissions。
4. 会话内权限视图一致
启动 claude,输入 /permissions。预期:允许列表条数与第 2 步导出的清单一致,高危项已消失。
5. 钩子能正确拦截
```bash
echo '{"tool_name":"Bash","tool_input":{"command":"git push origin main"}}' | .githooks/guard-bash.sh
echo "exit=$?"
```
预期:打印拦截提示,exit=2。再用一条安全命令验证放行:
```bash
echo '{"tool_name":"Bash","tool_input":{"command":"git status"}}' | .githooks/guard-bash.sh
echo "exit=$?"
```
预期:无输出,exit=0。
6. 提交前检查真的会拦
造一个假密钥文件测试(用完删掉):
```bash
printf 'AWS_KEY=AKIA%s\n' 'ABCDEFGHIJKLMNOP' > /tmp/fake-secret.txt
cp /tmp/fake-secret.txt ./fake-secret.txt
git add fake-secret.txt
git commit -m "test hook" || echo "提交被拦截,符合预期"
git restore --staged fake-secret.txt 2>/dev/null || git reset HEAD fake-secret.txt
rm -f fake-secret.txt /tmp/fake-secret.txt
```
预期:输出「提交被拦截,符合预期」。如果居然提交成功了,检查 git config core.hooksPath 是否生效。
7. 实际会话行为验证
在 claude 里让它执行 curl https://example.com。预期:被 deny 直接拒绝,或被钩子拦截,而不是静默执行。
常见报错与解决
报错:parse error: Invalid numeric literal at line 1(jq 读取 settings.json 时)
→ 原因:配置文件里写了注释或留了尾随逗号。JSON 标准不支持这两样,而部分编辑器会自动加注释。
→ 解决:用编辑器把注释和多余的逗号去掉,再校验:
```bash
jq empty .claude/settings.json && echo OK
```
如果确实想保留注释,可以换成支持 JSONC 的工具读取,但配置文件本身建议保持纯 JSON。
报错:改了 .claude/settings.json,但会话里 /permissions 看不到变化
→ 原因:三种可能——在错误的目录启动了会话(项目设置是按当前工作目录查找的);改动被更高优先级的配置覆盖(比如本地设置或企业策略里有同名规则);会话没有重新加载配置。
→ 解决:
```bash
pwd
ls -la .claude/
jq '.permissions' .claude/settings.local.json 2>/dev/null
```
确认当前目录就是仓库根目录,且本地设置里没有冲突规则;然后退出并重新启动 claude 会话。
**报错:deny 规则写了但没挡住,比如 Bash(rm -rf *) 形同虚设**
→ 原因:规则语法写错了。「空格 + *」不是前缀匹配的通配写法,会被当成精确字符串匹配,实际永远匹配不上。类似的还有 Bash(git *)、Read(*.env)。
→ 解决:前缀匹配统一用 :* 结尾,路径匹配用 gitignore 风格:
```json
{
"permissions": {
"deny": ["Bash(rm -rf:*)", "Read(./.env)", "Read(./secrets/**)"]
}
}
```
报错:钩子不触发,命令照样执行
→ 原因:matcher 写错(比如写成了小写 bash);脚本没有执行权限;hooks 所在的 settings 文件 JSON 不合法,整份配置被忽略。
→ 解决:
```bash
jq empty .claude/settings.json && echo "JSON OK"
chmod +x .githooks/guard-bash.sh
ls -l .githooks/guard-bash.sh
```
再把 matcher 改成 Bash(大小写敏感),重启会话。
报错:钩子里报 jq: command not found
→ 原因:钩子执行时的 PATH 与交互式 shell 不同,brew 装的工具常在 /opt/homebrew/bin,不在默认 PATH 里。
→ 解决:在脚本里用绝对路径:
```bash
JQ_BIN="$(command -v jq || echo /usr/bin/jq)"
cmd="$(printf '%s' "$input" | "$JQ_BIN" -r '.tool_input.command // empty')"
```
报错:gitleaks: unknown flag --protect
→ 原因:gitleaks 在大版本更新中调整过子命令结构,旧参数被移除。
→ 解决:以本机 gitleaks --help 输出为准,改用当前版本对应的子命令(如 gitleaks git --staged 或 gitleaks dir),并同步更新 .githooks/pre-commit。
后续维护
备份:把三份配置纳入版本管理是个好办法,但只提交脱敏后的模板。建议在仓库里放 .claude/settings.example.json,真实文件靠本地生成。仓库之外,定期把 ~/.claude/settings.json 和 ~/.claude.json 加入你现有的 dotfiles 备份流程即可。
升级:Claude Code 迭代较快,权限规则的语法、defaultMode 的取值、钩子的事件名都可能调整。每次升级后跑一遍本文「验证部署是否成功」里的第 2、3、4 条,三分钟就能确认配置没失效。升级前先备份两份 settings.json。
定期审计:给自己定个节奏,比如每月第一周跑一次第 2 步的导出命令,看看规则总数有没有反弹。数量明显上涨,说明有人在频繁点「don't ask again」,这时该做的是把那类命令整理成明确的 allow 规则,而不是放任它自动长。
日志与监控:把 gitleaks 的扫描结果接进 CI,只报新增问题、不阻断历史存量,这样团队不会被旧账淹没。同时留意 ~/.claude/projects 的目录体积,会话记录会持续增长,长期不清可能占掉可观磁盘;清理前先确认里面没有被误打印的密钥,清完再删。
团队传播:权限收紧这件事,靠一个人做没用。把 .githooks/ 和 .claude/settings.json 一起提交进仓库,在 README 里写清三句话:本地配置不要提交、bypassPermissions 不要进共享配置、钩子报错不要用 --no-verify 绕过。新人克隆仓库后 git config core.hooksPath .githooks 一行就能跟上,成本很低。
