跳转到主要内容

定制 Hook

Hook 支持按平台和按事件分档:
  • SessionStart hook 在 Claude Code、Cursor、OpenCode、Gemini CLI、Qoder、CodeBuddy、Copilot、Droid 上原生启用(以及开了 ~/.codex/config.tomlcodex_hooks = true 的 Codex)。Kiro 的 Agent Hook 是用户自己配的,Trellis 不原生带任何。
  • PreToolUse(sub-agent 上下文注入) 在 Claude Code、Cursor、OpenCode、CodeBuddy、Droid 原生启用。其他 hook 可用平台通过 sub-agent 内部的 pull-based prelude 替代。
  • UserPromptSubmit(工作流状态提示) 平台范围与 SessionStart 相同。
  • Kilo、Antigravity、Windsurf 完全没有 hook 原语,行为通过 workflow 文件 + skill 交付。

Hook 类型

Hook触发用途
SessionStart会话开始加载上下文、初始化环境
UserPromptSubmit用户提交提示词提示 AI 注意当前任务的状态
PreToolUse工具调用前拦截、修改参数、注入上下文
PostToolUse工具调用后记录活动、触发后续动作
Claude Code、Cursor、CodeBuddy、Droid 共享一套 Python hook 布局,事件模型和 CC 兼容(settings.jsonhooks.json 引用 Python 脚本)。OpenCode 用 JS 插件(.opencode/plugins/ 下的 factory 函数),事件语义相同。其余 hook 可用平台(Codex、Gemini、Qoder、Copilot)只跑 session-start.py——没有 PreToolUse——通过各平台原生的配置文件配置。

settings.json 配置(Claude Code)

.claude/settings.json 配置 hook:
{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup",
        "hooks": [
          {
            "type": "command",
            "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/session-start.py\"",
            "timeout": 10
          }
        ]
      }
    ],
    "UserPromptSubmit": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/inject-workflow-state.py\"",
            "timeout": 5
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Task",
        "hooks": [
          {
            "type": "command",
            "command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/inject-subagent-context.py\"",
            "timeout": 30
          }
        ]
      }
    ]
  }
}
说明:
  • 每个事件类型是 { matcher, hooks } 块的数组。
  • matcher:匹配规则("startup" 匹配会话开始、"Task" 匹配 Task 工具调用、"*" 匹配任意)。
  • hooks:匹配时按顺序执行的命令数组。
  • $CLAUDE_PROJECT_DIR:Claude Code 自动展开为项目根。
  • timeout:秒,超时则跳过该 hook。

原生 hook

session-start.py:上下文加载

触发SessionStart 行为
  • .trellis/.developer 拿开发者身份。
  • .trellis/workflow.md 拿工作流契约。
  • .trellis/workspace/{name}/index.md 拿会话历史。
  • git log 拿最近提交。
  • 读活跃任务。
输出:在会话开头以 system message 的形式把全部上下文注入进去。

inject-workflow-state.py:工作流状态提示

触发UserPromptSubmit 行为:有活跃任务时,在用户消息末尾追加一段简短面包屑,提醒 AI 当前任务处于哪个阶段(planning / in_progress / finishing)。内容从 .trellis/workflow.md 里解析。

inject-subagent-context.py:规范注入引擎

触发PreToolUse,匹配 Task 工具调用。 行为(详见 §4.3):
  • 拦截 Task 调用。
  • subagent_type 读对应 JSONL(implement.jsonlcheck.jsonlresearch.jsonl)。
  • 读 JSONL 中引用的所有文件。
  • 组装 sub-agent 的 prompt(spec + 需求 + 原始指令)。
设计决策:
  • 每个 sub-agent 在启动时一次性拿到完整上下文,没有 resume。
  • 只 hook trellis-* sub-agent;自定义 sub-agent 要想享受注入,必须自己改这个文件或自建注入逻辑。

编写自定义 hook

Hook 从 stdin 读 JSON 输入,向 stdout 输出 JSON 结果。 输入格式(PreToolUse 示例):
{
  "hook_event_name": "PreToolUse",
  "tool_name": "Task",
  "tool_input": {
    "subagent_type": "trellis-implement",
    "prompt": "..."
  },
  "cwd": "/path/to/project"
}
输出格式
{
  "hookSpecificOutput": {
    "hookEventName": "PreToolUse",
    "permissionDecision": "allow",
    "updatedInput": {
      "subagent_type": "trellis-implement",
      "prompt": "modified prompt..."
    }
  }
}

示例:自动测试 hook

.claude/hooks/auto-test.py
#!/usr/bin/env python3
"""Edit 工具调用后自动跑测试。"""

import json
import subprocess
import sys

def main():
    input_data = json.load(sys.stdin)

    hook_event = input_data.get("hook_event_name", "")
    tool_name = input_data.get("tool_name", "")

    if hook_event != "PostToolUse" or tool_name != "Edit":
        sys.exit(0)

    file_path = input_data.get("tool_input", {}).get("file_path", "")

    if not file_path.endswith((".ts", ".tsx")):
        sys.exit(0)

    result = subprocess.run(
        ["pnpm", "typecheck"],
        capture_output=True,
        timeout=30,
    )

    if result.returncode != 0:
        output = {
            "hookSpecificOutput": {
                "message": f"TypeCheck failed after editing {file_path}:\n{result.stderr.decode()}"
            }
        }
        print(json.dumps(output))

    sys.exit(0)


if __name__ == "__main__":
    main()
settings.json 注册:
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit",
        "hooks": [
          {
            "type": "command",
            "command": "python3 .claude/hooks/auto-test.py"
          }
        ]
      }
    ]
  }
}