如何用 Claude Code Hook 把团队「提示词约定」固化为可执行的 Skill 自动映射?
本文面向有一定 Claude Code 使用经验的开发者。如果你的项目已经积累了大量自定义 Skill,并且正在为「每次都要手动挂载」感到烦恼,这篇文章或许能帮到你。
在一个业务系统项目的迭代过程中,我们沉淀了十几个领域专属的 Claude Code Skill,同时团队也形成了一套「[关键词] 触发对应能力」的提示词约定。但这套约定只存在于 Wiki 和口头传递里------Claude 本身并不知道 [模块A] 和 /skill-a 之间有任何关系。每次切换业务域都要先手动调用 Skill,这个摩擦点虽小,但在高频迭代下积累下来,已经严重影响了节奏。本文记录了我们最终的解法。
背景:一个真实的工程痛点
随着项目迭代,团队往往会沉淀大量领域专属的 Claude Code Skill:
bash
/skill-a # 模块A 核心处理
/skill-b # 模块B 核心处理
/skill-c # 模块C 核心处理
/skill-d # 模块D 核心处理
这些 Skill 极大提升了 AI 的专业能力,但有一个问题:它们都是手动挂载的局部 Skill,每次使用都要先 /skill-a,再描述需求。
更麻烦的是,团队已经形成了一套「提示词约定」:
用
[模块A]来表示这条需求需要用模块A相关能力处理。
这个约定存在于 Wiki、代码注释、甚至口头传递。但 Claude 不知道 [模块A] 和 /skill-a 之间有任何关系。
目标
我们想要达成的效果:
css
用户输入:[模块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 都能理解约定的含义。
完整实现
目录结构
bash
项目根目录/
├── .claude/
│ ├── hooks/
│ │ └── skill-trigger.js # Hook 脚本
│ ├── skill-mapping.json # 关键词映射表(核心配置)
│ └── settings.json # Hook 注册配置
└── CLAUDE.md # 项目规范文档(兜底说明)
Step 1:定义映射表
新建 .claude/skill-mapping.json,这是整套方案的核心配置,所有关键词和 Skill 的对应关系都在这里维护:
css
{
"模块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:
ini
#!/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 事件:
json
{
"hooks": {
"UserPromptSubmit": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "node .claude/hooks/skill-trigger.js"
}
]
}
]
}
}
注意 :
matcher为空字符串表示拦截所有用户输入。如果你只想在特定条件下触发,可以填写正则表达式,例如"\[.+\]"表示只处理包含方括号标记的输入。
Step 4:CLAUDE.md 文档兜底
在项目 CLAUDE.md 中补充说明,让 AI 和团队成员都理解这套约定:
markdown
## Skill 关键词约定
本项目使用 `[关键词]` 格式的标记来声明所需的业务能力域。
Hook 系统会自动识别并调用对应的 Skill,无需手动挂载。
### 关键词映射表
| 关键词 | 对应 Skill | 说明 |
|--------|-----------|------|
| `[模块A]` | `/skill-a` | 模块A 核心业务处理 |
| `[模块B]` | `/skill-b` | 模块B 核心业务处理 |
| `[模块C]` | `/skill-c` | 模块C 核心业务处理 |
| `[模块D]` | `/skill-d` | 模块D 核心业务处理 |
### 使用示例
模块A\] 这批数据需要处理,帮我整理一下 \[模块C\]\[模块D\] 这个需求涉及两个模块,帮我一起处理 ```markdown > 多个关键词可以同时使用,Hook 会依次触发对应的所有 Skill。 ``` *** ** * ** *** ### 运行效果演示 #### 单关键词触发 ```yaml # 用户输入 [模块A] 帮我处理这批数据 # Hook 处理后 Claude 看到的 prompt 【系统提示 - Skill 自动触发】 - 检测到关键词 [模块A],请先调用 skill: "skill-a",获取该领域的专业能力后再处理请求。 请按上述指引依次调用对应 skill,然后处理以下原始请求: --- [模块A] 帮我处理这批数据 ``` #### 多关键词叠加触发 ```yaml # 用户输入 [模块C][模块D] 这个需求涉及两个业务模块 # Hook 处理后 Claude 看到的 prompt 【系统提示 - Skill 自动触发】 - 检测到关键词 [模块C],请先调用 skill: "skill-c",获取该领域的专业能力后再处理请求。 - 检测到关键词 [模块D],请先调用 skill: "skill-d",获取该领域的专业能力后再处理请求。 请按上述指引依次调用对应 skill,然后处理以下原始请求: --- [模块C][模块D] 这个需求涉及两个业务模块 ``` #### 普通输入透传 ```bash # 用户输入(无关键词标记) 帮我看一下这段代码有没有问题 # Hook 无操作,prompt 原样透传给 Claude ``` *** ** * ** *** ### 进阶:动态加载全局映射 如果你的 Skill 是跨项目共享的,可以将映射表放在用户级别的 Claude 配置目录,实现全局生效: ```csharp // 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 * 团队已形成稳定的「关键词提示词」约定 * 对话场景中频繁需要切换不同业务领域 如果你的团队恰好在这个阶段,不妨试试看。