打开任何一个AI编程工具的会话界面,你有没有过这种感觉:代码生成速度飞快,但安全审计反而成了最容易被跳过的环节。最近在Claude Code、Codex、opencode这类工具里,给Agent挂一份专属的skill是很多团队在折腾的事情。我基于这个思路,从零做了一个security-audit-skill,把代码安全审计的方法论固化成一个可复用的技能包,让Agent在合代码、做重构、提交MR之前能顺手做一轮安全审查,而不是等问题上线了再去找人补窟窿。这篇文把我从设计到落地过程中踩过的坑、反复试出来的方案一起整理出来,适合正在用AI编程助手、又需要给代码安全把关的工程师参考。
1. 为什么我最终把安全审计做成了Skill,而不是一段提示词
很多人的第一反应是:安全审计不就是给模型一段提示词,让它"找一下代码里的安全漏洞"吗?我最初也这么干,后来发现提示词方案在真实项目里根本扛不住。原因可以从Skill的本质聊起。
1.1 Skill是什么:一份给Agent的"岗位说明书"
Skill本质上是一份结构化的技能说明书,通常由一个包含SKILL.md主文件和若干辅助资源的目录组成。SKILL.md里写清楚这个技能适用于什么场景、要按什么步骤执行、需要输出什么格式,辅助资源里则是检查清单、规则表、脚本等更详细的材料。
和把指令硬塞进系统提示词相比,Skill的核心机制是按需加载。Agent在实际执行任务时,会根据用户请求决定要不要读取这份说明书,说明书里提到的references目录和scripts目录,也是按需展开的。这个设计的好处是:你可以在项目里放几十个Skill,而Agent的上下文不会被长期占用,只有真正需要用到某个技能时才去翻对应手册。
我自己实测下来的感觉是,把安全审计写成Skill,本质上是把一份容易被大段提示词淹没的"方法论"变成了一份独立、可复用、可版本管理的"岗位说明书"。换一个项目、换一台机器、换一个Agent环境,直接把这个目录拷过去就能用。
1.2 Skill、Agent、插件:边界到底在哪里
热词里一大堆人在搜"skill和agent的区别""skill插件"之类的问题。我用自己的说法给你捋一下:
- Agent是执行任务的"员工",负责拆解目标、调用工具、决策下一步动作。
- Skill是给Agent看的"工作手册",定义某个专业领域怎么做。
- **插件/工具(tool)**是员工手里的"工具",负责执行具体动作,比如执行Shell命令、读文件、调API。
一个常见的误区是把Skill理解成插件。插件告诉Agent"你能做什么"(比如能执行命令、能抓网页),Skill告诉Agent"这件事该怎么做"(比如安全审计要先收集范围、再识别入口、最后出报告)。两者可以配合使用——Skill的流程里可以指定Agent去调用某些插件工具,但Skill本身不直接暴露给Agent作为可调用的函数接口。
安全审计这个场景特别适合Skill化,因为审计方法论相对稳定,有CWE分类、有OWASP Top 10、有STRIDE模型打底,规则是可以沉淀下来的。同时审计又是一个需要多轮"搜证"的过程,不是一句"帮我找漏洞"就能完成的,它需要Agent反复看文件、追调用链、对比依赖版本,最后输出结构化报告。这部分流程编排,正好是Skill的强项。
1.3 做一个安全审计Skill比直接扫描器好在哪
你可能还会问:市面上有那么多SAST工具(静态应用安全测试工具),为什么还要让AI Agent做这事?我最直观的感受是,传统扫描器擅长找"已经命名的漏洞模式",但对于业务逻辑漏洞、越权访问、敏感数据过度暴露这类需要理解业务上下文的问题,传统扫描器几乎无能为力。而Agent配合Skill,可以在理解整个仓库结构的前提下做推理,把代码路径、数据流、业务语义串起来,这就是"扫描器"和"初级审计员"之间的区别。
当然,Skill方案也不是替代扫描器,后面我会讲到它更适合做扫描器的"前置分诊"和"补充判断"。
2. 开工之前:先定好这个Skill的审计边界
动手写文件之前,最重要的一件事是明确:这个Skill到底该干什么、不该干什么。我见过很多翻车的Skill,基本都是因为边界没定清楚,导致Agent在执行时要么像无头苍蝇,要么越权做了不该做的事。
2.1 目标定位:让Skill当"初级评审专家",不是"自动修复机器人"
我设计security-audit-skill时的定位是:发现风险、给出证据、建议修复,但绝不直接改代码。
为什么不让它自动改?因为安全审计的KPI是"发现问题多、误报少、证据扎实",一旦让模型在审计过程中顺手改代码,它很容易为了"完成任务"而做出不安全的"修复",比如把验证逻辑删掉、把加密算法换成更弱的、或者改了接口签名导致业务崩溃。审计和修复是两个认知负荷完全不同的任务,混在一起两件事都做不好。
所以在SKILL.md的description里我就明确写了:
适用于对代码仓库、变更集、配置进行安全审计; 输出结构化漏洞报告与修复建议; 不负责自动修改代码;不负责部署后渗透测试。这段描述不只是写给用户看的,更是写给Agent看的。Agent会靠这段描述来判断"现在要不要加载这个Skill、加载后能做什么、边界在哪"。
2.2 目录结构:一次设计到位,后面扩展省心
我在设计目录时参考了一个原则:主文件做流程编排,细节内容全部下沉到references和scripts里。这样好处很明显——SKILL.md本身不会太长,Agent读取时不会占据大量上下文,侦察到具体场景时再按需拉取对应细节。
我最终用的目录结构是这样的:
security-audit-skill/ ├── SKILL.md ├── scripts/ │ ├── collect_evidence.py # 收集高危文件的静态信息 │ ├── lockfile_scan.py # 扫描依赖锁文件 │ └── gen_report.py # 生成审计报告模板 ├── references/ │ ├── cwe_checklist.md # CWE Top 25检查清单 │ ├── dependency_risks.md # 依赖审计细则 │ ├── config_hardening.md # 配置加固清单 │ └── untrusted_input.md # 污染源定义与追踪方法 └── assets/ └── report_template.md # 审计报告模板不同Agent工具对Skill目录的放置路径略有差异,比如Claude Code习惯放在.claude/skills/下,Codex可以放在项目级目录或用户级目录,opencode也有自己的约定。对于安全审计这种团队级能力,我建议放在仓库根目录的.claude/skills/、.codex/或其他Agent约定的目录下,这样所有用这个仓库的人都能自动继承这个技能。
2.3 Frontmatter:决定Skill什么时候被触发
SKILL.md开头的Frontmatter是整个Skill的"门面",直接影响Agent会不会正确调用它。我踩过最典型的坑就是description写得过于笼统,导致Agent根本不触发,或者乱触发。
推荐写法是这样:
--- name: security-audit description: 当用户要求进行安全审计、漏洞扫描、安全性评审、检查代码安全问题、分析变更集风险、审查依赖安全时使用。适用于代码仓库、独立文件、配置、依赖锁文件的静态安全审查。不适用于渗透测试、运行时动态扫描。 version: 1.0.0 allowed-tools: read_only_shell, grep, ripgrep, git_diff ---几个关键点:
- description里要写明触发词和应用范围。Agent会用这个description和用户请求做匹配,写清楚"什么时候该用"比写"它能做什么"更重要。
- version字段建议保留,Skill文件迭代时,Agent可以根据版本号判断是否读取过旧版缓存。
- allowed-tools字段不是所有Agent都支持,但它是一个很好的安全声明。安全审计这个Skill只需要只读工具,明确限制可以极大降低误操作风险。
- 目录名最好用小写加连字符风格,避免和Frontmatter里的name大小写不一致导致部分Agent加载失败(这个坑我后面细说)。
3. 手写SKILL.md:从侦察到出报告的全流程编排
SKILL.md是Skill的灵魂。它决定Agent按什么思路干活。我第一版写得很随意,结果Agent经常跳过侦察直接报漏洞,或者把日志级别的"风险提示"当成"高危漏洞"。后来我把整条流程重写成了固定五步,每一步做什么、输出什么,全部显式写清楚。
3.1 审计流程的五步编排:先侦察、再深挖、后下结论
在SKILL.md的主体部分,我要求Agent必须按照以下顺序执行,不允许跳步:
- 范围收集:确定审计目标。如果是整仓,分析仓库语言、目录结构、构建方式;如果是变更集,先用
git diff确定变更文件列表。这一步的输出是一个清单,列出要审计的文件和优先级。 - 入口识别:定位所有与外部交互的代码入口,包括HTTP接口、消息队列消费端、命令行参数处理、文件导入功能、反序列化点。这是审计的关键起点,也是我见过Agent最常跳过的一步。
- 逐项检查:按references里的检查清单,对入口代码做数据流追踪。重点追踪"不可信输入"从入口到敏感函数(SQL、Shell、文件路径、反序列化、加密)的完整路径。
- 交叉验证:对有嫌疑的点做二次验证。判断是否存在前置校验、是否使用了安全API、依赖版本是否已被修复、是否存在可利用场景。这一个步骤是降低误报的命门。
- 输出报告:严格按照模板输出,包含机器可读的JSON汇总和人工可读的Markdown详情。
我在SKILL.md里用"必须""禁止"这样的强指令来约束Agent,比如"在完成步骤2之前,禁止输出任何漏洞结论"。这样写虽然听起来挺粗暴,但对提升审计质量非常有效。
3.2 检查清单:从六个维度去找问题
我的references/cwe_checklist.md里维护了一张清单,Agent会按这个维度逐项排查。这张表我直接贴出来供你参考:
| 检查维度 | 具体风险点 | 对应CWE参考 |
|---|---|---|
| 注入类 | SQL注入、命令注入、路径穿越、模板注入 | CWE-89 / 78 / 22 / 1336 |
| 认证与授权 | 硬编码凭证、弱口令逻辑、越权访问、JWT缺陷 | CWE-798 / 287 / 285 |
| 敏感信息泄露 | 日志打印密钥、前端硬编码密钥、明文传输 | CWE-532 / 312 / 200 |
| 加密缺陷 | 使用了弱哈希、不安全随机数、自定义加密算法 | CWE-327 / 338 |
| 依赖风险 | 高危版本依赖、锁定文件缺失、包来源不可信 | CWE-1104 |
| 配置安全 | Debug模式开放、CORS配置过宽、权限配置过大 | CWE-284 / 942 |
每个维度下还有更细的指引,比如在"注入类"里,我会要求Agent特别关注字符串拼接SQL、eval()、os.system()、file_get_contents()这类敏感函数,同时要求它必须以数据流为线索,不能看到敏感函数就直接报漏洞。
3.3 证据链要求:漏洞报告里必须写清"从哪里来到哪里去"
这是我和这个Skill反复磨合后最满意的设定之一。我要求Agent在每一条漏洞发现里,都必须给出完整证据链,缺一不可:
- 漏洞位置:文件路径和行号,格式为
src/xxx.py:42。 - 污染源:不可信输入的来源,比如HTTP参数、用户上传文件、HTTP请求头。
- 传播路径:从污染源到触发点经过的关键步骤和中间变量。
- 触发场景:一段尽量短的、可以人工复现的恶意输入描述。
- 修复建议:至少给两种可选方案,并说明各自的适用场景和副作用。
这个设定看起来只是输出格式要求,实际效果是逼着Agent把"猜测"变成"推演"。因为如果没有证据链要求,模型非常容易基于模糊匹配就给出一堆"存在SQL注入风险"的泛泛结论,有了传播路径要求之后,它必须真的去追踪变量之间的数据流,准确率会明显上了一个台阶。
3.4 输出报告模板:人工可读为主,机器可读兜底
我在assets/report_template.md里定义了标准的报告结构,核心骨架如下:
## 审计范围 - 审计目标: - 仓库/文件规模: - 审计语言与框架: ## 漏洞汇总表 | 编号 | 严重级 | 漏洞名称 | 位置 | CWE | ## 漏洞明细 ### [P0] 漏洞名称 - 位置: - 类型(CWE): - 污染源: - 传播路径: - 触发场景: - 修复建议: ## 审计结论 - 总体风险评级: - 优先处理项:同时在报告末尾,我会要求Agent输出一段JSON格式的机器可读摘要,方便后续接CI脚本做自动判断。这个JSON可以直接放进Markdown的代码块里,不会影响人工阅读:
{ "critical": 0, "high": 1, "medium": 3, "low": 5, "total": 9, "suggested_action": "block" }4. 让Agent真正执行审计的三个关键设置
写完了SKILL.md,还有一个很现实的问题摆在那里:Agent的上下文窗口是有限的,一个大型仓库可能有几千个文件,怎么让它"审得动"?实测下来,必须从机制上做一些设计。
4.1 上下文窗口有限?采用"分批审计、最后合并"策略
我在SKILL.md里明确写了一条规则:单次审计会话默认不超过30个目标文件。如果审计范围超过30个文件,就按以下批次执行:
- 第一次迭代:先审计入口类文件和敏感API调用类文件。
- 第二次迭代:审计数据模型、配置、数据库访问层。
- 第三次迭代:审计工具类、测试代码、脚本。
- 每次迭代结束后,生成临时审计小结,存入工作记忆。
- 所有批次完成后,合并生成完整报告。
为什么是30个文件?这个数值是我实测得出的折中方案。一次给模型灌太多文件,它后期会明显出现"漏审",尤其是中间段的文件几乎被遗忘;一次太少,迭代次数太多,容易在批次衔接时丢失上下文。30个文件(大约对应几千行代码)能让模型在仔细阅读和总览全局之间找到平衡。
另外我强烈建议在SKILL.md里明确"审计优先级规则"——优先处理处理外部输入的文件、初始化权限的文件、进行加解密的文件,把这些文件放在第一批次,这样即使后面批次来不及完成,最关键的风险也已经被覆盖了。
4.2 让审计结论"既可读又可算":JSON摘要不能省
前文提到报告末尾要求输出JSON摘要,这个设计不是可有可无的装饰。实际使用中,这个JSON摘要承担了两个重要职责:
- 接入CI流水线做自动门禁。CI脚本读取JSON里的
critical和high计数,如果超过阈值就直接让流水线失败,阻止高危变更合入。 - 做多轮审计的进度对账。把一次重大重构拆成多次审计,每次留一份JSON,最后汇总对比,看风险是收敛还是发散。
我在scripts/gen_report.py里还加了一个小功能:解析Markdown报告,提取所有[P0]、[P1]标记的行,自动生成一个漏洞清单。这样即使Agent输出的格式偶尔有偏差,脚本也能兜底提取关键信息。
4.3 只读工具限定的两层含义
安全审计Skill里,Agent能调用的工具必须严格限定为只读操作:Read、Grep、Ripgrep、Git Diff、正则搜索。在SKILL.md的流程说明里,我会专门加一段:
审计过程中禁止执行可能修改系统或仓库状态的命令,包括但不限于:写入文件、运行依赖安装、执行数据库迁移、启动服务、调用外部API发送数据。如需执行脚本辅助分析,脚本本身必须只读,且运行前先向用户确认。
这个限制不只是出于安全考虑,也是为了让Agent更专注。如果它总是想着"要不要运行一个脚本把项目跑起来",注意力就会从静态审计上散掉,很容易把审计搞成一次"半吊子动态测试"——既没有静态的完整性,也没有动态的准确性。
5. 在Claude Code、Codex上实测踩过的坑
把Skill写好和让Skill在真实环境里稳定跑起来,是两种完全不同的体验。我在几个主流Agent工具(Claude Code、Codex、opencode这类环境)里反复测试了几轮,踩了不少坑,挑几个最典型的分享一下。
5.1 模型把"潜在风险"当成"已确认漏洞"一刀切
这是我在第一轮测试时遇到的最大问题。Agent看到代码里用了eval()就直接报"高危命令注入",但实际场景是那个eval()的输入是开发者自己写死在配置文件里的,根本不接收外部输入。这类误报会让整个审计报告失去可信度,团队看着一堆假漏洞就会对审计结论集体免疫,"狼来了"喊多了,真漏洞反而被忽略。
我的对策是在references/untrusted_input.md里给"污染源"做了非常明确的定义,并要求Agent对"输入可控性"做三级判定:
- 已确认可控:输入来自网络请求参数、消息队列消息、用户上传文件等外部通道。此类可确认为"有污染源"。
- 疑似可控:输入来自配置文件、环境变量、数据库记录,但暂不确定是否可被外部影响。此类标记为"疑似"。
- 不可控:输入是代码内联的字面量、编译期常量。此类必须排除或降级为"代码异味"。
有了这个三级判定,Agent在报漏洞前必须先把污染源状态写清楚,误报率明显下降。我曾经在一个中型项目上对比过,加上污染源判定后,高误报量从第一轮的十几条降到了三条以内。
5.2 严重级别分级:不搞CVSS全套,用简洁四级制
一开始我用CVSS打分思路来要求Agent,结果模型输出极不稳定,同一类漏洞在不同文件里经常给出不同分数。后来我抛弃了精确打分,改为简化四级制,并把这套分级写死在SKILL.md里:
| 级别 | 定义 | 典型场景 | 处理策略 |
|---|---|---|---|
| P0 | 无需认证即可远程利用,影响范围大 | 未授权SQL注入、任意文件读取RCE | 立即阻止合入 |
| P1 | 需要认证或本地触发的高危问题 | 越权访问、反序列化、硬编码密钥 | 当轮迭代修复 |
| P2 | 配置与依赖层面的风险 | 过宽CORS、高危版本依赖、错误日志泄露 | 排期修复 |
| P3 | 规范类或潜在风险 | 弱随机数、明文存储、缺少输入长度校验 | 择机优化 |
这个四级分级最大的好处是让Agent在"级别判断"上不需要做复杂计算,只需要把漏洞场景和表格里的定义做模式匹配。分级越简洁,模型输出的一致性越高,这一点在多次实测中验证过。
5.3 Skill加载失败和触发混乱的排查思路
我在换了一个项目目录后,发现Skill怎么都不触发,排查了半天才发现是目录名和Frontmatter里的name大小写不一致导致的。目录叫SecurityAuditSkill,name却写的是security-audit,部分Agent加载时对不上号直接忽略。
排查Skill加载问题,我建议按这个顺序来定位:
- 先确认目录路径是否正确。不同Agent对不同目录有偏好,有些只认项目根目录,有些也认用户全局目录;路径不对,一切白搭。
- 检查Frontmatter格式。
name、description字段是否完整,YAML缩进是否正确,description里是否有特殊字符。 - 确认目录名与name的一致性。统一用小写连字符风格,这是兼容性最高的做法。
- 检查SKILL.md体积。有些Agent会限制单个技能文件的体积,过大可能导致加载超时。我一般把主文件控制在200行以内,额外的细节全部下沉到references。
- 用最短指令验证。比如只输入"对当前项目做安全审计,输出摘要",观察Agent是否触发,排除了用户指令本身就很模糊的情况。
6. 从代码审计到供应链审计:继续扩展的方向
Skill的目录结构天然支持渐进式扩展。security-audit-skill第一版只覆盖代码级审计,后来我给它加了不少能力,扩展起来非常顺滑。
6.1 从单仓库审计扩展到依赖供应链
现代应用超过八成的代码其实来自第三方依赖,只审业务代码远远不够。我给Skill额外加了一个lockfile_scan能力,通过scripts/lockfile_scan.py实现:
- 解析常见的
package-lock.json、pnpm-lock.yaml、Cargo.lock、go.sum、poetry.lock文件; - 标记可疑的版本来源(比如非官方registry镜像域名);
- 检查锁文件是否存在(缺少锁文件本身就是一个供应链风险信号);
- 尝试识别已知的高危版本区间(这个需要定期更新一份本地规则文件,不能依赖模型记忆)。
实测下来,这个扩展对"新项目初始化"场景特别有价值。很多脚手架生成的初始依赖里都藏着历史漏洞版本,模型在没有实时漏洞库的情况下很难凭记忆判断,但配上规则文件后,准确率就完全可用了。
6.2 把审计结果接入CI流水线:让Skill成为团队门禁
一个Skill的价值如果只停留在"开发者本地跑一跑",那它只是个人提效工具。把它变成团队资产,最直接的方式是接入CI流水线。我在团队里设计了一个非常轻量的方案:
- 在CI脚本里调用Agent工具,让它用
security-audit这个Skill扫描本次变更涉及的文件; - 让Agent输出JSON格式摘要;
- 用一条很短的后置判断命令处理摘要,如果
P0或P1数量大于0,流水线直接失败,并附上报告链接。
# 参考命令(需结合具体Agent工具调整) agent_cli audit --diff HEAD~1 --skill security-audit --output audit_result.json python3 -c " import json with open('audit_result.json') as f: data = json.load(f) if data.get('p0_count', 0) > 0 or data.get('p1_count', 0) > 0: print('安全审计未通过') exit(1) print('安全审计通过') "这种做法虽然不能替代人类安全工程师的最终裁定,但能挡住相当一部分低级错误在合入主干之前流入主线,已经能为团队省下大量返工时间。
关于这个Skill,我个人实际使用最大的体感是:它不是用来炫技的,而是能把一次专业的代码安全评审经验,沉淀成任何一个Agent都能照着执行的标准作业流程。如果团队里每周还在重复做同样模式的代码评审,那把这份经验固化成一个Skill,可能是这个月性价比最高的一件事。一个小建议:与其一开始就想做一个"全知全能"的安全审计Skill,不如把团队过去半年里踩过的最常见的安全问题整理进references,从五个最痛的场景起步,效果一定比照搬我这一套来得更快。