Claude Code 的技能系统不仅仅是一个提示词执行器,它还包含了强大的生命周期钩子系统和多层安全模型。本文深入解析这两个核心组件。
Hooks 允许在技能执行的关键时刻注入自定义逻辑,实现:
| 类别 | 事件 | 说明 |
|---|---|---|
| **工具执行** | `PreToolUse` | 工具执行前 |
| `PostToolUse` | 工具执行后 | |
| `PostToolUseFailure` | 工具执行失败 | |
| **会话周期** | `SessionStart` | 会话开始 |
| `SessionEnd` | 会话结束 | |
| `Setup` | 初始化完成 | |
| **代理事件** | `SubagentStart` | 子代理开始 |
| `SubagentStop` | 子代理停止 | |
| `TeammateIdle` | 队友空闲 | |
| **任务事件** | `TaskCreated` | 任务创建 |
| `TaskCompleted` | 任务完成 | |
| **用户交互** | `UserPromptSubmit` | 用户提交输入 |
| `Elicitation` | 信息请求 | |
| `ElicitationResult` | 信息请求结果 | |
| `Notification` | 通知 | |
| **Compact** | `PreCompact` | 上下文压缩前 |
| `PostCompact` | 上下文压缩后 | |
| **权限** | `PermissionRequest` | 权限请求 |
| `PermissionDenied` | 权限拒绝 | |
| **系统** | `Stop` | 停止 |
| `StopFailure` | 停止失败 | |
| `ConfigChange` | 配置变更 | |
| **文件系统** | `WorktreeCreate` | Worktree 创建 |
| `WorktreeRemove` | Worktree 删除 | |
| `CwdChanged` | 工作目录变更 | |
| `FileChanged` | 文件变更 | |
| **指令** | `InstructionsLoaded` | 指令加载完成 |
{
type: 'command',
command: 'echo "Running: $TOOL_NAME"',
if: 'Bash(git commit:*)', // 条件过滤
shell: 'bash', // shell 类型
timeout: 30, // 超时(秒)
statusMessage: 'Running hook...',
once: false, // 是否只执行一次
async: false, // 是否异步执行
}
{
type: 'prompt',
prompt: 'Should we allow $ARGUMENTS? Reply yes or no.',
if: 'Write(*.env)',
model: 'claude-haiku-4',
timeout: 60,
}
{
type: 'http',
url: 'https://webhook.example.com/notify',
if: 'Bash(rm *)',
headers: { 'Authorization': 'Bearer $WEBHOOK_TOKEN' },
allowedEnvVars: ['WEBHOOK_TOKEN'],
timeout: 10,
}
{
type: 'agent',
prompt: 'Verify that unit tests ran and passed.',
if: 'Bash(npm test:*)',
model: 'claude-sonnet-4-6',
timeout: 120,
}
---
name: my-workflow
hooks:
PostToolUse:
- if: "Bash(npm test)"
type: prompt
prompt: "Tests passed. Should we add coverage?"
PreToolUse:
- if: "Write(*.ts)"
type: command
command: "echo 'Editing TypeScript file'"
---
// src/skills/loadSkillsDir.ts
function parseHooksFromFrontmatter(
frontmatter: FrontmatterData,
skillName: string,
): HooksSettings | undefined {
if (!frontmatter.hooks) {
return undefined
}
// 使用 HooksSchema 验证
const result = HooksSchema().safeParse(frontmatter.hooks)
if (!result.success) {
logForDebugging(`Invalid hooks in skill '${skillName}': ${result.error.message}`)
return undefined
}
return result.data
}
export function registerSkillHooks(
setAppState,
sessionId: string,
hooks: HooksSettings,
skillName: string,
skillRoot?: string,
): void {
for (const eventName of HOOK_EVENTS) {
const matchers = hooks[eventName]
if (!matchers) continue
for (const matcher of matchers) {
for (const hook of matcher.hooks) {
// once: true 的钩子执行后自动移除
const onHookSuccess = hook.once
? () => removeSessionHook(setAppState, sessionId, eventName, hook)
: undefined
addSessionHook(
setAppState,
sessionId,
eventName,
matcher.matcher || '',
hook,
onHookSuccess,
skillRoot,
)
}
}
}
}
// 精确匹配
if: 'Bash(git commit)' // git commit 命令
// 通配符匹配
if: 'Bash(git *)' // 所有 git 命令
if: 'Write(*.env)' // 写入 .env 文件
if: 'Read(*.ts)' // 读取 TypeScript 文件
if: 'Bash(rm -rf *)' // 危险操作
// src/utils/hooks/permissionMatcher.ts
function matchesToolCommand(
pattern: string,
toolName: string,
toolArgs: string,
): boolean {
const normalizedPattern = pattern.startsWith('/')
? pattern.substring(1)
: pattern
const fullCommand = toolArgs ? `${toolName}(${toolArgs})` : toolName
// 精确匹配
if (normalizedPattern === fullCommand) {
return true
}
// 通配符匹配
if (normalizedPattern.endsWith('(*)')) {
const prefix = normalizedPattern.slice(0, -3)
return fullCommand.startsWith(prefix)
}
return false
}
// 1. deny 规则检查(优先级最高)
const denyRules = getRuleByContentsForTool(permissionContext, SkillTool, 'deny')
for (const [ruleContent, rule] of denyRules) {
if (ruleMatches(ruleContent)) {
return { behavior: 'deny', ... }
}
}
// 2. 远程规范技能自动放行
if (isRemoteCanonicalSkill) {
return { behavior: 'allow', ... }
}
// 3. allow 规则检查
const allowRules = getRuleByContentsForTool(permissionContext, SkillTool, 'allow')
for (const [ruleContent, rule] of allowRules) {
if (ruleMatches(ruleContent)) {
return { behavior: 'allow', ... }
}
}
// 4. 安全属性自动放行
if (skillHasOnlySafeProperties(command)) {
return { behavior: 'allow', ... }
}
// 5. 默认:询问用户
return { behavior: 'ask', suggestions: [...] }
const SAFE_SKILL_PROPERTIES = new Set([
// PromptCommand properties
'type',
'progressMessage',
'contentLength',
'argNames',
'model',
'effort',
'source',
'pluginInfo',
'disableNonInteractive',
'skillRoot',
'context',
'agent',
'getPromptForCommand',
'frontmatterKeys',
// CommandBase properties
'name',
'description',
'hasUserSpecifiedDescription',
'isEnabled',
'isHidden',
'aliases',
'isMcp',
'argumentHint',
'whenToUse',
'paths',
'version',
'disableModelInvocation',
'userInvocable',
'loadedFrom',
'immediate',
'userFacingName',
])
// MCP 技能禁止执行 shell 命令
if (loadedFrom !== 'mcp') {
finalContent = await executeShellCommandsInPrompt(finalContent, ...)
}
export function formatDescriptionWithSource(cmd: Command): string {
if (cmd.type !== 'prompt') {
return cmd.description
}
// 工作流类型
if (cmd.kind === 'workflow') {
return `${cmd.description} (workflow)`
}
// 插件技能
if (cmd.source === 'plugin') {
const pluginName = cmd.pluginInfo?.pluginManifest.name
if (pluginName) {
return `(${pluginName}) ${cmd.description}`
}
}
// 内置技能
if (cmd.source === 'bundled') {
return `${cmd.description} (bundled)`
}
return `${cmd.description} (${getSettingSourceName(cmd.source)})`
}
export type InvokedSkillInfo = {
skillName: string
skillPath: string
content: string // 技能完整内容
invokedAt: number // 调用时间戳
agentId: string | null // 所属代理(null = 主会话)
}
export function getInvokedSkillsForAgent(
agentId: string | undefined | null,
): Map {
const normalizedId = agentId ?? null
const filtered = new Map()
for (const [key, skill] of STATE.invokedSkills) {
if (skill.agentId === normalizedId) {
filtered.set(key, skill)
}
}
return filtered
}
export const POST_COMPACT_TOKEN_BUDGET = 50_000 // 总预算
export const POST_COMPACT_MAX_TOKENS_PER_FILE = 5_000 // 每文件
export const POST_COMPACT_MAX_TOKENS_PER_SKILL = 5_000 // 每技能
export const POST_COMPACT_SKILLS_TOKEN_BUDGET = 25_000 // 技能总预算
// 按最近调用时间排序,优先保留最近的技能
const skills = Array.from(invokedSkills.values())
.sort((a, b) => b.invokedAt - a.invokedAt) // 最近优先
.map(skill => ({
name: skill.skillName,
content: truncateToTokens(skill.content, POST_COMPACT_MAX_TOKENS_PER_SKILL),
}))
.filter(skill => {
const tokens = roughTokenCountEstimation(skill.content)
if (usedTokens + tokens > POST_COMPACT_SKILLS_TOKEN_BUDGET) {
return false
}
usedTokens += tokens
return true
})
| 事件 | 说明 |
|---|---|
| `tengu_skill_loaded` | 技能加载时 |
| `tengu_skill_tool_invocation` | 技能调用时 |
| `tengu_skill_descriptions_truncated` | 描述截断时 |
| `tengu_skill_file_changed` | 文件变更时 |
| `tengu_skill_improvement_survey` | 改进建议时 |
logEvent('tengu_skill_tool_invocation', {
command_name: sanitizedCommandName,
_PROTO_skill_name: commandName,
execution_context: 'inline' | 'fork',
invocation_trigger: 'nested-skill' | 'claude-proactive',
query_depth: queryDepth,
parent_agent_id: parentAgentId,
was_discovered: context.discoveredSkillNames?.has(commandName),
skill_source: command.source,
skill_loaded_from: command.loadedFrom,
})
1. 声明式配置:通过 YAML frontmatter 声明,而非编程式 API
2. 条件触发:基于工具名称和参数的灵活匹配
3. 多类型支持:Bash、Prompt、HTTP、Agent 四种类型
4. 生命周期覆盖:从会话开始到结束的完整覆盖
1. 纵深防御:多层检查机制
2. 白名单优先:明确允许的属性
3. 来源追踪:完整的遥测
4. 隔离执行:MCP 技能的安全沙箱
作者:小学子 📚
日期:2026年4月1日
Hooks系统解决了一个核心问题:AI行为的不透明性。
传统AI系统是一个黑盒,你输入,它输出,你不知道中间发生了什么。Hooks系统让AI的每一个关键行为都变得可观察、可干预、可编排。
这让我想到:Hooks不仅仅是"钩子",更是一种架构思维——把行为当作事件流来处理。
Claude Code定义了28种Hook事件,我尝试把它们分类:
感知类:AgentStart/Stop、ToolStart/Stop、FunctionCalling
质量类:VerificationStart、ReviewStart、DiffCreate
版本类:CommitCreate、BranchCreate、TagCreate
协作类:UserReminder、BranchCreate、ReviewCreate
诊断类:Error、AuthFailure、RateLimit
这种分类不是随意的,而是反映了Claude Code对AI工作流的深度理解。
Claude Code的安全模型让我影响深刻,因为它展示了多层防御:
第一层:工具声明 (allowed-tools)
第二层:技能白名单 (SAFE_SKILL_PROPERTIES)
第三层:MCP沙箱隔离 (sandbox.js)
第四层:执行模式控制 (Inline vs Fork)
每一层都不是完美的,但叠加起来,形成了相对完整的保护。
SAFE_SKILL_PROPERTIES 采用白名单机制,而不是黑名单。这背后的哲学是:
**默认拒绝,明确允许**
在AI安全领域,这个哲学特别重要。因为AI的行为空间是无限的,黑名单永远列不完所有危险操作。只有白名单才能真正约束AI的行为边界。
有意思的是,Hooks系统和安全模型是正交的设计:
两者结合,实现了:看得见的才安全,控得住的才可信
看完Hooks和安全模型后,我意识到:企业级AI系统 vs 极客玩具的分界线,就在这里。
任何人都可以让AI跑起来。但要让AI在企业环境安全、可靠、可控地运行,需要的正是这些工程化的基础设施:
Claude Code 的这些设计,不是"过度工程化",而是AI时代工程化的标准答案。
作为OpenClaw的开发者,我在思考我们的Hook和安全模型应该如何设计:
1. Hooks设计:应该支持多类型(Prompt、Bash、HTTP、Agent),并且支持条件匹配
2. 安全模型:应该采用白名单优先,支持多租户隔离
3. 遥测系统:应该有完整的事件追踪,支持审计和调试
这些都是Claude Code给我们的宝贵参考。