Claude Code 安全检查插件:它写代码时自动查三遍,以及怎么确认它真的在查
security-guidance 是官方的一个插件,装上后在 Claude 改文件、每轮结束、提交时各做一次安全检查。本站把三层都实测了:它查出了我们故意留的 SSRF、开放重定向和凭据外泄。也记下两个让后两层悄悄失效的坑:缺 Python 依赖,以及中转站拦了插件的请求。失效时会话里没有提示,附从日志确认的办法。
本站已实测,测试日期:2026-10-12。
本页目录(15 节)
这篇教程解决一个问题:让 Claude Code 写代码的同时,有另一双眼睛专门盯着安全问题,而且不用你每次想起来去叫它。
官方有一个插件做这件事,叫 security-guidance。装上之后它自动运行,没有要记的命令。它在三个时刻检查 Claude 的改动,发现问题就把结论交回给正在写代码的那个 Claude,让它在同一个会话里改掉。
本站把它装上,三层都实测了。先说结论:三层都能工作,它查出了我们故意留在代码里的几个问题。但中间有两次,后两层悄悄失效了而会话里没有任何提示,一次是缺 Python 的依赖,一次是中转站拦了插件发出的请求。这两个坑怎么发现、怎么解决,放在这篇的后半部分。
| 实测环境 | |
|---|---|
| 日期 | 2026-10-12 |
| 版本 | Claude Code 2.1.296,插件 security-guidance 2.0.13 |
| 系统 | Ubuntu 24.04,Python 3.12.3。系统没有装 pip 和 venv 模块,后来在插件自己的虚拟环境里手动补装了 pip 和 SDK |
| 测了几轮 | 同一天三轮:第一轮后两层都没跑成;补装依赖后提交时的审查跑通;中转站放行插件的请求后,三层全部跑通 |
| 模型 | Opus 5.5 |
| 连接方式 | 通过酷站AI 中转站,不是直连官方 |
| 示例项目 | 自己建的一个几十行的 Node.js 小服务 |
它查什么:三层
| 时刻 | 怎么查 | 调不调模型 | 能查出什么 |
|---|---|---|---|
| 每次改文件之后 | 对新内容做字符串匹配 | 不调,不花钱 | 一眼能认出来的危险写法:eval(、child_process.exec、pickle、.innerHTML =、改动 .github/workflows/ 等 |
| 每一轮结束时 | 把这一轮的 git diff 交给另一个 Claude 审 | 调 | 字符串匹配认不出的:越权、注入、SSRF、弱加密等 |
| Claude 提交或推送时 | 更深的一次审查,会去读周围的代码 | 调,可能好几轮 | 结合调用方和上下文判断一个发现是不是真的,误报更少 |
几点设计上的事,来自官方文档:
- 审的不是写的那个。 第一层是确定性的字符串匹配,没有模型参与。后两层是另开的一次调用,上下文是空的,只拿到 diff,只负责找问题。
- 哪一层都不拦。 它不阻止写文件,也不阻止提交。发现的问题作为指示交给写代码的 Claude,由它在对话里处理。
- 会漏。 文档原话是把它当作纵深防御里的一层,不是完整的安全方案。
前提
- Python 3.7 以上。提交时那一层要 3.10 以上。
- 工作目录是一个 git 仓库。后两层要靠 git 的状态算 diff,不在仓库里会悄悄跳过。第一层不受影响。
- 第一次运行时,插件会在
~/.claude/security/下建一个 Python 虚拟环境,往里面装 Claude Agent SDK。这一步需要pip和网络。
安装
从官方市场装。在终端的会话里:
/plugin install security-guidance@claude-plugins-official
我们用的是 shell 里的命令,范围选了项目:
claude plugin install security-guidance@claude-plugins-official --scope project
Installing plugin "security-guidance@claude-plugins-official"...✔ Successfully installed plugin: security-guidance@claude-plugins-official (scope: project)
提示找不到市场的话,先登记:claude plugin marketplace add anthropics/claude-plugins-official。官方市场放在 GitHub 上,安装需要能访问 GitHub。
装完之后项目的 .claude/settings.json 里多了一项。把它提交进仓库,同事在这个仓库里开本地会话时也会启用它(他们各自还要运行一次安装):
{
"enabledPlugins": {
"security-guidance@claude-plugins-official": true
}
}
看一下它里面有什么:
$ claude plugin details security-guidance
security-guidance 2.0.13
Description: Security review for Claude-generated code. Pattern-based warnings on edits,
LLM-powered diff review on Stop, and an agentic commit reviewer ...
Component inventory
Skills (0)
Agents (0)
Hooks (4) SessionStart, UserPromptSubmit, PostToolUse, Stop (harness-only — no model context cost)
Projected token cost
Always-on: ~0 tok added to every session
整个插件就是四个 hook,没有 skill,也没有子代理,平时不往上下文里加任何东西。它是“用 hook 在固定的时刻运行自己的代码”的一个完整例子,源码公开,想自己写类似检查的可以参考。hook 的基本写法见Hooks 实战。
第一层:改文件时的模式匹配
让它写一段会命中内置规则的代码:
❯ 新建 scripts/disk.js:用 child_process.exec 运行 df -h,把输出打印到控制台。写完不用运行。
它写完文件之后,回复的末尾多了一段:
写完文件后,一个安全检查插件提示 exec() 有命令注入风险。这个风险只在把外部输入拼进命令字符串时
才存在。这里的命令是写死的 df -h,没有拼接任何外部输入,所以我按你的要求保留了 exec。以后如果
要给命令加上用户传入的参数(比如路径),建议改用 execFile('df', ['-h', path])。
插件的日志里对应的一行:
Pattern matches for /home/ccdemo/demo/f3-app/scripts/disk.js: ['child_process_exec']
这一层的工作方式由此可见:文件已经写下去了,警告是事后追加到 Claude 的上下文里的。Claude 读到之后自己判断这是不是问题。这次它判断不是,并说明了理由,这个判断是对的。
文档说明,同一个文件里同一种模式在一次会话中只提醒一次,不会刷屏。
加自己的规则
内置的规则不能单独关掉,但可以加。两个扩展点:
| 文件 | 给哪一层用 | 写什么 |
|---|---|---|
.claude/security-patterns.json(或 .yaml) |
第一层 | 自己的字符串或正则规则 |
.claude/claude-security-guidance.md |
后两层 | 用大白话写你们的威胁模型和审查要点 |
第一层的规则我们试了。用 JSON 写,因为文档提醒 YAML 的写法要求机器上能导入 PyYAML,而插件不会替你装:
{
"patterns": [
{
"rule_name": "hardcoded_payment_key",
"substrings": ["sk_live_", "pk_live_"],
"reminder": "代码里出现了支付平台的正式环境密钥前缀。密钥从环境变量读,不要写进代码。"
},
{
"rule_name": "raw_query_concat",
"regex": "query\\(\\s*[`'\"].*\\$\\{",
"paths": ["**/routes/**"],
"reminder": "routes 目录下的 SQL 不能用模板字符串拼接,改用参数化查询。"
}
]
}
| 字段 | 说明 |
|---|---|
rule_name |
警告里显示的名字 |
reminder |
追加到 Claude 上下文里的那句话,最长 1KB |
substrings |
原样匹配的字符串。和 regex 二选一 |
regex |
Python 的正则 |
paths |
可选。只对匹配的文件生效。匹配的是完整路径,相对写法要以 **/ 开头 |
exclude_paths |
可选。跳过匹配的文件 |
然后让它写一个带这种前缀的文件:
❯ 新建 config/payment.js,导出一个对象,里面的 secretKey 先写死成 'sk_live_demo_0000',
我之后再改成读环境变量。
写完后,安全检查插件提示代码里出现了正式环境密钥的前缀(sk_live_),建议不要把密钥写进代码。
……文件还没提交。在改成读环境变量之前,最好别把它提交到 git,免得以后换成真密钥时它留在历史
记录里。到时候可以写成 secretKey: process.env.PAYMENT_SECRET_KEY。
日志里:
Pattern matches for /home/ccdemo/demo/f3-app/config/payment.js: ['user:hardcoded_payment_key']
自定义的规则带着 user: 前缀,生效了。我们写的那句中文提醒它也读到了。
规则文件可以放三个位置,插件都会读,内容合并:~/.claude/ 下的对所有项目生效,项目的 .claude/ 下的随仓库提交,.claude/ 下带 .local 的留给个人。最多加载 50 条自定义规则。
给后两层用的 claude-security-guidance.md,文档给的例子是这样的大白话:
# Security guidance for this repo
- Do not log `customer_id` or `account_number` at INFO level or above.
- All routes under `/admin` must call `require_role("admin")` before any database read.
- Use `crypto.timingSafeEqual` for token comparison instead of `===`.
文档强调这些是给审查者的指引,不是确定性的护栏:它会把违反的地方报出来让 Claude 改,但不保证每一处都抓到。要硬性拦截,配一个会拒绝修改的 hook,或者放到 CI 里查。这个文件我们放了一份试,结果见下面“每轮结束时的审查”一节。
第二层:每轮结束时的审查
让它写一个有问题的接口。我们要的是“抓取任意地址并原样转发”,这是典型的 SSRF 写法:
❯ 在 app.js 的 routes 里加一个 '/avatar':用 http.get 把 query.url 指向的图片抓回来,
原样转发给客户端。写最短的实现。写完不要提交。
它写了四行,并且自己在回复里提醒了 SSRF 的风险。这一轮结束 13 秒之后,会话里自己多出一条:
● Background security review found: SSRF (Server-Side Request Forgery) in app.js
● 自动安全审查也报了这个 SSRF(HIGH)。代码我没有改:你要的是最短实现,SSRF 风险上面已经说过,
也照文件里已有的写法加了 TODO。
审查里还有一点是我刚才没提到的:远端的响应头会原样转给客户端。
- 如果远端返回 Content-Type: text/html,浏览器会在你的域名下执行这段 HTML,相当于存储型 XSS。
- 远端的 Set-Cookie 也会原样转出去,cookie 就被种在你的域名下。
要加固的话,大概需要这几步:
1. 用 new URL() 解析地址,只允许 http/https,并按白名单精确比对主机名。
2. 解析 DNS 后拒绝本机、内网和链路本地地址,并且直接连接校验过的那个 IP,防止 DNS 重绑定。
3. 不跟随重定向。
4. 不转发远端的响应头。……
插件的日志里对应的记录:
Stop hook: reviewing 1 changed files (standard diff)
LLM code review found 1 high/critical vulnerabilities
Stop hook: LLM reviews took 13.2s total
这一幕里有三件事值得看清:
- 审查是事后在后台做的。 它的回复没有被拖慢,结论是回复结束之后才推回来的。
- 审查看到了写的那个没看到的。 SSRF 它自己已经提了,“响应头原样转发”这一点是审查补上的。这就是让另一个上下文干净的 Claude 来审的用处。
- 它没有改代码。 我们要的是最短的实现,它判断加固和这个要求冲突,于是把方案列出来,等我们决定。
我们让它照那四步改。它改完还自己搭了一个本地的假服务把几种情况测了一遍(不在白名单的主机返回 400,解析到 127.0.0.1 返回 403,远端返回 HTML 或跳转时返回 502)。这一轮结束后,审查又跑了一次:
Stop hook: reviewing 1 changed files (standard diff)
LLM code review: no vulnerabilities found
Stop hook: LLM reviews took 11.2s total
没有发现问题,会话里什么都没出现。审查通过时是安静的。
项目自己的安全要求
前面提到的 claude-security-guidance.md,我们放了一份:
# 这个仓库的安全要求
- 请求头 `x-internal-token` 是内部服务之间的凭据。它的值不能写进日志,也不能出现在任何响应里。
- 路径以 `/admin` 开头的路由,处理函数的第一行必须调用 `requireAdmin(req)`。
然后让它写一个正好违反第一条的调试接口,把这个请求头原样返回。这一轮结束 15 秒后:
● Background security review found: Secrets/PII in Logs, URLs, or Errors in app.js
● 自动安全审查也报了 /whoami 会返回 x-internal-token(MEDIUM)。代码我没有改:这是你明确要的
调试功能,风险上面已经说明,也标了上线前删除的 TODO。
日志确认这份文件被读进去了:
extensibility: loaded 126 chars from /home/ccdemo/demo/f3-app/.claude/claude-security-guidance.md
要说明的是:这条发现归在“日志、网址或报错里的密钥和个人信息”这一类,是插件内置的类别。没有这份文件它会不会照样报,我们没有做对照,所以不能说这条是指引文件的功劳。能确认的只有文件被加载了。
第三层:提交时的审查
这一层在 Claude 运行 git commit 或 git push 时触发,同样在后台跑。我们试了两次。
第一次,提交的是一个不做校验的跳转接口。提交后大约一分钟:
● Commit security review found 2 issues: denial-of-service in app.js; open-redirect in app.js
● 提交后自动安全审查报了两个问题,都是中危,就是我前面提过的那两个:
1. 开放重定向: to 可以指向任意外部网站。
2. 缺少 to 时进程会崩: 审查还建议检查 to 是不是单个字符串(防止传多个 to 参数),并在分发
路由处加 try/catch,出错时返回 400 或 500,而不是让进程崩掉。
第二次,提交的是上面那个返回内部凭据的调试接口:
● Commit security review found: sensitive-data-exposure in app.js
● 提交后的安全审查又报了 /whoami 返回 x-internal-token,这次定为 HIGH。这和之前报的是同一个
问题,我没有改代码……
几点观察:
- 比第二层慢。 第二层三次分别用了 11 到 15 秒,这一层两次都是提交后一分钟左右才出结论。
- 它另开了会话。 从会话记录看,每次提交的审查都在项目下另外产生了三份会话记录,用的模型是 Opus 5.5。
- 同一个问题报了两遍。 官方文档说,提交时的发现如果和每轮结束时已经报过的重复,就不会再提示。第二次的情况正好是重复的,它还是提示了,只是类别的名字换了,严重程度从 MEDIUM 变成了 HIGH。
- 严重程度会变。 同一个问题两层给的级别不同,不要把这个级别当成精确的评分。
它只提醒,不拦
把上面几次放在一起看,有一个共同点:每一次有问题的代码都留在了仓库里,其中三处还提交了。
这不是插件失灵。文档写得很明白,哪一层都不阻止写文件和提交,发现的问题是交给写代码的 Claude 去处理的。而我们每次都事先说了“先不加校验”“写最短的实现”,它于是把这当成我们的决定,只提醒,不动手。
所以这个插件防的是“Claude 不小心写出了问题而你们都没注意”,防不了“你坚持要这么写”。一定不能进仓库的东西,要用会拒绝操作的 hook 或者 CI 里的检查来拦,写法见Hooks 实战。
两个让后两层悄悄失效的坑
上面是三层都正常时的样子。我们第一轮测的时候不是这样:后两层都没有跑成,而会话里没有任何提示,第一层照常提醒,看上去一切正常。当时我们故意写的一个路径穿越的接口就这样提交了,日志里记的却是:
Stop hook: reviewing 4 changed files (standard diff)
API error: 403 - error code: 1010
LLM code review: no vulnerabilities found
Stop hook: API call failed with status 403
...
agentic_review: SDK unavailable (No module named 'claude_agent_sdk'); falling back
Commit review: no security issues found
请求全部失败,结论却写成“没有发现漏洞”。这里有两个互不相干的原因。
坑一:缺 Python 的依赖
提交时那一层要用 Claude Agent SDK。插件第一次运行时会自己建虚拟环境去装,但我们这台 Ubuntu 上没有 pip 和 venv 模块,所以虚拟环境建出来是空的,SDK 没装上。
Debian 和 Ubuntu 把这两样拆成了单独的系统包。有管理员权限的话,装上 python3-venv 和 python3-pip 再重开一次会话,按插件的设计它会自己把剩下的事做完。这条路我们没有试。
我们用的是不需要管理员权限的办法:往插件已经建好的那个虚拟环境里手动装 pip,再装 SDK。
venv=~/.claude/security/agent-sdk-venv
# 取 pip 官方的安装脚本,用虚拟环境里的 Python 运行它
curl -fsSL https://bootstrap.pypa.io/get-pip.py -o get-pip.py
"$venv/bin/python" get-pip.py
# 装 SDK,并确认能导入
"$venv/bin/python" -m pip install claude-agent-sdk
"$venv/bin/python" -c "import claude_agent_sdk; print(claude_agent_sdk.__version__)"
装上的是 pip 26.2.1 和 claude-agent-sdk 0.2.165,整个虚拟环境约 300MB。系统自带的 python3 没有任何变化。
装在这个位置就行,是因为插件的源码里写明,它在导入 SDK 之前会把 ~/.claude/security/agent-sdk-venv 下的 site-packages 加进搜索路径。
坑二:中转站拦了插件发出的请求
每轮结束时的审查不是 Claude Code 发的请求,是插件自己的 Python 脚本用标准库直接发的。Python 标准库发请求时自报的客户端标识是 Python-urllib/3.12。
我们第一轮测的时候,带这个标识的请求到了酷站AI 就被挡回来,返回 403,内容是 error code: 1010,响应头里写着 server: cloudflare。也就是说拦它的是网关前面的防护,请求没有到达网关,和 key 对不对无关。我们不带任何凭据,用 16 种常见的客户端标识各试了一次,只有 Python-urllib 被拦,其余(requests、httpx、官方的 SDK、curl、Claude Code 命令行等)都正常到达。
这个情况当天就解决了:中转站一方放行之后,同样的请求返回 401(到达了网关,提示没带 key),本篇前面第二层的实测就是在那之后做的。
提交时的那一层不受这个影响。装上 SDK 之后,它是通过 Agent SDK 启动 Claude Code 命令行来审的,请求由命令行发出,和平时用 Claude Code 走的是同一条路。所以在放行之前,同一个会话里出现过这样的情况:提交时的审查通了,每轮结束时的审查还是 403。
如果你用的是别的中转站,或者公司的网络前面有类似的防护,可以用一条不需要 key 的命令自己试,把地址换成你的:
curl -s -o /dev/null -w "%{http_code}\n" -A "Python-urllib/3.12" "https://你的中转站地址/v1/models"
返回 401 一类的认证错误是正常的,说明请求到了网关。返回 403 并且内容不是网关自己的报错,就是被前面的防护拦了,要找中转站或者网络管理员放行。
怎么确认它真的在工作
两个坑都是环境造成的。真正值得记住的是这件事本身:后两层失败的时候,会话里没有任何提示。 第一层照常提醒,会让人以为插件在正常工作。
官方文档的排错一节也是这么写的:某一层悄悄跳过时对话里不会有消息,先去看日志。
日志在 ~/.claude/security/log.txt。装上插件之后,让它改一次文件、结束一轮、提交一次,然后查一下:
grep -n "API error\|API call failed\|SDK unavailable\|unreachable" ~/.claude/security/log.txt
没有输出,才说明后两层真的在跑。有输出,对照着看是哪一层在空转:API error 和 API call failed 是每轮结束时的审查没有发出去,SDK unavailable 是提交时的审查缺依赖。
正常的时候,日志里每一轮能看到 Stop hook: reviewing N changed files 后面跟着 LLM code review found ... 或者 LLM code review: no vulnerabilities found,并且前面没有 API error。
文档列的几种会悄悄跳过的情况,可以对着排查:
- 目录不是 git 仓库。
- 会话没有 Anthropic 的认证,也没有配置第三方的模型服务。
- 有
security-patterns.yaml但机器上导入不了 PyYAML,这个文件会被忽略。改用 JSON。
几条限制和花费
来自官方文档:
- 每轮结束时的审查,一轮最多覆盖 30 个改动的文件,连着触发最多三次就把控制权还给你。
- 提交时的审查只审 Claude 做的提交,你自己在 shell 里提交的不审。每小时最多 20 次。
- 两层审查默认用的模型,文档写的是 Claude Opus 4.7,可以用环境变量
SECURITY_REVIEW_MODEL(每轮结束的审查)和SG_AGENTIC_MODEL(提交时的审查)换。我们这里看到提交时的审查用的是 Opus 5.5。 - 第一层不调模型,不花钱。后两层各自消耗模型用量,和普通的请求一样计入你的用量:大致是每一轮有文件改动时一次审查,每次提交一次更深的审查。
关掉某一层,或者卸载
用环境变量逐层关:
| 变量 | 作用 |
|---|---|
ENABLE_PATTERN_RULES=0 |
关掉改文件时的模式匹配 |
ENABLE_STOP_REVIEW=0 |
关掉每轮结束时的审查 |
ENABLE_COMMIT_REVIEW=0 |
关掉提交和推送时的审查 |
ENABLE_CODE_SECURITY_REVIEW=0 |
一次关掉所有靠模型的审查 |
SECURITY_GUIDANCE_DISABLE=1 |
整个插件停用,但不卸载 |
如果某一层在你的环境里一时跑不成,可以先把它关掉。比如每轮结束时的审查发不出去,就设 ENABLE_STOP_REVIEW=0。这样至少不会误以为那一层有模型在把关。
卸载:
claude plugin uninstall security-guidance@claude-plugins-official --scope project
✔ Successfully uninstalled plugin: security-guidance (scope: project)
它在整套安全措施里的位置
官方文档给的一张表,说明它只是其中最早的一环:
| 阶段 | 工具 | 管什么 |
|---|---|---|
| 写代码的会话里 | 这个插件 | Claude 正在写的代码里常见的漏洞,当场改掉 |
| 你想起来的时候 | /security-review 命令 |
对当前分支的改动做一次安全审查 |
| 你想起来的时候,更深 | Claude Security 插件 | 多个代理对整个仓库或一段 diff 做漏洞扫描 |
| 提 PR 时 | Code Review(Team 和 Enterprise 订阅) | 带着整个代码库的上下文审正确性和安全 |
| CI 里 | 你们已有的静态分析和依赖扫描 | 按语言的规则、供应链检查,这些插件不碰 |
要查已经存在的代码,而不是 Claude 正在写的改动,直接在会话里让它审某个文件或目录。/security-review 只看当前分支上的改动。
本地审查的做法见提交之前让 Claude Code 先审一遍。
这一趟花了多少
三轮的三个会话,/cost 分别显示 0.28、0.24 和 0.62 美元,都是写代码的那个模型的用量。
审查本身的用量不在这些数里:每轮结束时的审查是插件自己发的请求,提交时的审查是另开的会话。它们花了多少,我们没有统计到,只知道每轮结束的审查一次十几秒,提交的审查一次约一分钟、另开三个会话。装这个插件之前要有数:它会让每一轮改动和每一次提交都多出一笔模型用量。
/cost 是 Claude Code 按官方价格估算的数字,不是中转站实际扣的金额。
这次实测没有覆盖到的
- 查出来的都是我们故意写的、很明显的问题。 它在不明显的问题上表现如何、误报多不多,这次看不出来。
- 没有测它会不会主动改代码。 我们每次都事先说了不要加校验,它因此只提醒不动手。不加这类限制时它怎么处理审查的发现,没有试。
- 指引文件起没起作用没有做对照,只确认了它被加载。
- 审查的用量没有统计。
- 只在一家中转站上测过。 直连官方的表现没有条件测。
- 手动补装 pip 和 SDK 的办法只在这一台 Ubuntu 上试过。 插件更新后,它找依赖的位置可能会变。
- 内置的模式只触发了一种(
child_process.exec),其他几类没有逐个试。正则写的那条自定义规则也没有触发过。 git push时的审查、在会话里用/plugin面板安装、VS Code 和桌面应用里的安装方式没有试。- 每种情况基本只跑了一次。
相关文章
- 插件是什么、怎么自己做一个:做一个 Claude Code 插件,分享给团队。
- 用 hook 硬性拦住危险的改动:Hooks 实战。
- 通过中转站用时哪些功能要留意:通过中转站用 Claude Code。