把AI编码代理当成安全审计员来用,听起来很高效,但真正落地上手之后你会发现,它要么漏掉关键风险,要么把正常代码当成漏洞疯狂误报。我最近做的security-audit-skill项目,就是为了解决这个"能用但用不精"的问题。简单来说,这是一个面向大模型编码代理的安全审计技能包,里面包含整套审计指令、扫描脚本、检查清单和报告模板,让代理从"能回答安全问题的聊天机器人"变成一个"知道该按什么流程查代码、查依赖、查配置"的审计工具。如果你正在给 AI 代理写 skill,或者想用 Codex、Claude Code、OpenCode 这类工具做安全审计,这篇博文里的设计思路、目录结构和踩坑记录可以直接拿去参考。
1. 从 security-audit-skill 这个项目说起:到底在解决什么问题
1.1 AI 代理安全审计,为什么不能只靠"临场提问"
最初我用大模型做代码安全审计时,走的是最朴素的路子:把仓库代码贴给对话窗口,然后问一句"有没有漏洞"。结果不稳定到让人怀疑人生——同一个仓库,上午问和下午问,给出来的结论重点完全不同;隔一次对话,它又会盯着某个无关紧要的console.log说"存在信息泄露风险"。
后来我想明白了问题在哪:大模型本身是"概率语言模型",不是"确定性扫描器"。它擅长的是理解和归纳,而不是稳定复现同一套检查流程。代码安全审计恰恰是最依赖"标准化流程"的工作,什么时候查什么、查出来的东西怎么分级、从哪里开始查、怎么确认误报,每一步都要有约定。你把流程交给对话运气,结果当然飘忽不定。
这就是security-audit-skill要补的位:它不是让模型"自由发挥"去做审计,而是把审计方法论固化成一个可复用的技能包。模型调用这个 skill 之后,会按照既定顺序执行:先收集项目清单,再扫描依赖和密钥,接着查危险 API 与注入点,最后汇总成带严重级别的报告。整个流程被"强制"住了,模型只负责在每个环节做判断和补充解释,而不是从头自由发挥。
1.2 这个 skill 适合谁、在什么场景下真正有用
从我的实际使用情况看,下面这几类人最值得尝试:
| 使用人群 | 典型场景 | 能解决的问题 |
|---|---|---|
| 独立开发者 | 自己维护的开源项目,没预算买商业扫描工具 | 在发版前快速过一遍常见风险 |
| 安全工程师 | 拿到一个不熟悉的代码仓库,需要前置摸底 | 减少人工逐文件翻代码的时间 |
| DevSecOps 工程师 | 把安全审计接入 CI/CD 或 PR 检查 | 让 AI 代理按统一标准执行检查 |
| 团队技术负责人 | 给团队引入 AI 编码代理,又担心代码质量 | 给代理装上"安全红线"意识 |
需要注意,它不能替代真正的 SCA 商业产品或者人工代码审计。像业务逻辑漏洞、复杂的权限绕过,这类问题靠静态扫描和模型推理很难百分百命中。它的价值在于把"低垂的果实"快速摘掉——硬编码密钥、过期的高危依赖、SQL 拼接、危险函数调用,这些高频且模式化的问题,用 skill 来查效率非常高。
1.3 这个项目最初的形态是什么样的
项目刚起步时,我没有直接写代码,而是先整理了一份"人工安全审计清单"。我把平时做渗透测试和代码审计时的检查项,按"依赖安全、密钥泄漏、注入风险、危险函数、配置风险"分成五类,然后逐条想"这条能让 AI 代理怎么判断"。
这一步特别关键。因为大模型不像传统扫描器那样读正则就行,它需要的是"判断规则 + 判断上下文"。比如"检查 SQL 注入",你不能只给一条正则,你得告诉它:哪些参数会进入数据库查询方法、这些方法的参数是否来自用户请求、前面有没有经过参数化处理。这些都是上下文判断,模型擅长,但你得把上下文供给它。所以我为每个检查项都写了对应的references文件,这为后续脚本开发打下了基础。
2. skill、agent、脚本,名字虽多但别搞混
2.1 skill 和 agent 到底有什么区别
我在搜索相关资料时,看到最多的疑问就是"skill 和 agent 的区别"。网上说法五花八门,不少文章把这两个概念混成一团,实际上去做项目时根本不能稀里糊涂。我的理解是这样的:
Agent 是拿主意、做决策和执行循环的主体。它能看到用户需求、决定调用什么工具、分几步完成任务、自己做中间检查,最后交付结果。它是一个"干活的人"。
Skill 是这个人手里的"标准作业手册"。它不负责决策,它只负责告诉 agent:面对这类任务时,按什么步骤走、用哪些工具、输出什么样的格式。Agent 可以在不同场景下调用不同 skill,一个 agent 完全可能挂载十几个 skill。
脚本则是更底层的执行单元。skill 内部可以包含脚本,用来跑确定性扫描、生成中间结果,但不是说"有个脚本就叫有了 skill"。脚本没有指令上下文,它不知道什么时候该跑、跑完输出给谁看、结果要怎么组织。
| 层面 | 职责 | 典型例子 |
|---|---|---|
| Agent | 决策、规划、多轮执行、总结 | Claude Code、Codex CLI、OpenCode |
| Skill | 任务方法论与流程手册 | security-audit-skill、会议纪要素材 |
| 脚本 | 具体执行某项原子操作 | 密钥扫描脚本、依赖版本检查脚本 |
打个不太严谨的比方:Agent 是你请来的顾问,skill 是他手里的检查表,脚本是检查表里那台用来测量的仪器。顾问决定做哪几项检查,仪器负责把读数测出来,检查表保证每一项都不漏。缺了 skill,顾问容易凭经验随机发挥;缺了脚本,检查全凭肉眼效率太低。
2.2 主流框架里的 skill 通用结构
目前我接触过的三个主流编码代理里,对 skill 的支持方式不完全一样,但底层思想高度一致:都是在一个约定目录下放结构化文件,模型启动后读取并理解它。
- Claude Code:使用
.claude/skills/<skill-name>/SKILL.md或用户目录下~/.claude/skills/。SKILL.md 带有 YAML 前置元信息,描述何时启用、用什么工具。 - Codex:较新的 Codex CLI 支持
~/.codex/skills和项目级.codex/skills,同样读取 SKILL.md 作为入口。 - OpenCode:支持
~/.config/opencode/skill/与项目级.opencode/skill/,用类似机制加载技能。
通用结构基本是:
SKILL.md:技能的入口文件,包含名称、描述、触发条件、步骤概述。scripts/:存放可执行脚本,供模型按需运行。references/:静态参考资料,比如规则表、模式库、最佳实践文档。templates/:输出模板,比如审计报告模板。assets/:其他辅助文件。
我建议不管目标是哪个框架,文件结构都按这个通用形式组织。这样后续迁移框架时,只需要改路径和少量元信息,核心逻辑不用重写。
2.3 设计 skill 时的边界感:哪些不该写进 skill
见过很多刚上手的朋友,恨不得把 AI agent 能做的所有事情都写进 skill,结果把 SKILL.md 写成了几百行的大型文档。这个方向是错的。
Skill 里应该写的是"稳定的方法论",而不是"每个问题的标准答案"。比如安全审计 skill,稳定的方法论是:收集项目结构、扫描密钥、检查依赖、查危险函数、汇总报告。这五步在几乎任何代码仓库里都适用,属于可复用流程。
而那些会变的东西——比如某个项目特殊的框架版本、某个团队自定义的安全规范、某次审计的具体目标范围,都不该写死在 skill 里。它们应该由 agent 在执行时从用户对话或项目配置中读取,动态拼接到执行流程里。
我当时给自己定了一条规则:能在运行时获取的信息,就不要写进 skill 文件。写进 skill 的内容必须同时满足两个条件:跨项目复用、执行路径相对稳定。违反这条规则,skill 很快就会变得臃肿且难以维护。
3. security-audit-skill 的目录结构与元信息设计
3.1 一套可以直接复制的目录结构
我最终落地的目录结构是这样的:
security-audit-skill/ ├── SKILL.md ├── scripts/ │ ├── collect_manifest.py │ ├── run_secret_scan.py │ ├── run_dep_check.py │ └── generate_report.py ├── references/ │ ├── owasp_top10_mapping.md │ ├── secret_patterns.json │ ├── dangerous_apis.txt │ └── risk_levels.md ├── rules/ │ ├── context_rules.md │ └── output_rules.md └── templates/ └── audit_report_template.md每个文件都有明确职责:
collect_manifest.py:自动识别项目类型,收集package.json、requirements.txt、go.mod、Cargo.toml等依赖清单,以及源码文件分布。它是后续扫描的基础。run_secret_scan.py:基于正则与熵检测扫描硬编码密钥,包括 API Key、Token、私钥块等。run_dep_check.py:读取依赖清单中关键包版本,与内置漏洞库进行比对。dangerous_apis.txt:记录危险函数/方法名列表,供模型在代码搜索时参考。比如 Python 的eval、exec、pickle.loads,JavaScript 的eval、new Function,SQL 拼接常见的字符串与执行方法。context_rules.md:给模型看的上下文约束,告诉它哪些情况算误报,哪些情况需要进一步确认。output_rules.md:规定报告输出格式、严重级别定义和置信度说明。audit_report_template.md:最终报告模板,避免模型每次生成格式都不一样。
这套结构的好处是:脚本负责跑确定性扫描,模型负责上下文判断,规则文件负责统一口径。三层各管一摊,互不越权。
3.2 SKILL.md 的关键字段怎么设计
SKILL.md是整个技能包的入口,模型首先读取它。用了一段时间后,我觉得开头这几个元信息字段最重要:
--- name: security-audit-skill description: 对当前代码仓库执行安全审计,包括依赖风险、密钥与敏感信息、危险 API 调用、注入类风险,并输出带严重级别与修复建议的报告。 allowed-tools: grep, find, bash, file, glob, git, python only-if: 用户要求进行安全审计、代码漏洞检查、依赖风险评估、密钥泄漏排查 ---description千万别写得太泛。有些模型的 skill 触发机制是靠语义匹配,描述写得越具体,越容易在合适的时候被准确触发。如果你写成"帮助用户检查代码质量",模型可能在用户问"这段代码有没有 bug"的时候就错误加载审计技能,反而干扰正常对话。
allowed-tools很实用。它限制了 skill 在执行时能调用的工具范围,防止模型为了找某个字符串而做出特别离谱的操作。比如我这边只放开文件搜索、目录遍历、简单脚本执行这几个能力,不允许它随便调用网络请求工具。
only-if这个字段不是所有框架都支持,但如果框架支持条件加载,这个字段能大幅提高触发精度。我写了几个典型的用户意图,让模型能快速判断"现在是不是该用这个 skill"。
3.3 规则文件是给模型看的,不是给人看的
很多人在做 skill 时,容易把规则文件写成"团队公约",语言抽象,充满原则性表达。但你要记住,规则文件的读者是模型,不是同事。
我后来重写了context_rules.md,把里面所有抽象表述改成了"可判断""可操作"的描述。举几个实际例子:
- 不要写"注意误报",而要写"当 .env 文件处于 .gitignore 中时,该密钥不视为泄漏;当密钥仅出现在测试文件且使用明显测试值时,降级为信息级。"
- 不要写"检查危险的 SQL 写法",而要写"若参数直接拼入 SQL 字符串且不是通过参数化查询接口执行,标记为高危 SQL 注入风险;若参数经过白名单校验或类型转换后进入查询,标记为低危或正常。"
这两条改动带来的效果提升是肉眼可见的。模型对模糊指令的理解方差很大,但对带明确条件的指令,输出稳定性会明显提高。写规则时,尽量把你的判断经验"翻译"成条件分支,而不是风格描述。
4. 从审计流程到脚本实现:这套 skill 到底怎么工作
4.1 审计流程可以拆成哪三个环节
整个 skill 在运行时会被拆成三个环节:收集、扫描、汇总。这样一个简单分层,让模型在执行时的思路非常清晰,不会在某个环节无限深挖导致流程失控。
环节一,收集。由collect_manifest.py完成。它会扫描仓库根目录,识别常见的包管理文件,判断技术栈,统计源码文件类型和数量,并输出一个简明的"项目清单"。这个清单决定了后面模型把精力放在哪里。如果检测到只有 Python 代码,就不必花时间查node_modules的依赖。
环节二,扫描。这是脚本最密集的部分。密钥扫描脚本、依赖风险脚本、危险函数扫描脚本会依次执行。每个脚本只做一件事,输出原始结果给模型。
环节三,汇总。脚本不负责下结论,结论由模型基于脚本输出和上下文规则来下。模型把原始结果逐条带入context_rules.md里的判断条件,过滤误报,确定严重级别,最后按照audit_report_template.md的格式输出。
这个分层带来一个额外好处:每个环节都能独立测试。我可以在没有大模型的情况下直接跑脚本,确认识别逻辑没问题,再让模型去装配。省掉了很多调试时间。
4.2 一次完整扫描的标准操作步骤
以典型的 Node.js 仓库为例,整个 skill 的执行路径如下:
- 运行
collect_manifest.py,发现项目根目录有package.json、src/、config/,技术栈为 Node.js + Express。 - 读取
package.json中的依赖列表,运行run_dep_check.py,发现某个版本存在已知原型链污染漏洞,输出"依赖风险:高危"。 - 运行
run_secret_scan.py,在config/prod.env.example和src/auth.js中发现疑似密钥。模型再判断:prod.env.example中的值明显是占位符,降级为信息级;src/auth.js中的密钥看起来像真实 token,且该文件未被 gitignore,标记为高危密钥泄漏。 - 模型读取
dangerous_apis.txt,用 grep 在源码中定位eval、child_process.exec、字符串拼接 SQL 等调用点。 - 模型对每个调用点做上下文分析。比如发现
eval(req.query.code),并且输入直接来自用户参数,没有过滤逻辑,判定为高风险代码执行注入。 - 所有结果按严重级别汇总,生成带文件路径、行号、问题描述、修复建议的 Markdown 报告。
整个过程脚本运行不到一分钟,模型判断和报告生成大概两三分钟。对一个中小型仓库来说,这个耗时完全可以接受。
4.3 实测复盘:一份报告里的结果到底长什么样
拿一份我实际测试过的 Demo 项目为例,报告核心部分大致长这样:
| 风险类别 | 位置 | 严重度 | 置信度 | 问题简述 |
|---|---|---|---|---|
| 密钥泄漏 | src/auth.js:42 | 高危 | 高 | 疑似 OpenAI API Key 硬编码 |
| 依赖漏洞 | package.json | 高危 | 高 | axios 版本存在 SSRF 相关已知漏洞 |
| SQL 注入 | src/db.js:88 | 高危 | 中 | 用户输入直接拼接 SQL |
| 危险函数 | src/utils.js:12 | 中危 | 中 | 使用eval处理外部输入 |
| 配置风险 | config/index.js | 低危 | 高 | CORS 配置为*,允许所有来源 |
这份报告的价值在于,每一条都有关键文件位置和具体原因,修复时不需要再从头翻代码。特别是密钥泄漏和依赖漏洞这两条,在高置信度条件下可以直接交给开发者处理,省下了大量重复劳动。
4.4 置信度分级:防止模型自嗨
我最开始在报告模板里只放了"严重度",没放"置信度",结果模型经常自信地给出错误结论。后来我在output_rules.md里强制要求每条发现必须附带置信度:高、中、低三档。
置信度的判断规则也写得很细:
- 高:脚本输出明确匹配 + 上下文规则中没有任何降级条件 + 文件不是测试/示例文件。
- 中:模式匹配但上下文信息不完整,比如无法确定用户输入是否真正可控。
- 低:只是模式相似,大概率是误报,仅作为提示信息保留。
把置信度从人工经验变成模型的输出要求之后,报告的可用性有了质的提升。看到"高置信度高危漏洞",可以直接安排修复;看到"低置信度"的提示,不会浪费时间去深究。
5. 把这套 skill 装进主流代理框架:安装与调用实测
5.1 Claude Code 中的安装与权限细节
在 Claude Code 中使用时,我把 skill 放在项目根目录.claude/skills/security-audit-skill/下。Claude Code 会自动扫描SKILL.md,并在对话中按条件加载。
这里有一个权限相关的细节要注意:Claude Code 对工具调用有权限提示机制,模型运行脚本之前会征询用户确认。如果目录里脚本很多,频繁弹权限确认会影响体验。我的做法是在项目配置文件里明确授权该 skill 目录下的脚本执行权限,例如只允许python解释器运行scripts/下的脚本,其他路径的脚本一律不给权限。既减少了打断,也把风险控制在可接受范围内。
调用方式很简单,直接在对话里说"对当前仓库做一次安全审计"即可。模型会根据only-if条件判断是否启用 security-audit-skill。如果仓库里既有代码审计需求,也有普通的代码补全需求,它会优先处理触发条件更明确的任务。
5.2 Codex 和 OpenCode 的挂载方式
Codex 的 skill 目录既支持用户级也支持项目级。我习惯把通用型 skill 放在~/.codex/skills/,把跟具体仓库相关的规则放在项目.codex/skills/下。security-audit-skill属于通用型审计技能,我放在用户级目录,这样无论打开哪个项目都能用。
OpenCode 的配置路径是~/.config/opencode/skill/或项目级目录,加载逻辑类似。在这两个框架里,SKILL.md 的元信息已经足够触发加载,不需要额外注册中心。
有一点需要提醒:不同框架对allowed-tools字段的解析方式不一样。有些框架会严格按照字段限制工具调用,有些只把它当成参考。因此,在换框架运行时,一定要先在测试仓库里跑一遍,确认脚本执行权限符合预期,再拿到真实项目上使用。
5.3 上下文管理:不要把所有输出都塞给模型
我踩过最深的一个坑,是脚本输出太多导致上下文爆掉。比如密钥扫描脚本默认会输出所有疑似命中的行,一个稍大的仓库能跑出几百行结果。模型根本看不过来,后面的判断质量急剧下降。
解决思路是让脚本"预聚合"。run_secret_scan.py不会输出每一行命中,而是按文件名聚合,输出:
- 文件路径
- 命中数量
- 最可疑的前 3 个位置
- 具体匹配类型(AKIA 开头、sk- 开头、PEM 私钥块等)
模型只需要看聚合结果,就能判断哪些文件值得深入检查。这项改动让整个 skill 的可用性提升了一大截,上下文占用至少减少了 60%。
5.4 把审计输出接进 CI 或 PR 流程
除了交互式调用,我还把 skill 包装成了 CI 脚本。做法是在 CI 里用命令行启动代理框架,传入"执行 security-audit-skill"的指令,指定审查范围是本次git diff --name-only列出的文件。这样每次 PR 提交时,代理只审计变更文件,既快又有重点。
这种用法的好处是代码审查不只是看 diff 风格,还能自动带出安全风险提示。坏处是 CI 跑大模型有额外成本,所以我在 YAML 里做了限制,只在主干分支和包含依赖文件变更的 PR 上触发完整审计,其他分支只做密钥扫描加依赖检查。
6. 每次做完审计后,我把这几个坑认真记了下来
6.1 skill 编写阶段的三大误区
第一个误区是过于详细。SKILL.md 写得像操作手册全文,每个步骤都有十行解释,模型反而失去弹性,只知道按步骤执行,遇到异常情况不会灵活处理。后来我把 SKILL.md 压缩到逻辑框架级别,把"为什么"的内容挪到references里,模型既能看到流程,也能在遇到具体问题时查到背景知识。
第二个误区是脚本输出不设上限。前面提到上下文管理,本质上是编写阶段就犯下的错。现在我的所有脚本都强制带输出裁剪参数,默认只输出前 N 条结果。不是怕结果多,是怕结果多到模型处理不了。
第三个误区是忽视 git 上下文。没有明确审查范围时,模型容易把整个历史代码都扫一遍,浪费时间,结果还分散。现在 SKILL.md 里明确写着:优先询问用户要审计全部代码、最近改动,还是某个目录。只有用户没给范围时,才默认全量扫描。
6.2 运行时高频问题:误报、权限、路径假设
我把运行阶段遇到的问题整理成了清单:
| 问题 | 原因 | 解决方式 |
|---|---|---|
| 模型把测试密钥报成高危 | 没有判断测试目录 | 在 context_rules 中明确测试/示例文件降级规则 |
| 脚本找不到项目根目录 | 不同仓库的目录层级不同 | 先运行 collect_manifest.py 确认根目录 |
| 密钥扫描对短随机串误报高 | 熵检测阈值过低 | 提高阈值,并要求至少匹配已知前缀特征 |
| 依赖检查结果过期 | 本地漏洞库更新不及时 | 增加更新时间提示,建议定期拉取最新数据 |
| 模型在某个脚本上反复运行 | 缺少执行次数上限 | 在 SKILL.md 中规定每个脚本最多运行 N 次 |
权限问题是最容易被忽略的。以我的经验,在非本地环境下部署时,脚本解释器的路径可能不同,python3和python的差异就能让 skill 直接跑不起来。我在脚本入口统一做了解释器探测,同时保留错误提示,避免模型卡在运行脚本那一步。
6.3 后续扩展:与更细粒度规则引擎联动
security-audit-skill 目前的定位是"静态审计常用项",但它预留了很清晰的扩展口。接下来我打算把语义化的风险规则也纳入进来,也就是在rules/下增加按语言分类的规则文件,比如rules/python_security_rules.md、rules/javascript_security_rules.md。这些规则会描述特定语言的高危模式,供模型在扫描阶段参考。
还有一个方向是接入真实的实时漏洞库。因为离线比对只能覆盖内置数据库里的已知漏洞,对于新发布的 CVE 无能为力。如果脚本在检测依赖时能主动获取最新漏洞信息,审计结果的时效性会大幅提升。不过这个功能涉及网络调用,放在代理框架里需要单独设计权限策略,不能无条件放行。
根据我个人的使用体验,skill 类的项目最忌讳一次做太大,最好的迭代方式是先跑通最小闭环,再做增量扩展。security-audit-skill 从最初一份检查清单到现在能稳定输出审计报告,中间改了很多版,但每一步都只解决一个具体问题。如果你也在做类似的 agent 技能,建议你先把"收集—扫描—汇总"这个闭环跑通,再逐步往里加规则和参照库。安全审计这件事,流程稳定比功能多更重要。