
我一直有这样一个习惯不管是用 Claude Code、Codex 还是 OpenCode 写项目生成完代码之后一定会再开一轮 review。但 review 来 review 去发现一个问题——让 AI 自动写代码很爽但让它主动发现安全问题真的很难。它能把功能写得花里胡哨却常常在 SQL 拼接、日志打敏感字段、依赖版本陈旧这些地方毫无知觉地踩坑。所以我自己动手做了一个 security-audit-skill一个专门给 coding agent 用的技能包让 agent 在写代码前、改代码后或者准备提交时自动按一套安全审计清单做检查然后输出带风险等级和修复建议的报告。这篇文章是完整的落地复盘包括目录结构、SKILL.md 编写、如何接入 Claude Code / Codex / OpenCode以及我调优和排错的全部经验。如果你正在折腾 agent skill或者想知道skill 和普通提示词到底区别在哪skill 怎么写才能真的被模型执行建议认真看完。1. 为什么我要单独做一个 security-audit-skill而不是靠口头提醒1.1 coding agent 的安全失明现象先讲个真实现象。我用 coding agent 写一个内部工具的后端接口输入需求非常明确根据用户名查询用户信息并返回。结果它就真的给我写了一段类似这样的代码app.route(/user) def get_user(): name request.args.get(name, ) conn sqlite3.connect(app.db) cur conn.execute(fSELECT * FROM users WHERE username {name}) ...功能完全正确查询也没写错但这就是教科书级别的 SQL 注入。我把这段代码丢给同类模型问它有没有安全问题它能头头是道地分析出来。问题在于当它作为 agent 在完成编码任务时安全约束根本没有进入它的执行优先级。它的注意力全在怎么让代码跑通怎么满足需求描述上安全只是附带项。这种安全失明不是模型智商问题而是任务定义问题。用户说实现一个查询接口agent 就把这个当唯一目标。如果你不在任务里显式注入安全审计的要求它默认不会主动检查。所以在 agent 工作流里安全能力必须被制度化——不能依赖某次对话突然想起来而是要有一个每次都会自动触发的审计机制。这正是我建 security-audit-skill 的根本动因。1.2 skill 和普通提示词、agent 到底有什么区别热词里有人问skill 和 agent 的区别我在刚开始接触的时候也困惑过。这里用表格把三者捋清楚维度普通提示词Skill 技能包Agent存在形式一句话或一段文字一个文件夹 SKILL.md 规则/脚本一个完整应用或容器化工作流可复用性低每次都要粘贴高一次安装到处使用高但通常绑定具体环境维护成本无结构改起来乱规则文件独立改一处即可升级和依赖管理较重是否带逻辑纯文本无程序逻辑可带参考脚本、规则文件有自己的循环、工具调用、决策逻辑典型场景临时问一句帮我 review 这段代码反复执行的安全审计单元测试生成端到端自动运维、自动测试执行一句话理解提示词是口头的嘱咐skill 是放在工位上的操作手册agent 是代替你干活的实习生。Skill 比提示词更结构化比 agent 更轻量。它不接管整个流程只在合适的时候注入专业知识然后让主模型完成理解和输出。我用 security-audit-skill 就是这么个思路它不是一个独立运行的 agent而是让 Claude Code 这类 agent 在需要的时候能翻开的一本安全审计手册。1.3 这个 skill 的能力边界先把丑话说前面任何工具如果不管边界最后都会被滥用然后被骂。我把 security-audit-skill 的定位定得非常清楚它能做静态代码审计注入类、认证类、敏感信息类问题、依赖版本风险提示、常见的云配置和框架配置检查、根据问题给出修复建议。它不做不替代 Semgrep、Snyk、CodeQL 这类专业扫描引擎不做运行时攻击模拟不能保证审过就零漏洞。它的核心价值把安全审查从人工偶尔想起来变成agent 每次默认执行的固定环节。专业扫描器是精确制导武器我这个 skill 更像日常巡逻。它能挡掉 80% 的常规漏洞剩下 20% 靠工具链和人工兜底。别指望一个 SKILL.md 文件就能让代码绝对安全但作为第一道防线性价比极高。2. security-audit-skill 的目录结构与执行流程设计2.1 目录结构别把所有内容塞进一个文件很多第一次写 skill 的人习惯性把规则全写进 SKILL.md结果文件长到连模型都懒得读。我的做法是分层组织security-audit-skill/ ├── SKILL.md ├── scripts/ │ ├── check_dependencies.py │ └── scan_sensitive_patterns.py ├── references/ │ ├── owasp-top10-checklist.md │ ├── languages/ │ │ ├── python.md │ │ ├── javascript.md │ │ └── golang.md │ └── report_template.md └── assets/ └── 一些示例报告SKILL.md 是入口和总指挥它只负责告诉模型你接下来要按什么流程走、参考哪些文件。references 目录放具体规则按语言和主题拆分。scripts 目录放那些需要精确字符串匹配时的辅助脚本比如用正则扫硬编码密钥。assets 放示例报告和模板方便模型按照既定格式输出。为什么这么拆分核心原因有两个。第一是 token 控制。模型一次能读的上下文是有限的如果 SKILL.md 本身就有上万字真正进入审计执行的上下文就所剩无几了。第二是可维护性。你不可能让一份 Python 规则同时管好 JavaScript、Go、SQL 的所有场景按语言拆分后某个语言规则更新不会影响其他部分。2.2 执行流程把审计变成一条清晰的流水线我在 SKILL.md 里定义的执行流程是这样的整个审计过程分成五步收集上下文扫描目标目录识别项目语言、框架、依赖清单明确审计范围。加载规则根据识别到的语言和框架去 references/languages 下加载对应规则文件。逐项检查按规则文件中的检查点逐个检查目标代码记录问题位置和风险描述。交叉验证对于疑似问题让模型自己再读一遍上下文排除因为代码片段截断造成的误判。输出报告按 assets 里的模板生成报告包含风险等级、文件位置、问题描述、修复建议。这个流程的关键在设计上就坚持了两点先识别再审计先报告再改码。先识别再审计是避免模型拿到一堆代码就开始猜。如果不告诉它项目用的什么框架它可能用 Express 的规则去审 Flask 项目结果全是无效预警。先报告再改码是因为我不希望 agent 在审计的同时擅自修改代码——自动修复的前提是建议已经被人确认过。审计和修复的职责分离在工程上能减少很多不可控的连带问题。2.3 为什么规则要外置而不是写死在提示词里可能有朋友会说规则外置多麻烦我直接把检查清单写进 prompt 不也一样吗我一开始也是这么干的把 20 条审计规则写进一个长 prompt结果效果很差。原因有三点模型对提示词里的内容不是平等对待的。写在越靠后、越琐碎的内容注意力权重越低。一堆规则混在任务描述里模型很容易只记得前几条。规则文件外置后模型在需要时会主动查阅这种查阅行为会让规则以更独立的身份进入推理过程而不是被淹没在上下文中。外置规则可以被版本管理和动态替换。项目改成 Go 了我只需把 references/languages/golang.md 放进去SKILL.md 一行不用改。把规则外置本质上是让专业内容保持独立身份。这是我这次实践里最值得推荐的一个设计取舍。3. SKILL.md 与审计检查点的编写细节3.1 SKILL.md 的 frontmatter 和核心指令SKILL.md 是 skill 的入口大部分工具都会读取文件头部的 YAML frontmatter。不同工具对字段要求不完全一样但 name 和 description 是通用的。我的写法如下--- name: security-audit description: 对项目代码执行安全审计检查注入、身份认证、敏感信息泄露、依赖漏洞等问题输出包含风险等级和修复建议的报告。当用户请求安全审计、code review、安全检查或代码准备提交时使用。 ---description 这一段特别重要它决定了模型在什么场景下会主动想起这个 skill。我的经验是 description 里要写清楚两件事这个 skill 做什么审计些什么以及什么时机触发用户说审计、code review、安全检查或提交前。早期我把 description 写得太文艺说什么守护代码安全结果模型根本不知道怎么触发几乎等于白写。3.2 核心指令怎么组织让模型知道自己在扮演谁SKILL.md 的正文部分我用了角色设定 流程 输出要求三段式# Security Audit Skill 你是一名资深的应用安全审计员。你的任务是对目标代码进行安全审计而不是直接编写功能代码。 执行流程 1. 扫描目标目录识别项目语言和框架。 2. 找到 references/ 下对应的语言规则文件并仔细阅读。 3. 按规则文件中的检查点逐项检查目标代码。 4. 对疑似问题做二次确认排除上下文截断造成的误判。 5. 使用 assets/report_template.md 格式输出报告。 输出要求 - 每个问题必须包含风险等级严重/高/中/低、文件位置、问题描述、修复建议。 - 修复建议必须给出可落地的代码示意不能只说应该使用参数化查询。 - 如果没有发现问题必须主动说明已检查的范围和使用的规则避免空口无凭。关键点在于角色设定不是让模型装样子而是改变它的任务目标。你说你是一名安全审计员它就会把寻找问题当作优先目标你说请帮我检查一下代码安全它很可能仍然停留在辅助编码的角色里下意识地帮代码说好话。角色设定本质上是重新定义任务优先级这是写 skill 最容易忽略也最有效的一点。3.3 把 OWASP Top 10 变成可执行的检查点规则不能停留在注意 SQL 注入这种层面必须具体到看到什么形式的代码要报警。我把 OWASP Top 10 拆成了十几个检查点每个检查点都对应具体的代码特征。下面是我实际在用的部分检查清单检查项危险信号风险等级常见修复SQL 注入字符串拼接 SQL、f-string 传入 execute()严重参数化查询 / ORM 预编译硬编码密钥password、api_key、token 后紧跟明文常量严重环境变量 / 密钥管理服务敏感信息泄露日志打印 request、密码、token、身份证号高脱敏后输出认证失效自定义加密算法、弱口令校验、Session 固定高使用成熟认证库越权访问接口未校验资源归属直接按 ID 查询高增加属主校验XSS前端代码将用户输入直接通过 innerHTML 注入高转义 / 使用 textContent不安全的反序列化pickle.loads、JSON.parse 直接处理不可信输入严重改用安全格式并校验依赖漏洞依赖版本低于已知修复版本中/高升级并重新测试这个表格我原样放进了 references/owasp-top10-checklist.md。注意这里每一项都给出了危险信号和常见修复而不只是问题名称。模型在审计时需要的不是概念而是判断依据。3.4 语言规则文件的实际写法示例以 Python 为例references/languages/python.md 里我会这样写## Python 审计检查点 ### 危险函数与模式 - os.system、subprocess 调用中拼接用户输入 → 命令注入 - eval、exec 直接执行不可信字符串 → 代码注入 - SQL 字符串拼接传给 cursor.execute() → 拼接注入 - pickle.loads 处理来自外部的数据 → 不安全的反序列化 ### 框架相关 - Flaskrequest.args / request.form 的值直接进入 SQL、HTML、shell - Djangomark_safe 使用不当绕过自动转义 - 日志配置logging.info(str(request.json)) 打印请求体 ### 豁免条件 - eval 调用对象是硬编码常量且无外部输入时可降级为提示 - 测试代码中的 subprocess 调用如果只执行固定命令可忽略看到没规则文件要做到两件事第一给出具体的函数名和模式让模型能够精确匹配第二给出豁免条件用来减少误报。如果没有豁免条件任何一个含 eval 的项目都会被标记出一堆严重漏洞最后模型和人都对这个 skill 失去信任这才是审计工具最可怕的失败方式。4. 接入 Claude Code / Codex / OpenCode 的实操步骤4.1 不同工具的 skill 目录放哪里不同工具的 skill 目录不统一变化也很快这里以我实际用过的路径为例供参考工具目录示例备注Claude Code~/.claude/skills/security-audit-skill/个人级全局可用Codex~/.codex/skills/security-audit-skill/也可放项目.codex/skills/内OpenCode~/.config/opencode/skills/security-audit-skill/有的版本在~/.opencode/skills/因为这些目录结构迭代得很快我每次升级工具版本后都会先看一眼--help或官方文档。最简单的判断方式它在启动时默认加载哪些 skill 目录doc里一般写得很清楚。4.2 触发方式与验证怎么确认 skill 真的生效了接入 skill 之后最大的困惑就是它到底有没有生效我分享一个土办法。先在 SKILL.md 里加一句特殊标记比如遇到触发场景时必须先输出一行进入安全审计模式。然后你输入用 security-audit 审计当前项目中的 auth 模块如果模型在报告开头输出了标记说明 skill 被正确加载了。如果没有先检查目录名和 frontmatter 里的 name 是否完全一致——很多工具是通过目录名匹配 skill 的拼写不对等于没装。4.3 跨工具兼容的几个坑同时对接多个工具时我踩过几个具体的坑frontmatter 字段格式不完全一致。有的工具只认name和description有的还支持allowed-tools之类字段未知字段可能导致解析失败。路径引用要写相对路径。我在 SKILL.md 里写references/languages/python.md不要写绝对路径这样整套 skill 目录拷到任何机器上都能用。脚本失败不能影响审计主流程。scripts 里的 Python 脚本是为了辅助精确匹配但如果脚本执行失败skill 要能降级成仅靠规则文件做语义审计否则整个流程会卡在脚本报错上。4.4 从手动触发到自动触发skill 的最高级用法是自动触发也就是模型自觉在提交前调用。自动触发靠的是 description 写得足够准。我把 description 里加了几类触发关键词安全审计、code review、安全检查、提交前检查、依赖检查。实测下来只要项目里我提到准备提交或review 一下Claude Code 就有大概率主动加载 security-audit。这个成功率不是 100%所以重要提交我会手动输入一次触发命令求个稳。5. 一次真实审计的完整复盘从 SQL 注入到误报排除5.1 测试项目是什么样我用一个自建的 Flask SQLite 博客项目做测试。功能不复杂文章搜索、用户登录、操作日志。表面上看代码干净但我故意埋了几个典型问题想看看 skill 能不能找全。其中一个接口长这样app.route(/search) def search(): keyword request.args.get(q, ) conn sqlite3.connect(blog.db) cursor conn.execute( fSELECT * FROM posts WHERE title LIKE %{keyword}% ) rows cursor.fetchall() return render_template(results.html, rowsrows)代码短、逻辑清楚但问题一眼就能看出来keyword 是用户输入直接拼进 SQL 了。5.2 security-audit-skill 的输出报告把项目丢给接入了 skill 的 Claude Code它生成的报告节选如下## 审计发现 ### 严重SQL 注入 - 位置app.py:12 - 描述keyword 来自 request.args.get(q)直接通过 f-string 拼入 SQL 查询。 攻击者可构造 q% OR 11 绕过过滤条件。 - 修复建议使用参数化查询 python cursor.execute( SELECT * FROM posts WHERE title LIKE ?, (% keyword %,) )高敏感信息泄露位置logger_config.py:20描述登录接口打印了完整请求体包含明文密码。修复建议日志中过滤 password/token 字段只记录用户 ID 和登录结果。报告把问题位置、风险等级、修复建议全部列出来了而且没有只停在有注入风险给出了直接可改的代码。这点很重要我不能每次只得到一句存在安全问题就完事了那对实际修复没有帮助。 ### 5.3 为什么这类问题人审容易漏 你会说SQL 注入这么明显人也能看到。但真实项目里代码量一上来谁 review 的时候能保证每个 SQL 语句都看一遍人审的注意力是有限的越是高频、重复的 CRUD 代码越容易在疲劳中略过。而 skill 不会累它会每次都按规则逐行扫描。同时coding agent 它的价值是快速定位疑似点人能集中精力做判断和修复效率和准确率都会上一个台阶。 ### 5.4 误报排除报告也需要二次复核 第一次跑测试skill 还报了一个疑似硬编码密钥的问题指向配置文件里的 SECRET_KEY flask-blog-demo。这确实是硬编码但因为这是个本地演示项目密钥本身不是生产凭证我把它标为中危提示而不是严重。这个例子想说明的是审计报告是辅助判断不是最终裁决。别盲信 skill 的输出尤其当它把什么都标为严重时要检查规则是否写得过于激进。 ## 6. 排错实录skill 被加载了但规则没触发问题出在哪 ### 6.1 现象 有段时间我接入 Codex 后发现个奇怪问题skill 确实被加载了模型也承认正在使用 security-audit但最终报告只有一句未发现明显安全问题。我当时整个人都不好了——我的测试项目里明明放着那个 SQL 注入它怎么会没发现。 ### 6.2 完整排查链路 我把排查过程逐步记录下来这比直接给结论更有参考价值 1. 先确认 skill 是否真的加载我在 SKILL.md 里加了标记输出模型没有输出标记说明加载环节就出了问题。 2. 检查目录名与 name 字段是否一致。结果发现 Codex 的 skill 目录名用了下划线 security_audit而 frontmatter 里 name 是 security-audit部分工具能容忍这种不一致部分工具不行后者直接不加载。 3. 统一目录名后再测试标记输出了但报告还是说没发现问题。 4. 进一步检查 references 路径引用。发现 SKILL.md 里写的是 references/languanges/python.md拼错了一个字母模型加载规则文件失败就哑巴审计了。 5. 路径修正后问题解决注入点被成功标记。 这次排错最大的教训是**skill 链路里每一环失败都可能造成静默降级**。路径写错不会报错只会让模型在缺失规则的情况下硬着头皮审计然后告诉你没事。配置 skill 之后一定要在上线前用已知有问题的代码验证一遍不要等真出事了才发现规则根本没生效。 ### 6.3 误报和漏报的调参经验 规则太严全是误报规则太松全是漏报。我控制误报率的方法是给检查项加上下文条件 - 不要看到 eval 就报严重要看 eval 的参数是否来自外部输入。 - 不要看到密码字段就报泄露要看它是否被写入日志或返回给前端。 - 不要看到版本低就报高危有的内部项目不暴露公网风险等级可以降到中危。 同时在 SKILL.md 里写一个豁免机制项目中可以通过 # nosec 这类注释标记来声明此处风险已知且可接受模型看到标记后可跳过。这个机制借鉴了 Go 生态里安全工具的惯例实际用起来对减少噪声非常有效。 ### 6.4 一个我至今仍在意的边界问题 skill 的审计质量高度依赖模型的上下文长度和注意力分配。项目一大文件一多模型很可能只审了前几个文件就开始输出报告。我现在的办法是在 SKILL.md 里强行要求输出报告时附带已审计文件列表如果列表明显不全人一眼就能看出来。这不是完美的解法但足够实用。更大的项目我建议直接把目标锁定在增量 diff 上而不是全量目录让 skill 专注审计本次变更的代码比审整个仓库靠谱得多。 ## 7. 从个人 skill 到团队规范的进阶方向 ### 7.1 和 Semgrep / Snyk 配合而不是二选一 Skill 的语义理解能力很强但精确度不如专业扫描器Semgrep 规则精确但配置成本高Snyk 对依赖漏洞的覆盖好但对业务逻辑漏洞无能为力。我的最终方案是组合使用先用 skill 让 agent 在编码环节做第一层兜底再在 CI 里跑 Semgrep 自定义规则和 Snyk 依赖扫描两类结果合并成一份审计报告。这样既能享受 agent 的灵活性又能拿到精确扫描器的确定性。 ### 7.2 报告模板化并接入协作平台 重复生成自然语言报告格式不统一团队里没人想看。我把 skill 输出的报告模板改成了结构化格式每个问题带 risk、file、line、suggestion 字段再配合一段脚本把 skill 产出的审计结果转成 Markdown 评论自动发到代码评审平台。这样开发者打开 MR 页面就能看到机器人留下的审计意见不用切换到终端看报告。 ### 7.3 版本管理与规则库持续更新 SEcurity-audit-skill 本身我放在 git 仓库里管理每个规则文件的变更都有记录。依赖漏洞的规则库会跟着 OWASP 和已知漏洞公告更新我大概每两周拉动一次。这个频率不需要太快但定期更新是必须的——安全规则是活的知识几个月不管就过时了。 ### 7.4 下一步让 skill 从发现问题走向输出修复补丁 我目前在测试的一个方向是让 skill 在审计后自动生成修复补丁再由人 review 合并。流程是先审计输出问题清单然后针对每个已确认的问题让 agent 生成 diff人不审批不合并。这个方向把 AI 的价值往前推了一步但也对 skill 的精确度提出了更高要求。如果误报率压不下来自动修复补丁就是灾难。所以我会先在内部项目的非核心模块上验证等稳定了再推广。 最后再分享一个我自己实践中的体会skill 这个东西难的不是写规则而是让模型在真正的场景里愿意按规则执行。一个结构清晰、路径正确、规则具体、带豁免机制的 skill才是一个能被模型真正信任和使用的 skill。security-audit-skill 让我在 agent 工作流里重新找回了对代码安全的掌控感。也希望这套从设计到落地的完整思路能帮你写出属于自己的第一个高质量 skill。