很多团队用 Claude Code 的方式,还停留在"打开终端、输入问题、等回答"的阶段。
这没什么问题,Claude Code 本身就是一个很好的对话式编程助手。但用得越多越会发现,真正拉开效率差距的,不是谁的 prompt 写得更好,而是谁把自己的工作流"固化"进了 Claude Code 的扩展体系里。
换句话说,Claude Code 提供了四种扩展机制 -- Skill、Command、Hook、Subagent -- 它们分别解决不同层面的问题。
这篇文章适合已经在用 Claude Code、想把它从"好用的工具"升级为"团队基础设施"的工程师和技术管理者。
太长不看版
- Command:一个 Markdown 文件就是一个斜杠命令,零成本固化常用 prompt,是最轻量的扩展方式
- Skill:支持 YAML frontmatter、目录结构、自动触发,是 Command 的全面升级,也是官方推荐的扩展方式
- Hook:确定性的安全护栏,通过 exit code 控制是否阻断工具执行,不存在"LLM 偶尔忘了"的问题
- Subagent:运行在独立上下文窗口中的专项智能体,不会污染主会话,适合处理重型任务
- 组合使用:四种机制不是互斥的 -- Skill 定义流程、Hook 守住底线、Subagent 处理重活,所有配置都是文件驱动的,天然适合 Git 管理和团队协作
先把这四个概念的关系理清楚
在展开细节之前,先看一张全景图。很多人第一次接触这四个概念时会觉得它们有重叠,其实它们各自守着不同的职责边界:
graph TB
User["开发者"] -->|"/命令名"| CMD["Command / Skill"]
User -->|"正常对话"| Claude["Claude Code 主会话"]
Claude -->|"自动识别触发"| SKL["Skill (auto)"]
Claude -->|"调用工具前后"| HK["Hook"]
Claude -->|"委派复杂任务"| SA["Subagent"]
CMD -->|"注入 prompt"| Claude
SKL -->|"注入领域知识"| Claude
HK -->|"exit 0: 放行 / exit 2: 阻断"| Tool["工具执行"]
SA -->|"独立上下文窗口"| Result["返回结果"]
classDef user fill:#1A9090,stroke:#147070,color:#fff
classDef core fill:#0d4f4f,stroke:#1A9090,color:#fff
classDef ext fill:#1a3a3a,stroke:#1A9090,color:#ccc
class User user
class Claude,Tool core
class CMD,SKL,HK,SA,Result ext
简单来说:
| 机制 | 触发方式 | 执行者 | 核心价值 |
|---|---|---|---|
| Command | 用户手动 /命令名 | Claude(LLM) | 固化常用 prompt |
| Skill | 手动或 Claude 自动 | Claude(LLM) | 领域知识 + 工作流自动化 |
| Hook | 工具调用时自动触发 | Shell 脚本(确定性) | 安全护栏 + 自动化后处理 |
| Subagent | Claude 主动委派 | 独立 Claude 实例 | 隔离上下文 + 专项任务 |
接下来逐个拆开看。
Command:最轻量的扩展,一个 Markdown 就够了
Command 是 Claude Code 最早提供的扩展方式,核心思路极其简单:把一个 Markdown 文件变成一个斜杠命令。
你在 .claude/commands/ 目录下放一个 review.md,里面写上你希望 Claude 执行的 prompt 模板,之后在对话中输入 /project:review,Claude 就会把这个文件的内容当作指令来执行。
目录结构
项目根目录/
└── .claude/
└── commands/
├── review.md → /project:review
├── test-plan.md → /project:test-plan
└── refactor.md → /project:refactor
~/.claude/
└── commands/
├── daily-standup.md → /user:daily-standup
└── code-style.md → /user:code-style
两个层级:
- 项目级
.claude/commands/:跟着项目走,团队共享,用/命令名调用 - 用户级
~/.claude/commands/:跟着个人走,所有项目可用,用/命令名调用
一个实际例子
假设你经常需要让 Claude 帮你做 Code Review,每次都要重复说一遍"关注安全性、性能、可读性..."。把它固化成 Command:
<!-- .claude/commands/review.md -->
请对以下代码进行 Code Review,重点关注:
1. 安全性:是否存在注入、XSS、敏感信息泄露等问题
2. 性能:是否有明显的 N+1 查询、不必要的循环、内存泄漏风险
3. 可读性:命名是否清晰、逻辑是否易于理解
4. 边界条件:是否处理了空值、异常输入、并发场景
目标文件:$ARGUMENTS
$ARGUMENTS 是 Claude Code 提供的变量,会被替换为你在斜杠命令后面输入的内容。比如 /project:review src/auth/login.ts,Claude 就会对 login.ts 做一次结构化的 Code Review。
Command 的局限
Command 够用,但也就"够用"。它有几个明显的短板:
- 只能手动触发,Claude 不会自己判断"现在该用这个命令了"
- 没有元数据(frontmatter),无法声明依赖的工具、适用场景
- 单文件结构,无法附带示例文件或模板
- 不支持编号参数(
$1、$2),只有一个$ARGUMENTS
这些局限催生了它的继任者 -- Skill。
Skill:我愿称为最强扩展
如果说 Command 是"一个 Markdown 文件当 prompt 模板",那 Skill 就是"一个目录当能力包"。它多了三个关键能力:
- YAML Frontmatter:声明名称、描述、触发方式、允许的工具列表
- 目录结构:可以附带示例文件、模板、参考资料
- 自动触发:Claude 可以根据对话上下文自动判断是否加载某个 Skill
目录结构
项目根目录/
└── .claude/
└── skills/
└── create-api-endpoint/
├── SKILL.md ← 必须大写
├── examples/
│ └── user-endpoint.md
└── template.ts
~/.claude/
└── skills/
└── code-review/
├── SKILL.md
└── examples/
└── review-samples.md
注意:文件名必须是 SKILL.md(大写),小写的 skill.md 会被静默忽略 -- 这是一个常见的坑。
SKILL.md 的写法
---
name: create-api-endpoint
description: 根据数据模型生成 RESTful API 端点代码
invocable: manual
allowed-tools:
- Read
- Write
- Bash(npm:*)
---
# 创建 API 端点
根据提供的数据模型定义,生成完整的 RESTful API 端点,包括:
1. Controller 层:路由定义 + 参数校验
2. Service 层:业务逻辑
3. 数据访问层:数据库查询
4. 测试文件:单元测试 + 集成测试
## 参数
- 模型名称:$ARGUMENTS
- 参考 examples/ 目录下的示例了解输出格式
## 约束
- 遵循项目现有的目录结构和命名规范
- 错误处理使用项目统一的 AppError 类
- 不要生成 migration 文件,只生成代码
手动触发 vs 自动触发
默认情况下,用户和 Claude 都可以触发 Skill。你可以用 /skill-name 手动调用,Claude 也会根据对话上下文自动判断是否加载。两个 frontmatter 字段可以控制这个行为:
| 字段 | 效果 | 适用场景 |
|---|---|---|
disable-model-invocation: true | 只有用户能手动触发,Claude 不会自动加载 | 有副作用的操作,如部署、发消息 |
user-invocable: false | 只有 Claude 能自动加载,用户在 / 菜单中看不到 | 背景知识,如遗留系统上下文 |
| (默认) | 用户和 Claude 都能触发 | 通用能力,如代码解释、Review |
一个实际的例子:部署操作你肯定不希望 Claude 自己判断"代码看起来准备好了"就自动触发,所以要加 disable-model-invocation: true:
---
name: deploy
description: 部署应用到生产环境
disable-model-invocation: true
---
部署 $ARGUMENTS 到生产环境:
1. 运行测试套件
2. 构建应用
3. 推送到部署目标
4. 验证部署是否成功
值得注意的是,默认情况下 Skill 的描述信息会被加载到上下文中(让 Claude 知道有哪些能力可用),但完整的 Skill 内容只在被触发时才加载。设置了 disable-model-invocation: true 的 Skill,描述信息也不会出现在上下文中,完全零开销。
内联命令注入
Skill 还支持一个很实用的特性:用 ! 前缀嵌入 shell 命令的输出。
---
name: review-pr
description: 审查当前 PR 的代码变更
---
请审查以下 PR 变更:
- PR diff: !`gh pr diff`
- 变更文件列表: !`gh pr diff --name-only`
重点关注安全性和性能问题。
这些内联命令会在 Skill 加载时执行,结果直接插入到 prompt 中。这意味着你可以让 Skill 动态获取当前上下文,而不是写死静态内容。
Hook:不靠 LLM 自觉,靠 exit code 说话
这是四种机制里最容易被低估的一个。
很多团队会在 CLAUDE.md 里写规则:"不要修改 .env 文件"、"提交前必须跑 lint"。这些规则大多数时候有效,但它们本质上是"建议" -- Claude 作为 LLM,偶尔会忘、会误判、会在复杂上下文中跳过。
Hook 解决的就是这个问题:把"建议"变成"强制"。
Hook 是用户定义的事件处理器,在 Claude Code 工作流的关键节点执行 shell 命令。它不经过 LLM,直接由系统调度,用 exit code 决定是放行还是阻断。
Hook 的事件类型
Claude Code 支持 17 种 Hook 事件,最常用的是这几个:
| 事件 | 触发时机 | 能否阻断操作 |
|---|---|---|
PreToolUse | 工具执行之前 | 能(exit 2 阻断) |
PostToolUse | 工具执行成功之后 | 不能(但可反馈信息给 Claude) |
Stop | Claude 完成回复时 | 能(让 Claude 继续工作) |
UserPromptSubmit | 用户提交 prompt 时 | 能(可预处理或拦截) |
SessionStart | 会话开始或恢复时 | 不能 |
配置方式
Hook 配置在 JSON 设置文件中,支持三个层级:
.claude/settings.json ← 项目级,可提交到 Git,团队共享
.claude/settings.local.json ← 项目级,gitignore,个人偏好
~/.claude/settings.json ← 全局级,所有项目生效
基本结构:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/validate-bash.sh",
"timeout": 10
}
]
}
]
}
}
几个关键字段:
- matcher:正则表达式,匹配触发 Hook 的工具名。
"Bash"只匹配 Bash 工具,"Edit|Write"匹配编辑和写入,"mcp__.*"匹配所有 MCP 工具 - type:
"command"(shell 命令)、"prompt"(LLM 判断)、"agent"(多轮 Agent 验证) - timeout:超时秒数,默认 600 秒
exit code 的语义
这是 Hook 最核心的设计,也是最容易踩坑的地方:
| Exit Code | 含义 | 效果 |
|---|---|---|
0 | 成功 | 放行,继续执行 |
2 | 阻断错误 | 阻止工具执行,错误信息反馈给 Claude |
| 其他非零值 | 非阻断警告 | 记录日志但不阻止执行 |
常见错误:很多人习惯用 exit 1 表示失败,但在 Hook 里 exit 1 只是警告,不会阻断操作。必须用 exit 2 才能真正拦住。
实战:保护敏感文件
#!/bin/bash
# .claude/hooks/protect-files.sh
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
PROTECTED=(".env" ".env.local" "package-lock.json" ".git/")
for pattern in "${PROTECTED[@]}"; do
if [[ "$FILE_PATH" == *"$pattern"* ]]; then
echo "阻断:'$FILE_PATH' 是受保护文件" >&2
exit 2
fi
done
exit 0
实战:写入后自动格式化
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write 2>/dev/null || true",
"statusMessage": "Formatting..."
}
]
}
]
}
}
这个 Hook 在每次文件编辑或写入后自动跑 Prettier 格式化。因为是 PostToolUse,不需要阻断,所以用 || true 确保即使格式化失败也不影响流程。
实战:拦截危险命令
#!/bin/bash
# .claude/hooks/block-dangerous.sh
INPUT=$(cat)
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
DANGEROUS=('rm -rf' 'git push --force' 'DROP TABLE' 'git reset --hard')
for pattern in "${DANGEROUS[@]}"; do
if echo "$COMMAND" | grep -qi "$pattern"; then
echo "阻断:检测到危险命令 '$pattern'" >&2
exit 2
fi
done
exit 0
Hook 的输入输出
Hook 脚本通过 stdin 接收 JSON 格式的上下文信息:
{
"tool": "Write",
"tool_input": {
"file_path": "src/config.ts",
"content": "..."
},
"session_id": "abc123",
"cwd": "/home/user/project"
}
PreToolUse Hook 还可以返回 JSON 来修改工具的输入参数:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"updatedInput": { "command": "rm -ri ./tmp" },
"additionalContext": "已将 rm -rf 替换为 rm -ri(交互式删除)"
}
}
这意味着 Hook 不只能"拦",还能"改" -- 比如把 rm -rf 自动替换成更安全的 rm -ri。
Subagent:独立上下文窗口里的专项选手
前面三种机制 -- Command、Skill、Hook -- 都是在主会话里工作的。主会话的上下文窗口是有限的,当你让 Claude 一边研究代码库、一边写实现、一边做 Review,上下文很快就会被撑满,输出质量也会下降。
Subagent 的设计思路是:把复杂任务拆出去,让专门的子智能体在独立的上下文窗口里完成,结果再汇总回来。
这和微服务的思路很像 -- 主会话是网关,Subagent 是后端服务,各自有独立的资源和职责边界。
Subagent 的定义方式
Subagent 以 Markdown 文件的形式定义在 .claude/agents/ 目录下:
项目根目录/
└── .claude/
└── agents/
├── code-reviewer.md
├── test-writer.md
└── security-auditor.md
~/.claude/
└── agents/
├── researcher.md
└── doc-writer.md
同样分项目级和用户级,项目级优先级更高。
一个 Subagent 的定义示例
---
name: code-reviewer
description: 专注于代码质量审查的子智能体
model: claude-sonnet-4-6
tools:
- Read
- Grep
- Glob
---
# Code Review 专家
你是一个专注于代码审查的专家。请对提供的代码变更进行审查,重点关注:
1. **安全性**:注入攻击、敏感信息泄露、权限校验
2. **性能**:N+1 查询、不必要的内存分配、阻塞操作
3. **可维护性**:命名规范、函数职责单一、错误处理完整性
## 输出格式
按严重程度分类:
- CRITICAL:必须修复
- WARNING:建议修复
- INFO:可以改进
每条意见包含:文件路径、行号、问题描述、修复建议。
YAML frontmatter 里的关键字段:
| 字段 | 类型 | 作用 |
|---|---|---|
name | string | 唯一标识符 |
description | string | 描述能力和适用场景 |
model | string | 使用的 Claude 模型 |
tools | array | 允许使用的工具列表 |
工具列表的最小权限原则:只给 Subagent 它需要的工具。一个只做代码审查的 Subagent 不需要 Write 和 Bash 权限,给 Read、Grep、Glob 就够了。这既是安全考虑,也能减少 Subagent 的"发挥空间",让它更专注。
Subagent 的工作流程
sequenceDiagram
participant U as 开发者
participant M as Claude 主会话
participant S as Subagent
U->>M: "帮我审查这个 PR 的代码"
M->>M: 判断任务适合委派
M->>S: 创建 code-reviewer 实例<br/>传入任务描述和上下文
Note over S: 独立上下文窗口<br/>只有 Read/Grep/Glob 工具
S->>S: 阅读代码、分析问题
S->>M: 返回审查报告
M->>U: 整理并展示结果
关键点:Subagent 完成任务后,只有最终结果会返回给主会话。它中间的思考过程、工具调用记录都不会占用主会话的上下文窗口。这就是"上下文隔离"的价值。
什么时候该用 Subagent
不是所有任务都需要 Subagent。一个简单的判断标准:
适合用 Subagent 的场景:
- 需要大量阅读代码才能完成的任务(如全仓库安全审计)
- 需要独立判断、不希望被主会话上下文影响的任务(如客观的 Code Review)
- 可以并行执行的多个独立子任务
- 超过 5 个决策点的复杂工作流
不需要 Subagent 的场景:
- 简单的文件编辑或代码生成
- 需要频繁和用户交互确认的任务
- 上下文已经很清晰、不需要额外探索的任务
四种机制怎么组合使用
单独看每种机制都不复杂,真正体现工程功力的是组合。
来看一个贴近现实的场景:团队希望 Claude Code 能帮忙做 PR Review,但要满足几个要求:
- 一键触发,不用每次重复写 prompt
- 审查过程不能修改代码,只能读
- 不允许 Claude 访问
.env和密钥文件 - 审查结果要自动格式化
用四种机制组合起来:
graph LR
A["/project:review-pr"] -->|"Skill 注入 prompt"| B["Claude 主会话"]
B -->|"委派审查任务"| C["code-reviewer<br/>Subagent"]
C -->|"每次读文件前"| D["PreToolUse Hook<br/>拦截敏感文件"]
C -->|"返回审查报告"| B
B -->|"写入报告文件"| E["PostToolUse Hook<br/>自动格式化"]
E --> F["review-report.md"]
classDef skill fill:#1A9090,stroke:#147070,color:#fff
classDef core fill:#0d4f4f,stroke:#1A9090,color:#fff
classDef hook fill:#1a3a3a,stroke:#1A9090,color:#ccc
classDef output fill:#0a2e2e,stroke:#1A9090,color:#aaa
class A skill
class B,C core
class D,E hook
class F output
对应的文件结构:
.claude/
├── skills/
│ └── review-pr/
│ └── SKILL.md ← 定义审查流程和输出格式
├── agents/
│ └── code-reviewer.md ← 只有 Read/Grep/Glob 权限的审查专家
├── hooks/
│ ├── protect-secrets.sh ← 拦截对 .env/.key/.pem 的访问
│ └── format-output.sh ← 写入后自动格式化
└── settings.json ← Hook 配置
配置文件把它们串起来:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Read",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/protect-secrets.sh",
"timeout": 5
}
]
}
],
"PostToolUse": [
{
"matcher": "Write",
"hooks": [
{
"type": "command",
"command": ".claude/hooks/format-output.sh",
"timeout": 15
}
]
}
]
}
}
这套组合的好处是:每个机制只做自己擅长的事。Skill 管流程编排,Subagent 管专项执行,Hook 管安全底线和自动化后处理。职责清晰,互不干扰。
落地建议:从简单开始,按需升级
如果你刚开始接触这些扩展机制,不建议一上来就搞全套。一个比较稳的渐进路径大概长这样:
第一步:把重复的 prompt 变成 Command
找出你每天都在重复输入的 prompt,写成 .claude/commands/ 下的 Markdown 文件。这一步零成本、立竿见影。
第二步:把 Command 升级为 Skill
当你发现某些 Command 需要附带示例文件、需要限制工具权限、或者希望 Claude 能自动识别使用场景时,把它升级为 Skill。
第三步:用 Hook 守住安全底线
梳理团队的"绝对不能做"清单 -- 不能改的文件、不能跑的命令、必须执行的格式化 -- 写成 Hook 脚本,放进 .claude/settings.json。
第四步:用 Subagent 处理重型任务
当你发现某些任务总是把主会话的上下文撑满,或者需要不受主会话偏见影响的独立判断时,把它拆成 Subagent。
graph LR
A["Command<br/>固化 prompt"] -->|"需要更多能力"| B["Skill<br/>领域知识包"]
B -->|"需要确定性保障"| C["Hook<br/>安全护栏"]
C -->|"需要隔离上下文"| D["Subagent<br/>专项智能体"]
classDef step1 fill:#1A9090,stroke:#147070,color:#fff
classDef step2 fill:#157878,stroke:#1A9090,color:#fff
classDef step3 fill:#0d4f4f,stroke:#1A9090,color:#fff
classDef step4 fill:#1a3a3a,stroke:#1A9090,color:#ccc
class A step1
class B step2
class C step3
class D step4
最后再强调一点:所有这些配置都是文件驱动的。.claude/ 目录下的 settings.json、skills/、agents/、hooks/ 都可以提交到 Git。这意味着团队里一个人配好了,所有人都能用。这才是 Claude Code 从"个人工具"变成"团队基础设施"的关键一步。