如何用 Claude Code Hook 把团队「提示词约定」固化为可执行的 Skill 自动映射?
本文面向有一定 Claude Code 使用经验的开发者。如果你的项目已经积累了大量自定义 Skill,并且正在为「每次都要手动挂载」感到烦恼,这篇文章或许能帮到你。
在一个业务系统项目的迭代过程中,我们沉淀了十几个领域专属的 Claude Code Skill,同时团队也形成了一套「[关键词] 触发对应能力」的提示词约定。但这套约定只存在于 Wiki 和口头传递里——Claude 本身并不知道 [模块A] 和 /skill-a 之间有任何关系。每次切换业务域都要先手动调用 Skill,这个摩擦点虽小,但在高频迭代下积累下来,已经严重影响了节奏。本文记录了我们最终的解法。
背景:一个真实的工程痛点
随着项目迭代,团队往往会沉淀大量领域专属的 Claude Code Skill:
/skill-a # 模块A 核心处理
/skill-b # 模块B 核心处理
/skill-c # 模块C 核心处理
/skill-d # 模块D 核心处理
这些 Skill 极大提升了 AI 的专业能力,但有一个问题:它们都是手动挂载的局部 Skill,每次使用都要先 /skill-a,再描述需求。
更麻烦的是,团队已经形成了一套「提示词约定」:
用
[模块A]来表示这条需求需要用模块A相关能力处理。
这个约定存在于 Wiki、代码注释、甚至口头传递。但 Claude 不知道 [模块A] 和 /skill-a 之间有任何关系。
目标
我们想要达成的效果:
用户输入:[模块A] 帮我处理这批数据
Claude 自动:
1. 识别 [模块A] 标记
2. 调用 /skill-a skill
3. 结合原始请求处理任务
全程零手动,约定即规则。
思路拆解
解决这个问题有三个层次:
| 层次 | 方案 | 可靠性 | 可维护性 |
|---|---|---|---|
| 提示词层 | 在 CLAUDE.md 写映射表,让 Claude 自己判断 | ⭐⭐⭐ | ⭐⭐⭐⭐ |
| 基础设施层 | UserPromptSubmit Hook 拦截并注入 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
| 两者结合 | Hook 处理 + CLAUDE.md 兜底说明 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
推荐方案:Hook 处理 + CLAUDE.md 文档兜底。
原因:Hook 在基础设施层运行,不依赖 Claude 的「理解」和「记忆」,映射关系以代码形式固化,更可靠;CLAUDE.md 则作为文档,让团队成员和 AI 都能理解约定的含义。
完整实现
目录结构
项目根目录/
├── .claude/
│ ├── hooks/
│ │ └── skill-trigger.js # Hook 脚本
│ ├── skill-mapping.json # 关键词映射表(核心配置)
│ └── settings.json # Hook 注册配置
└── CLAUDE.md # 项目规范文档(兜底说明)
Step 1:定义映射表
新建 .claude/skill-mapping.json,这是整套方案的核心配置,所有关键词和 Skill 的对应关系都在这里维护:
{
"模块A": "/skill-a",
"模块B": "/skill-b",
"模块C": "/skill-c",
"模块D": "/skill-d",
"模块E": "/skill-e",
"模块F": "/skill-f"
}
设计原则:映射表与代码分离。新增一个业务场景,只需在这里加一行,不需要改任何逻辑代码。
Step 2:编写 Hook 脚本
新建 .claude/hooks/skill-trigger.js:
#!/usr/bin/env node
/**
* Claude Code UserPromptSubmit Hook
* 功能:检测用户输入中的 [关键词] 标记,自动注入对应 Skill 的调用指令
*/
const fs = require('fs');
const path = require('path');
// 读取用户输入
const input = JSON.parse(fs.readFileSync('/dev/stdin', 'utf8'));
const prompt = input.prompt ?? '';
// 加载映射配置
const mappingPath = path.join(
process.env.CLAUDE_PROJECT_DIR ?? process.cwd(),
'.claude/skill-mapping.json'
);
// 配置文件不存在则透传,不影响正常使用
if (!fs.existsSync(mappingPath)) {
process.exit(0);
}
const mapping = JSON.parse(fs.readFileSync(mappingPath, 'utf8'));
// 检测所有命中的关键词
const matchedSkills = [];
for (const [keyword, skill] of Object.entries(mapping)) {
const pattern = new RegExp(`\[${keyword}\]`, 'g');
if (pattern.test(prompt)) {
matchedSkills.push({ keyword, skill });
}
}
// 无命中,透传原始 prompt
if (matchedSkills.length === 0) {
process.exit(0);
}
// 构建注入指令
const injectionLines = matchedSkills.map(
({ keyword, skill }) =>
`- 检测到关键词 [${keyword}],请先调用 skill: "${skill.replace('/', '')}",获取该领域的专业能力后再处理请求。`
);
const injectedPrompt = `
【系统提示 - Skill 自动触发】
${injectionLines.join('\n')}
请按上述指引依次调用对应 skill,然后处理以下原始请求:
---
${prompt}
`.trim();
// 输出增强后的 prompt
const result = {
hookSpecificOutput: {
permittedToMakeChanges: true,
updatedPrompt: injectedPrompt,
},
};
process.stdout.write(JSON.stringify(result));
Step 3:注册 Hook
编辑 .claude/settings.json(项目级配置),将脚本挂载到 UserPromptSubmit 事件:
{
"hooks": {
"UserPromptSubmit": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "node .claude/hooks/skill-trigger.js"
}
]
}
]
}
}
注意:
matcher为空字符串表示拦截所有用户输入。如果你只想在特定条件下触发,可以填写正则表达式,例如"\[.+\]"表示只处理包含方括号标记的输入。
Step 4:CLAUDE.md 文档兜底
在项目 CLAUDE.md 中补充说明,让 AI 和团队成员都理解这套约定:
## Skill 关键词约定
本项目使用 `[关键词]` 格式的标记来声明所需的业务能力域。
Hook 系统会自动识别并调用对应的 Skill,无需手动挂载。
### 关键词映射表
| 关键词 | 对应 Skill | 说明 |
|--------|-----------|------|
| `[模块A]` | `/skill-a` | 模块A 核心业务处理 |
| `[模块B]` | `/skill-b` | 模块B 核心业务处理 |
| `[模块C]` | `/skill-c` | 模块C 核心业务处理 |
| `[模块D]` | `/skill-d` | 模块D 核心业务处理 |
### 使用示例
[模块A] 这批数据需要处理,帮我整理一下 [模块C][模块D] 这个需求涉及两个模块,帮我一起处理
> 多个关键词可以同时使用,Hook 会依次触发对应的所有 Skill。
运行效果演示
单关键词触发
# 用户输入
[模块A] 帮我处理这批数据
# Hook 处理后 Claude 看到的 prompt
【系统提示 - Skill 自动触发】
- 检测到关键词 [模块A],请先调用 skill: "skill-a",获取该领域的专业能力后再处理请求。
请按上述指引依次调用对应 skill,然后处理以下原始请求:
---
[模块A] 帮我处理这批数据
多关键词叠加触发
# 用户输入
[模块C][模块D] 这个需求涉及两个业务模块
# Hook 处理后 Claude 看到的 prompt
【系统提示 - Skill 自动触发】
- 检测到关键词 [模块C],请先调用 skill: "skill-c",获取该领域的专业能力后再处理请求。
- 检测到关键词 [模块D],请先调用 skill: "skill-d",获取该领域的专业能力后再处理请求。
请按上述指引依次调用对应 skill,然后处理以下原始请求:
---
[模块C][模块D] 这个需求涉及两个业务模块
普通输入透传
# 用户输入(无关键词标记)
帮我看一下这段代码有没有问题
# Hook 无操作,prompt 原样透传给 Claude
进阶:动态加载全局映射
如果你的 Skill 是跨项目共享的,可以将映射表放在用户级别的 Claude 配置目录,实现全局生效:
// skill-trigger.js 中修改映射文件查找逻辑
const mappingCandidates = [
// 优先项目级配置
path.join(process.env.CLAUDE_PROJECT_DIR ?? process.cwd(), '.claude/skill-mapping.json'),
// 兜底用户级全局配置
path.join(process.env.HOME, '.claude/skill-mapping.json'),
];
let mapping = {};
for (const p of mappingCandidates) {
if (fs.existsSync(p)) {
const partial = JSON.parse(fs.readFileSync(p, 'utf8'));
mapping = { ...mapping, ...partial }; // 项目级覆盖全局级
break;
}
}
这样,全局通用的 Skill 放全局配置,项目专属的覆盖全局,层次清晰。
方案优缺点总结
优点
- 零认知负担:开发者只需按约定写
[关键词],其他的交给基础设施 - 配置即文档:
skill-mapping.json本身就是 Skill 目录索引,提交到 git 后全团队共享 - 基础设施层保障:不依赖 Claude 的上下文记忆,每次对话都稳定触发
- 灵活扩展:新增映射只改 JSON,不动代码逻辑
- 渐进增强:无关键词的普通输入完全不受影响
局限
- 关键词冲突:如果两个 Skill 关键词相似,需要团队约定好命名规范,避免歧义
- 执行顺序:多 Skill 同时触发时,Claude 会依次调用,存在顺序依赖时需要额外说明
- Hook 维护成本:需要确保脚本在所有开发环境下可执行(Node.js 版本、权限等)
小结
这套方案的本质是:把隐性的团队约定,通过基础设施层的代码显式固化下来。
它不是银弹,但在以下场景下会显著提升开发体验:
- 项目已积累 5 个以上的业务领域 Skill
- 团队已形成稳定的「关键词提示词」约定
- 对话场景中频繁需要切换不同业务领域
如果你的团队恰好在这个阶段,不妨试试看。