Claude Code自定义Skill完整教程:手把手教你写MyBatis SQL注入检查
导读:知乎上"有哪些好的 Claude Code Skills 推荐"有 12 万多浏览,但别人的 Skill 多半是通用的,真正省事的是把自己团队反复交代的那套检查写成 Skill。这篇从零写一个 Java 后端用得上的 Skill:检查 MyBatis Mapper XML 里的 ${} 有没有 SQL 注入风险。在 Claude Code 2.1.299 上实测了自动触发、手动调用、命令注入,其中有一个坑会让 Claude 给出"没发现问题"的错误结论,写 Skill 的人最好都看一眼。
一、先说清楚:Skill 是什么,什么时候值得写
一句白话:Skill 就是一份写给 Claude 的操作手册,放在固定目录里,需要的时候它自己会翻出来看。
打个比方,CLAUDE.md 像是贴在工位上的团队规范,每次对话都会读;Skill 像是放在抽屉里的专项手册,平时 Claude 只看得到封面上的一句简介(description),等你问到相关的事,或者你直接敲 /手册名,它才把整本拿出来照着做。
| 对比 | CLAUDE.md | Skill |
|---|---|---|
| 什么时候进上下文 | 每次会话都加载 | 平时只加载一句简介,用到时才加载全文 |
| 适合放什么 | 人人都要遵守的通用规范 | 某一类任务的具体步骤、脚本、参考资料 |
| 能不能带脚本 | 不能 | 可以,放在 Skill 目录里一起分发 |
| 怎么触发 | 自动 | Claude 自动判断,或你敲 /skill-name |
不写会怎样:同一套要求每次都要在对话里重新交代一遍,换个同事就交代得不一样;或者全塞进 CLAUDE.md,越写越长,每次对话都在为用不上的内容付 token。
什么时候值得写:一件事你已经跟 Claude 交代过两三次、步骤基本固定,就该写成 Skill。比如提交前查 SQL、按团队模板生成 CRUD、发版前的检查清单。
二、最小的 Skill 长什么样
2.1 放在哪
| 位置 | 路径 | 谁能用 |
|---|---|---|
| 个人 | ~/.claude/skills/<skill名>/SKILL.md | 你在这台机器上的所有项目 |
| 项目 | .claude/skills/<skill名>/SKILL.md | 这个仓库里的会话,提交后团队都能用 |
| 插件 | <插件>/skills/<skill名>/SKILL.md | 装了这个插件的人,命令名带插件前缀 |
团队规范类的放项目目录,随代码提交;个人习惯类的放 ~/.claude/skills/。
2.2 SKILL.md 的两部分
一个目录、一个 SKILL.md 就是一个 Skill。文件开头 --- 之间是 YAML 格式的元信息(frontmatter),下面是给 Claude 的正文:
--- name: api-conventions description: 本项目的接口设计规范。写 Controller、定义接口出入参、设计错误码时使用。 --- 写接口时: - 统一返回 Result<T>,分页返回 PageVO<T> - 入参用 @Valid 校验,必填字段加 @NotBlank / @NotNull - Long 类型 ID 序列化成字符串,避免前端精度丢失
最常用的几个字段:
| 字段 | 作用 |
|---|---|
name | 命令名,不写就用目录名。上面这个可以用 /api-conventions 调用 |
description | 最重要。Claude 靠它判断什么时候自动加载这个 Skill |
disable-model-invocation | 设成 true 后只有你敲命令才会触发,Claude 不会自己调用 |
allowed-tools | 这个 Skill 运行的那一轮里,哪些工具不用你确认就能用 |
context: fork | 放到独立的子代理里跑,不占主对话的上下文 |
官方文档特别提醒:字段名写错(比如 disable_model_invocation 用了下划线),Claude Code 不报错,直接忽略;YAML 格式写坏了,Skill 照样能用 / 调用,但 description 失效,Claude 不会自动触发它。排查时用 claude --debug 看解析错误。
三、实战:写一个检查 MyBatis${}的 Skill
MyBatis 里 #{} 是预编译参数,安全;${} 是字符串直接拼进 SQL,用户能控制这个值就有注入风险。但 ORDER BY 字段名、排序方向这类地方又只能用 ${}。所以这件事适合交给 Claude:先用脚本把所有 ${} 找全,再让 Claude 逐条判断是该改 #{} 还是该加白名单。
3.1 测试项目
在 scratchpad 临时目录建了一个最小项目,OrderMapper.xml 里埋了 3 处 ${}:
<!-- src/main/resources/mapper/OrderMapper.xml -->
<mapper namespace="demo.mapper.OrderMapper">
<select id="getById" resultType="demo.entity.Order">
SELECT * FROM t_order WHERE id = #{id}
</select>
<select id="pageByUser" resultType="demo.entity.Order">
SELECT * FROM t_order WHERE user_id = #{userId} ORDER BY ${orderBy} ${sortDir}
</select>
<select id="searchByName" resultType="demo.entity.Order">
SELECT * FROM t_order WHERE order_no LIKE '%${keyword}%'
</select>
</mapper>另外一个 UserMapper.xml 只用了 #{},作为对照。
3.2 目录结构
.claude/skills/mybatis-sql-check/
├── SKILL.md
└── scripts/
└── scan_mapper.py
脚本跟着 Skill 一起放,提交到仓库后团队拉下来就能用,不用每个人自己装。
3.3 扫描脚本
找 ${} 这种机械活交给脚本,比让 Claude 自己 grep 稳定:
#!/usr/bin/env python3
# List every ${...} in MyBatis mapper XML files; exit 1 when any is found
import pathlib, re, sys
root = pathlib.Path(sys.argv[1] if len(sys.argv) > 1 else "src/main/resources")
if not root.is_dir():
print(f"目录不存在:{root},没有扫描")
sys.exit(2)
hits = 0
for xml in sorted(root.rglob("*.xml")):
for no, line in enumerate(xml.read_text(encoding="utf-8").splitlines(), 1):
for m in re.finditer(r"\$\{([^}]+)\}", line):
hits += 1
print(f"{xml}:{no}: ${{{m.group(1)}}} | {line.strip()}")
print(f"共扫描 {len(list(root.rglob('*.xml')))} 个 XML,{hits} 处 ${{}}")
sys.exit(1 if hits else 0)
在项目根目录单独跑一下,3 处都找到了:
# 先脱离 Claude Code 验证脚本本身 python3 .claude/skills/mybatis-sql-check/scripts/scan_mapper.py src/main/resources
输出:
src/main/resources/mapper/OrderMapper.xml:8: ${orderBy} | SELECT * FROM t_order WHERE user_id = #{userId} ORDER BY ${orderBy} ${sortDir}
src/main/resources/mapper/OrderMapper.xml:8: ${sortDir} | SELECT * FROM t_order WHERE user_id = #{userId} ORDER BY ${orderBy} ${sortDir}
src/main/resources/mapper/OrderMapper.xml:11: ${keyword} | SELECT * FROM t_order WHERE order_no LIKE '%${keyword}%'
共扫描 2 个 XML,3 处 ${}目录不存在时打印一句话并退出 2,这是踩坑之后才加的,第四节会讲为什么。
3.4 SKILL.md(最终版)
---
name: mybatis-sql-check
description: 检查 MyBatis Mapper XML 里的 ${} 拼接,判断有没有 SQL 注入风险并给出 #{} 或白名单改法。用户提到 SQL 注入、Mapper XML 检查、${} 和 #{} 区别、提交前查 SQL 时使用。
allowed-tools: Read Grep Bash(python3 ${CLAUDE_SKILL_DIR}/scripts/scan_mapper.py *)
---
## 扫描结果
!`python3 ${CLAUDE_SKILL_DIR}/scripts/scan_mapper.py ${CLAUDE_PROJECT_DIR}/src/main/resources || true`
## 要做的事
扫描结果里如果写着"目录不存在",先告诉用户,不要给结论。
逐条判断每一处 `${}`:
1. 来自用户输入的值(关键字、ID、名称):必须改成 `#{}`,LIKE 用 `CONCAT('%', #{keyword}, '%')`
2. ORDER BY 字段、排序方向、表名这类不能用 `#{}` 的:保留 `${}`,要求在 Java 层用白名单校验,给出校验代码
3. 输出一张表:文件:行号 / 变量 / 风险 / 改法
只给改法,不要直接修改文件。几处写法说明:
| 写法 | 作用 |
|---|---|
| !`命令` | 动态注入:Claude Code 先在本机执行命令,用输出替换这一行,再把整段内容交给 Claude。Claude 拿到的是扫描结果,不是命令 |
| ${CLAUDE_SKILL_DIR} | Skill 自己所在的目录。脚本路径用它,不管从哪个目录启动都能找到 |
| ${CLAUDE_PROJECT_DIR} | 项目根目录 |
| allowed-tools 里的 Bash(...) | 预先放行这条脚本命令。注入命令不会弹窗询问,没放行的会直接让 Skill 中止 |
| true` | 脚本发现问题时退出码是 1,不加这个 Skill 会中止(第四节坑 2) |
3.5 实测:两种触发方式
手动调用,在项目目录里执行(交互模式下直接输入 /mybatis-sql-check 一样):
# --permission-mode default:按默认权限模式跑,后面解释为什么要指定 claude -p "/mybatis-sql-check" --model haiku --permission-mode default
Claude 拿到扫描结果后读了一遍 XML 核对,输出一张表:${keyword} 判为高风险,改成 CONCAT('%', #{keyword}, '%');${orderBy}、${sortDir} 保留 ${},给了 Java 白名单代码,最后注明没有修改文件。
自动触发,不提 Skill 名,只说需求:
claude -p "帮我看看这个项目的 mapper 有没有 SQL 注入风险,只给结论和改法,不要改文件" --model haiku --permission-mode default
用 --output-format stream-json --verbose 看工具调用,第一步就是:
TOOL_USE Skill {"skill": "mybatis-sql-check", "args": "检查当前项目 Mapper XML 的 ${} 拼接注入风险,只给结论和改法,不修改文件"}
Claude 根据 description 自己选中了这个 Skill。作为对照,问一个无关的问题"src/main/resources 下面有几个 xml 文件",它直接用 find 数了,没有调用 Skill。
本文的用例为了省钱都用 --model haiku 跑,每种情况跑了 1~2 次。会不会自动触发是模型判断的,换模型、换问法结果可能不同,你自己的 Skill 要用平时用的模型多试几种问法。
description 里要把用户会说的词写进去:这里写了"SQL 注入""Mapper XML 检查""${} 和 #{} 区别""提交前查 SQL"。官方文档的说法是把关键用例放在最前面,description 加 when_to_use 合起来超过 1536 个字符会被截断。
四、实测踩到的 3 个坑
坑 1:注入命令没放行,Skill 直接中止
第一版 allowed-tools 只写了 Read Grep,没放行脚本。默认权限模式下一调用就中止,claude -p 的输出是空的,费用是 0,stream-json 里能看到:
Shell command permission check failed for pattern "!`python3 .../scripts/scan_mapper.py src/main/resources`": This command requires approval
注入命令在渲染 Skill 时执行,不会弹窗问你,没被放行就直接中止。解决办法就是 3.4 节那样,在 allowed-tools 里放行这条命令。
本机全局设置的默认权限模式是 auto,同一个 Skill 在 auto 模式下没有中止:Claude 收到"先执行这条命令"的提示,自己用 Bash 工具跑了一遍。官方文档对 auto 模式的描述也是这样。所以 Skill 在你机器上能跑,不代表在同事的默认模式下也能跑,测 Skill 时加 --permission-mode default。
坑 2:脚本退出码非 0,整个 Skill 中止
放行之后再跑,还是中止,这次的报错是:
Shell command failed for pattern "!`python3 .../scripts/scan_mapper.py src/main/resources`": [stderr]
src/main/resources/mapper/OrderMapper.xml:8: ${orderBy} | ...
脚本找到问题时 exit 1,Claude Code 把非 0 退出码当作命令失败,整个 Skill 中止,Claude 一个字都看不到。官方文档写明了:除了 grep、diff 这类搜索和比较命令的退出码 1,其余非 0 都算失败。检查类脚本"发现问题就返回非 0"是常见习惯,在 Skill 里要在命令后加 || true。
坑 3:把$ARGUMENTS传进注入命令,可能静默给出错误结论
这个坑最隐蔽。第一版的注入命令是这样写的,想让用户能传目录:
!`python3 ${CLAUDE_SKILL_DIR}/scripts/scan_mapper.py $ARGUMENTS || true`
手动调用 /mybatis-sql-check src/main/resources 没问题。但 Claude 自动触发时,传进来的参数是它自己写的一句话:
检查当前项目 Mapper XML 的 ${} 拼接注入风险,只给结论和改法,不修改文件
我分别用两种参数复现了一下:
| 传给 Skill 的参数 | 结果 |
|---|---|
检查 ${} 拼接(带 ${}) | Skill 中止:A variable in braces in this command can't be checked before it runs |
检查当前项目的注入风险(普通一句话) | 脚本把这句话当目录名去扫,扫到 0 个 XML,输出"共 0 处";Claude 据此回答"未发现需要改造的 ${}" |
第二种最危险:没有报错,结论是错的,看上去还很像回事。
改法有两处:
- 注入命令里不用
$ARGUMENTS,目录写死成${CLAUDE_PROJECT_DIR}/src/main/resources,需要可变的部分让 Claude 在正文里按参数自己处理 - 脚本对输入做校验:目录不存在就明确说"没有扫描",并在 SKILL.md 里要求 Claude 遇到这句话先告诉用户、不给结论
改完后两种参数都正常:带 ${} 的参数不再中止,自动触发时扫描结果是 3 处。
五、控制谁能触发
| 写法 | 你能 / 调用 | Claude 能自动调用 | 适合 |
|---|---|---|---|
| 默认 | ✅ | ✅ | 检查、规范、参考资料类 |
disable-model-invocation: true | ✅ | ❌ | 发版、提交、发消息这类有副作用的动作 |
user-invocable: false | ❌ | ✅ | 纯背景知识,比如老系统的业务说明 |
我在 mybatis-sql-check 上加了 disable-model-invocation: true 测了一下:同样问"mapper 有没有 SQL 注入风险",Claude 没有调用 Skill,自己用 grep 和 cat 查了一遍;直接敲 /mybatis-sql-check 仍然正常。
我的建议:会改文件、调外部系统、发版的 Skill 一律加 disable-model-invocation: true,什么时候跑由人决定;只读的检查类 Skill 保持默认,让它在你提到相关问题时自己出来。
allowed-tools 不受工作区信任限制,claude -p 在没信任过的目录里也会生效。别人仓库里带的 Skill,先看一眼它的 allowed-tools 放行了什么再跑。
六、几条写法建议
- 正文写"做什么",不写"为什么"。Skill 一旦加载,内容会一直留在对话里,每一行都是持续的 token 开销
- 机械活交给脚本,判断交给 Claude。找全
${}、统计行数、跑检查用脚本,结果注入进去;哪个该改、怎么改让 Claude 判断 - 长资料拆成附属文件。参考文档、示例放在 Skill 目录里,在 SKILL.md 里写清楚"什么情况读哪个文件",用到时才加载
- 改完先用默认权限模式跑一遍,再用一句不提 Skill 名的话试试能不能自动触发
- 项目级 Skill 提交到仓库,团队拿到的是同一份;个人偏好放
~/.claude/skills/
七、速查表
| 想做什么 | 怎么写 |
|---|---|
| 新建 Skill | .claude/skills/<名字>/SKILL.md |
| 让 Claude 知道什么时候用 | description 写清用途和用户会说的词 |
| 只允许手动触发 | disable-model-invocation: true |
| 运行时不弹确认 | allowed-tools: Bash(python3 ${CLAUDE_SKILL_DIR}/scripts/scan.py *) |
| 引用 Skill 自带脚本 | ${CLAUDE_SKILL_DIR}/scripts/... |
| 接收参数 | 正文里用 $ARGUMENTS、$0、$1,别放进注入命令 |
| 放到子代理里跑 | context: fork |
| 排查不触发 | claude --debug 看 frontmatter 解析错误 |
八、常见问题
Q:Skill 和以前的 .claude/commands/ 有什么区别?
commands 下的单个 md 文件是老格式,现在还能用,frontmatter 也基本一样。官方建议新写的用 Skill,因为 Skill 是一个目录,可以带脚本和参考文件。
Q:改了 SKILL.md 要重启会话吗?
按官方文档,不用。Claude Code 会监听 ~/.claude/skills/ 和项目 .claude/skills/ 的变化,改了 SKILL.md 当前会话就能用上;会话开始时还不存在的顶层 skills 目录要执行一次 /reload-skills。本文的测试每次都是新起的 claude -p,这一条没有实测。
Q:写好的 Skill 能给 claude.ai 或 API 用吗?
能用的字段有限制。按官方文档,上传到 claude.ai 或走 Skills API 只认 name、description、license、compatibility、metadata、allowed-tools 六个字段,多了会直接报错;!`命令` 注入也只在 Claude Code 里生效。本文没测这条路径。
以上就是Claude Code自定义Skill完整教程:手把手教你写MyBatis SQL注入检查的详细内容,更多关于Claude Code自定义Skill的资料请关注脚本之家其它相关文章!
相关文章
本文深入解析Claude Code中Skills的核心价值与5种高级应用,包括构建项目知识库Skill、设计链式代码影响分析Skill、打造沉浸式调试会话Skill、实现预提交自动化代码审查Ski2026-10-21
Claude Code子代理和Skills怎么写?一文分享3个拿来即用的实用示例
本文用三个实战例子教你配置代码审查员和测试排查员子代理,以及创建release-notes技能,让不同任务各归其位,掌握description写法,让Claude自动派活,告别手动指挥,感兴趣的2026-10-21
Claude Code Skills从项目级安装到全局复用实战指南
掌握ClaudeCode Skills机制,让AI秒懂你的项目规范与工作流,本文手把手教你安装项目级Skill、平滑迁移到全局复用,附必装清单和避坑经验,新手也能快速上手,彻底告别每次重复2026-10-10
11个提升Claude Code战斗力的顶级Skills小结
本文主要介绍了11个提升Claude Code战斗力的顶级Skills小结,涵盖代码审查、跨会话记忆和流程约束,帮你解决AI随意发挥、记忆丢失等痛点,感兴趣的可以了解一下2026-09-30
Claude Code Skill质量检查实战教学:用/skill-doctor + Plugin Evals验证Skill真实效果
Claude Code新出的/skill-doctor和claudeplugineval可以帮你解决Skill装了不调用、上下文越用越长的问题,下面我们就来看看如何用/skill-doctor体检存量Skill,找出高成本低2026-09-29
全网首发!Claude Code 10 个必装 Skills 扩展包保姆级配置详细攻略(附安装指令)
Claude Code Skills是可插拔的模块化能力扩展系统,每个Skill通过SKILL.md文件定义专项功能,无需代码即可为Claude注入新能力,Skills支持个人、项目和插件三种存储方式,本2026-09-23
本文主要介绍了Claude Code 常用 Skill 完全指南,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学2026-09-22
Trae / Claude Code Git提交Skill|AI编码完成自动生成规范Git提交脚本
使用Trae、ClaudeCode本地修改项目代码后,手动敲Git命令繁琐,本文分别提供Trae标准SKILL.md技能文件、Claude Code Skill源文档,AI改写代码结束后自动引导完整Git提交流程2026-09-03
Claude Code Skills实现AI辅助编程的高效指令集封装
本文主要介绍了Claude Code Skills实现AI辅助编程的高效指令集封装,文中通过示例介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编2026-07-20
Claude Code运维实战:用AI Skill打造自动化运维新范式
当 AI 编程成为主流,运维工程师如何让 Claude Code 从写代码的助手进化为7×24 值班的运维搭档?本文从实战角度出发,系统梳理 DevOps/SRE 场景下的 Skills 生态,让你的2026-07-10









最新评论