Claude Code 的四把钥匙:Skill、Command、Hook、Subagent 全解析

0 阅读10分钟

很多团队用 Claude Code 的方式,还停留在"打开终端、输入问题、等回答"的阶段。

这没什么问题,Claude Code 本身就是一个很好的对话式编程助手。但用得越多越会发现,真正拉开效率差距的,不是谁的 prompt 写得更好,而是谁把自己的工作流"固化"进了 Claude Code 的扩展体系里。

换句话说,Claude Code 提供了四种扩展机制 -- Skill、Command、Hook、Subagent -- 它们分别解决不同层面的问题。

这篇文章适合已经在用 Claude Code、想把它从"好用的工具"升级为"团队基础设施"的工程师和技术管理者。

太长不看版

  1. Command:一个 Markdown 文件就是一个斜杠命令,零成本固化常用 prompt,是最轻量的扩展方式
  2. Skill:支持 YAML frontmatter、目录结构、自动触发,是 Command 的全面升级,也是官方推荐的扩展方式
  3. Hook:确定性的安全护栏,通过 exit code 控制是否阻断工具执行,不存在"LLM 偶尔忘了"的问题
  4. Subagent:运行在独立上下文窗口中的专项智能体,不会污染主会话,适合处理重型任务
  5. 组合使用:四种机制不是互斥的 -- 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 脚本(确定性)安全护栏 + 自动化后处理
SubagentClaude 主动委派独立 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 就是"一个目录当能力包"。它多了三个关键能力:

  1. YAML Frontmatter:声明名称、描述、触发方式、允许的工具列表
  2. 目录结构:可以附带示例文件、模板、参考资料
  3. 自动触发: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)
StopClaude 完成回复时能(让 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 里的关键字段:

字段类型作用
namestring唯一标识符
descriptionstring描述能力和适用场景
modelstring使用的 Claude 模型
toolsarray允许使用的工具列表

工具列表的最小权限原则:只给 Subagent 它需要的工具。一个只做代码审查的 Subagent 不需要 WriteBash 权限,给 ReadGrepGlob 就够了。这既是安全考虑,也能减少 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,但要满足几个要求:

  1. 一键触发,不用每次重复写 prompt
  2. 审查过程不能修改代码,只能读
  3. 不允许 Claude 访问 .env 和密钥文件
  4. 审查结果要自动格式化

用四种机制组合起来:

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.jsonskills/agents/hooks/ 都可以提交到 Git。这意味着团队里一个人配好了,所有人都能用。这才是 Claude Code 从"个人工具"变成"团队基础设施"的关键一步。