[cc] Hook

处处体现 Harness 的思想~ 

 

Hook


 

Hook = 在 Agent 生命周期的关键事件上,插入一段“必经的自动控制逻辑”。

Claude Code 官方现在的 Hook 事件很多,包括 PreToolUsePostToolUseStopSessionStartUserPromptSubmitTaskCompleted 等;Hook 可以执行 shell command、HTTP endpoint,甚至用 prompt/agent 做检查。

我给你几个真正能体现价值的例子。

ID场景Hook 触发点Hook 做什么为什么有价值
1 禁止危险命令 PreToolUse 检查 Bash,发现 rm -rf 就阻止 安全边界不能靠 LLM 自觉
2 修改代码后自动格式化 PostToolUse 自动运行 formatter / lint 每次修改都保证代码质量
3 Agent 想结束任务 Stop 检查测试是否全部通过 防止“嘴上说完成了”
4 每次工具调用

PreToolUse

PostToolUse

PostToolUseFailure

写审计日志到服务器 企业追踪、合规
5 Session 启动 SessionStart 加载当前环境/分支/服务状态 自动准备运行环境
6 Prompt 提交 UserPromptSubmit 注入当前 ticket / issue / tenant 信息 自动补 Context

 

 

==》例子 1:阻止危险命令 —— Hook 作为“安全门”

假设 Claude 决定:

rm -rf ./data
 

如果只在 CLAUDE.md 写:

不要删除重要数据。(软约束,没用)
 

但用 PreToolUse Hook:

Claude 准备执行 Bash
        ↓
PreToolUse Hook
        ↓
检查 command
        ↓
发现 rm -rf
        ↓
DENY
        ↓
命令根本没有机会执行
 
 

Implementation

以下是实现的一个例子说明。

my-project/
├── .claude/
│ ├── settings.json --> 其中会调用block-dangerous-rm.sh
│ └── hooks/
│ └── block-dangerous-rm.sh
└── ...

 

settings.json says: 只要 Claude 准备调用 Bash --> 在 Bash 真正执行之前 --> 先运行 block-dangerous-rm.sh
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",  # 若 发现是 Bash,则触发 PreToolUse。
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-dangerous-rm.sh"  --> 调用的地方
          }
        ]
      }
    ]
  }
}

 

#!/usr/bin/env bash

# Claude Code 会把本次 Tool Call 的信息
# 通过 stdin 传给这个脚本
INPUT=$(cat)

# 取出 Claude 想执行的 Bash command
COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')

# 如果发现危险的 rm -rf
if echo "$COMMAND" | grep -Eq '(^|[;&|[:space:]])rm[[:space:]]+-rf([[:space:]]|$)'; then
    echo "BLOCKED: rm -rf is prohibited in this project." >&2
    exit 2
fi

# 没有发现危险命令
exit 0
block-dangerous-rm.sh

 

 

底层原理剖析

大模型返回给cc:
{
  "role": "assistant",
  "content": [
    {
      "type": "tool_use",
      "id": "toolu_01ABC123",
      "name": "Bash",
      "input": {
        "command": "rm -rf ./build"
      }
    }
  ],
  "stop_reason": "tool_use"
}

 

cc 心想:

这是一个 Bash Tool Call --> 检查 PreToolUse Hook 配置。
是个Bash --> 还是个 PreToolUse --> 最终匹配到了两个脚本。
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/block-dangerous-rm.sh"
          }
        ]
      },
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/block-sudo.sh"
          }
        ]
      },
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": ".claude/hooks/protect-sensitive-files.sh"
          }
        ]
      }
    ]
  }
}

 

写这个脚本,应需知道输入参数的定义。所以,至少要规定:tool_input.command。
HOOK_JSON='{
  "hook_event_name": "PreToolUse",
  "tool_name": "Bash",
  "tool_input": {
    "command": "sudo rm -rf ./build"
  }
}'

echo "$HOOK_JSON" | block-dangerous-rm.sh

 

Anthropic 官方 Hook 文档规定:PreToolUse 输入里会包含 tool_nametool_input 等字段;而 tool_input 的具体结构取决于当前 Tool。对于 Bash,关键字段就是 command。

出自:Hooks reference

 

 

 

==》例子 2:代码修改以后,强制质量检查

假设 Claude 修改:

payment.py
 

你希望任何代码修改之后都必须:

ruff check
 

那么:

Claude
 ↓
Edit payment.py
 ↓
修改成功
 ↓
PostToolUse Hook
 ↓
ruff check payment.py
 ↓
错误反馈回来
 

Claude Code 官方直接把“每次文件编辑后自动 formatting”作为 Hook 的典型用途,也支持在文件变化后异步运行测试。

 

 

Implementation

有一个示范。

my-project/
├── .claude/
│   ├── settings.json
│   └── hooks/
│       └── check-python-quality.sh
├── app.py
└── ...

 

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-python-quality.sh" -->
          }
        ]
      }
    ]
  }
}

 

#!/usr/bin/env bash

# 1. 从 stdin 读取 Claude Code 传来的 Hook Event JSON
INPUT=$(cat)

# 2. 取出刚刚被修改的文件路径
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

# 3. 如果拿不到文件路径,直接结束
if [[ -z "$FILE_PATH" ]]; then
    exit 0
fi

# 4. 只检查 Python 文件
if [[ "$FILE_PATH" != *.py ]]; then
    exit 0
fi

# 5. 对刚刚修改的 Python 文件运行 ruff
RESULT=$(ruff check "$FILE_PATH" 2>&1)
EXIT_CODE=$?

# 6. 把检查结果反馈给 Claude
if [[ $EXIT_CODE -eq 0 ]]; then
    MESSAGE="Python quality check passed: $FILE_PATH"
else
    MESSAGE="Python quality check failed for $FILE_PATH:

$RESULT"
fi

jq -nc \
  --arg msg "$MESSAGE" \
  '{
    hookSpecificOutput: {
      hookEventName: "PostToolUse",
      additionalContext: $msg
    }
  }'

exit 0
check-python-quality.sh

 

  

底层原理剖析

大模型返回,cc 接收到:

{
  "name": "Edit",
  "input": {
    "file_path": "/home/jeffrey/my-project/app.py",
    "old_string": "print('hello')",
    "new_string": "print('Hello World')",
    "replace_all": false
  }
}

 

因为是 "Edit",所以调用自己内部的 Edit Tool Executor,然后返回如下,cc 得到了编辑结果。

{
  "tool_response": {
    "filePath": "/path/to/file.txt",
    "success": true
  }
}

 

执行完后,进入 PostToolUse 的生命周期。(因为它就会在任何一个操作结束后 看看是否在自己的注册列表上)--> 发现匹配。

 

构造 Hook Event:本质上就是一个数据转换过程:拿已有的几份数据,拼成一个新的 JSON 对象。

{
  "session_id": "abc123",
  "cwd": "/project",
  "permission_mode": "default",
  "hook_event_name": "PostToolUse",

  "tool_name": "Edit",

  "tool_input": {
    "file_path": "/project/app.py",
    "old_string": "total_amout",
    "new_string": "total_amount"
  },

  "tool_response": {
    "...": "Edit Tool 的执行结果"
  },

  "tool_use_id": "toolu_123"
}

开始触发:

INPUT=$(cat)
# INPUT = 整份 PostToolUse Hook Event JSON

FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path')
# 根据 Edit Tool 已知的输入 Schema,取出被修改文件路径

ruff check "$FILE_PATH"
# 对这个文件执行检查

编写脚本时,按照doc是可以知道Hook Event中有以下字段的。

hook_event_name       → 确认这是 PostToolUse
tool_name             → 确认刚才是 Edit
tool_input            → 原始 Edit 参数
tool_input.file_path  → 被修改文件路径
tool_response         → Edit 执行结果
tool_use_id           → 对应哪一次 Tool Call

 

 

 

==》例子 3:Agent 说“完成了”,但系统不让它结束

我认为是最能体现 Harness Engineering 的 Hook 例子。

 

Implementation

my-project/
├── .claude/
│   ├── settings.json
│   └── hooks/
│       └── verify-before-stop.sh
├── app.py
├── tests/
└── ...

 

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/verify-before-stop.sh"
          }
        ]
      }
    ]
  }
}

 

#!/usr/bin/env bash

INPUT=$(cat)
# 读取 Claude Code 传入的整份 Stop Hook Event JSON

PROJECT_DIR=$(echo "$INPUT" | jq -r '.cwd')
# 从 Hook Event 中取得当前项目目录

cd "$PROJECT_DIR" || exit 1
# 进入项目目录

TEST_RESULT=$(pytest 2>&1)
TEST_EXIT_CODE=$?
# 运行测试,并保存测试输出和退出码

if [ $TEST_EXIT_CODE -ne 0 ]; then
    jq -nc \
      --arg reason "Tests failed. Fix them before finishing: $TEST_RESULT" \
      '{
        decision: "block",
        reason: $reason
      }'
    exit 0
fi
# 测试失败 → 告诉 Claude Code:不允许 Stop

exit 0
# 测试成功 → 没有 block 决策,允许 Claude 正常结束
verify-before-stop.sh

 

 

底层原理剖析

Stop 不是因为模型文字里写了“完成了”三个字才触发,而是 Claude Code 判断主 Agent 已经完成本轮响应、准备停止时触发。

于是,开始构造 Stop Hook Event。官方明确规定 Stop Event 会提供 stop_hook_activelast_assistant_messagebackground_taskssession_crons,再加通用字段。

{
  "session_id": "abc123",
  "transcript_path": "/.../transcript.jsonl",
  "cwd": "/home/jeffrey/my-project",
  "permission_mode": "default",

  "hook_event_name": "Stop",

  "stop_hook_active": false,

  "last_assistant_message": "任务已经完成,所有修改均已实施。",

  "background_tasks": [],
  "session_crons": []
}

 

脚本中调用了pytest。若返回失败,则 Harnes 表示 “并未结束”。

于是乎,Agent 只是“认为自己完成了”;真正有没有资格结束,最终决定权仍然在 Harness。

这就是 Hook + Eval + Loop 连起来了。

 

 

 

==》例子 4:企业审计——所有 Tool Call 自动留痕

假设是银行 Agent:

Agent 查询客户
Agent 查询交易
Agent 修改 Fraud Rule
Agent 调 Backtest
 

你不能依赖模型自己写:

“顺便记录一下我刚刚做了什么。”
 

而可以:

每次 Tool Call 成功
        ↓
PostToolUse
        ↓
HTTP Hook
        ↓
Audit Service
        ↓
保存:
- user
- agent
- timestamp
- tool
- parameters
- result
- session id
 

Claude Code 官方支持 HTTP Hook,甚至直接举了把所有 tool-use event POST 到共享 audit service 的例子。

这就非常像真正企业 Harness:

LLM 做业务
     ↓
Harness自动记录
     ↓
Audit DB
 

LLM完全没有决定权。

 

 

Implementation

my-project/
├── .claude/
│   ├── settings.json 
│   └── hooks/
│       └── audit-tool-call.sh
├── logs/
│   └── tool-audit.jsonl <--
└── ...

 

三个 Hook Event 都可以把自己的 Event JSON 传给 audit-tool-call.sh。

setting.json内容如下。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/audit-tool-call.sh"
          }
        ]
      }
    ],

    "PostToolUse": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/audit-tool-call.sh"
          }
        ]
      }
    ],

    "PostToolUseFailure": [
      {
        "matcher": "*",  # 表示匹配所有tool
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/audit-tool-call.sh"
          }
        ]
      }
    ]
  }
}

 

意思如下:

任何 Tool Call

PreToolUse → audit-tool-call.sh

执行成功

PostToolUse → audit-tool-call.sh

执行失败

PostToolUseFailure → audit-tool-call.sh

 

#!/usr/bin/env bash

INPUT=$(cat)
# 从 stdin 读取 Claude Code 传入的整份 Hook Event JSON

EVENT=$(echo "$INPUT" | jq -r '.hook_event_name')
# 判断当前是哪一种 Event:
# PreToolUse / PostToolUse / PostToolUseFailure

TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
# 生成当前 UTC 时间

LOG_DIR="${CLAUDE_PROJECT_DIR}/logs"
LOG_FILE="${LOG_DIR}/tool-audit.jsonl"

mkdir -p "$LOG_DIR"
# 确保日志目录存在


if [ "$EVENT" = "PreToolUse" ]; then

    echo "$INPUT" | jq -c \
      --arg time "$TIMESTAMP" \
      '{
        timestamp: $time,
        stage: "attempt",
        session_id: .session_id,
        tool_use_id: .tool_use_id,
        tool_name: .tool_name,
        tool_input: .tool_input
      }' >> "$LOG_FILE"

fi


if [ "$EVENT" = "PostToolUse" ]; then

    echo "$INPUT" | jq -c \
      --arg time "$TIMESTAMP" \
      '{
        timestamp: $time,
        stage: "success",
        session_id: .session_id,
        tool_use_id: .tool_use_id,
        tool_name: .tool_name,
        tool_input: .tool_input,
        tool_response: .tool_response,
        duration_ms: .duration_ms
      }' >> "$LOG_FILE"

fi


if [ "$EVENT" = "PostToolUseFailure" ]; then

    echo "$INPUT" | jq -c \
      --arg time "$TIMESTAMP" \
      '{
        timestamp: $time,
        stage: "failure",
        session_id: .session_id,
        tool_use_id: .tool_use_id,
        tool_name: .tool_name,
        tool_input: .tool_input,
        error: .error,
        duration_ms: .duration_ms
      }' >> "$LOG_FILE"

fi

exit 0
# 这里只负责审计,不阻止 Tool
audit-tool-call.sh

 

附加题:这是HTTP Hook的例子。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "*",
        "hooks": [
          {
            "type": "http",
            "url": "https://audit.company.com/claude-hook"
          }
        ]
      }
    ]
  }
}

 

 

底层原理剖析

{
  "type": "tool_use",
  "id": "toolu_123",
  "name": "Bash",
  "input": {
    "command": "npm test"
  }
}

 

From LLM,cc 收到如上,并转化为如下。

# Hook Event JSON
{
"session_id": "abc123", "cwd": "/project", "hook_event_name": "PreToolUse", "tool_name": "Bash", "tool_input": { "command": "npm test" }, "tool_use_id": "toolu_123" }

 

[do] 于是记录了一条日志。

{
  "timestamp": "2026-08-13T00:30:01Z",
  "stage": "attempt",
  "session_id": "abc123",
  "tool_use_id": "toolu_123",
  "tool_name": "Bash",
  "tool_input": {
    "command": "npm test"
  }
}

 

如果 Do successfully.

# Hook Event JSON
{
  "session_id": "abc123",
  "cwd": "/project",

  "hook_event_name": "PostToolUse",

  "tool_name": "Bash",

  "tool_input": {
    "command": "npm test"
  },

  "tool_response": {
    "stdout": "42 tests passed",
    "stderr": "",
    "interrupted": false,
    "isImage": false
  },

  "tool_use_id": "toolu_123",

  "duration_ms": 3250
}

如果 Failure,则触发PostToolUseFailure。

 

 

 

==》例子 4.1:高风险操作前,自动检查环境

这个也很好。

假设 Claude准备执行:

deploy
 

Hook 可以先检查:

当前 branch 是不是 main?
当前环境是不是 production?
测试是不是通过?
有没有未提交修改?
当前用户有没有 deploy 权限?
 

然后: 

Agent → deploy_prod()
            ↓
        PreToolUse
            ↓
      Policy Checker
       ↙        ↘
   不满足      满足
      ↓          ↓
    Block       Allow
 

这里 Hook 已经相当于一个小型 Policy Enforcement Point

未来你做 Business Harness,这种模式非常重要: 

Agent 决定“想做什么”
        ↓
Harness 决定“允不允许做”
 

这两个权力最好不要都交给 LLM。

 

 

Implementation

它发生在 Claude 已经生成 Tool 参数、但 Tool 真正执行之前,并且可以 deny 本次 Tool Call。

my-project/
├── .claude/
│   ├── settings.json
│   └── hooks/
│       └── check-deploy-environment.sh
├── deploy.sh
└── ...

settings.json

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-deploy-environment.sh"
          }
        ]
      }
    ]
  }
}

 

#!/usr/bin/env bash

INPUT=$(cat)
# INPUT = Claude Code 传进来的完整 PreToolUse Hook Event

COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command')
# 取出 Agent 准备执行的 Bash command

CWD=$(echo "$INPUT" | jq -r '.cwd')
# 取出当前工作目录

# 不是 production 部署命令,就不管
if [[ "$COMMAND" != *"deploy.sh production"* ]]; then
    exit 0
fi

cd "$CWD" || exit 1

BRANCH=$(git branch --show-current)
# 查询“当前真实 Git 环境”

CHANGES=$(git status --porcelain)
# 查询是否存在未提交修改

KUBE_CONTEXT=$(kubectl config current-context)
# 查询当前真实 Kubernetes 环境


if [[ "$BRANCH" != "main" ]]; then
    jq -nc '{
      hookSpecificOutput: {
        hookEventName: "PreToolUse",
        permissionDecision: "deny",
        permissionDecisionReason: "Production deployment blocked: current Git branch is not main."
      }
    }'
    exit 0
fi

if [[ -n "$CHANGES" ]]; then
    jq -nc '{
      hookSpecificOutput: {
        hookEventName: "PreToolUse",
        permissionDecision: "deny",
        permissionDecisionReason: "Production deployment blocked: working tree contains uncommitted changes."
      }
    }'
    exit 0
fi

if [[ "$KUBE_CONTEXT" != "prod-cluster" ]]; then
    jq -nc '{
      hookSpecificOutput: {
        hookEventName: "PreToolUse",
        permissionDecision: "deny",
        permissionDecisionReason: "Production deployment blocked: Kubernetes context is not prod-cluster."
      }
    }'
    exit 0
fi

exit 0
check-deploy-environment.sh

 

 

底层实现原理

大模型返回。

{
  "type": "tool_use",
  "id": "toolu_456",
  "name": "Bash",
  "input": {
    "command": "./deploy.sh production"
  }
}

 

进入PreToolUse的周期:因为是"Bash",匹配成功。

CC 开始构造 Hook Event。也就是脚本的输入参数。

{
  "session_id": "abc123",
  "cwd": "/home/jeffrey/my-project",

  "hook_event_name": "PreToolUse",

  "tool_name": "Bash",

  "tool_input": {
    "command": "./deploy.sh production"
  },

  "tool_use_id": "toolu_456"
}

 

本例子中,脚本实际上是做了两件事:

1. Agent 提出 Action;Harness 不只检查 Action 本身,

2. 还可以读取现实世界的 State,并判断,再决定这个 Action 此时是否允许执行。

 

上面的例子可能不是那么的合适,我们本来希望的是Session Start,如下。

但也表达了一部分类似的场景,毕竟,Session Start时,也会检查下环境是否对头。

 

启动 Claude Code
│
├─ 加载 settings / hooks 等配置
│
▼
Session 建立
│
▼
========================
SessionStart   ← 触发一次
========================
│
│  例如:
│  git branch
│  检查项目环境
│  检查依赖
│  获取服务状态
│  把这些信息提供给 Claude
│
▼
等待用户 Prompt
│
▼
UserPromptSubmit
│
▼
发送给 LLM
│
▼
LLM 返回
│
├─ 普通文字
│
└─ 或 Tool Call
       │
       │ 例如:
       │ Bash("./deploy.sh production")
       │
       ▼
========================
PreToolUse     ← 此时才触发
========================
       │
       │ 检查“现在这个操作能不能做”
       │
       ▼
真正执行 Tool
       │
       ▼
PostToolUse
       │
       ▼
LLM继续
       │
      ...

 

 

 

==》例子 5:Session 一启动,就自动准备 Context

Hook 也不一定是安全检查。

比如开发环境每次启动 Claude Code,都需要知道:

当前 Git branch
当前 ticket
当前数据库 schema version
当前部署环境
最近一次 CI 状态
 

可以在 SessionStart 时自动获取这些信息,再注入上下文。官方 Hook 生命周期就包括 session-level 的 SessionStart / SessionEnd

于是:

启动 Claude Code
      ↓
SessionStart Hook
      ↓
git branch
查询 Jira ticket
检查 CI
读取环境
      ↓
把结果加入 Context
      ↓
Claude 开始工作
 
 

Implementation

my-project/
├── .claude/
│   ├── settings.json
│   └── hooks/
│       └── prepare-session-context.sh
├── src/
├── tests/
└── ...

settings.json

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/prepare-session-context.sh"
          }
        ]
      }
    ]
  }
}

“新 Session 一启动,就准备 Context。”

#!/usr/bin/env bash

INPUT=$(cat)
# 读取 Claude Code 传入的整份 SessionStart Hook Event JSON

CWD=$(echo "$INPUT" | jq -r '.cwd')
# 从 Hook Event 中取出当前工作目录

SOURCE=$(echo "$INPUT" | jq -r '.source')
# 取出这次 SessionStart 的来源
# 此例中应为:startup

cd "$CWD" || exit 1
# 进入当前项目目录

BRANCH=$(git branch --show-current 2>/dev/null)
# 读取当前 Git 分支

GIT_STATUS=$(git status --short 2>/dev/null)
# 读取当前工作区是否存在未提交修改

RECENT_COMMITS=$(git log --oneline -3 2>/dev/null)
# 读取最近 3 次 Git commit

cat <<EOF
Current project context:

Working directory:
$CWD

Git branch:
$BRANCH

Uncommitted changes:
${GIT_STATUS:-None}

Recent commits:
$RECENT_COMMITS
EOF
# 把这些动态信息输出到 stdout
prepare-session-context.sh

 

 

底层实现机制

没到与大模型交互的阶段,所以直接生成 SessionStart Hook Event。
{
  "session_id": "abc123",

  "transcript_path":
    "/home/jeffrey/.claude/projects/.../transcript.jsonl",

  "cwd":
    "/home/jeffrey/my-project",

  "hook_event_name":
    "SessionStart",

  "source":
    "startup",

  "model":
    "claude-sonnet-5"
}

最终把当前工作环境的信息发送给大模型。

脚本 stdout 输出:

Current branch: feature/auth
Uncommitted: src/login.py
Recent commits: ...

↓
Claude Code

↓
把 stdout 加入 Context

↓
第一轮 LLM 请求时
Claude 已经知道当前项目状态
 
 
 

==》例子 6:Prompt 一提交,就自动补当前 Ticket Context

Implementation

“继续处理 PAY-1842,看看登录超时的问题”。
my-project/
├── .claude/
│   ├── settings.json
│   └── hooks/
│       └── inject-ticket-context.sh
├── tickets/
│   └── PAY-1842.json -->
└── ...

真实场景,可以换成例如 Jira API.

{
  "id": "PAY-1842",
  "title": "Login request occasionally times out",
  "status": "In Progress",
  "priority": "High",
  "owner": "Jeffrey",
  "description": "Login API occasionally exceeds the 5 second timeout."
}

  

settings.json

这里甚至不需要 BashEdit 这样的 matcher。

因为我们关心的是:每一条用户 Prompt。超高频!

{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/inject-ticket-context.sh"
          }
        ]
      }
    ]
  }
}

UserPromptSubmit 是少数几个可以直接把 command Hook 的 stdout 加进 Claude Context 的事件之一。

#!/usr/bin/env bash

INPUT=$(cat)
# INPUT = Claude Code 传入的整份 UserPromptSubmit Hook Event JSON

PROMPT=$(echo "$INPUT" | jq -r '.prompt')
# 取出用户刚刚提交的原始 Prompt

TICKET_ID=$(echo "$PROMPT" | grep -oE '[A-Z]+-[0-9]+' | head -1)
# 从 Prompt 中寻找类似 PAY-1842、ABC-123 的 Ticket ID

if [[ -z "$TICKET_ID" ]]; then
    exit 0
fi
# 如果这条 Prompt 根本没有 Ticket ID,就什么都不做

TICKET_FILE="${CLAUDE_PROJECT_DIR}/tickets/${TICKET_ID}.json"

if [[ ! -f "$TICKET_FILE" ]]; then
    exit 0
fi
# 找不到对应 Ticket,也直接结束

TITLE=$(jq -r '.title' "$TICKET_FILE")
STATUS=$(jq -r '.status' "$TICKET_FILE")
PRIORITY=$(jq -r '.priority' "$TICKET_FILE")
OWNER=$(jq -r '.owner' "$TICKET_FILE")
DESCRIPTION=$(jq -r '.description' "$TICKET_FILE")

cat <<EOF
Current Ticket Context:

Ticket: $TICKET_ID
Title: $TITLE
Status: $STATUS
Priority: $PRIORITY
Owner: $OWNER
Description: $DESCRIPTION
EOF
# stdout 中的这些内容会被加入 Claude 当前这一轮的 Context
inject-ticket-context.sh

 

 

底层原理机制

也不是由大模型发起。CC直接构造 Hook Event 类似:
{
  "session_id": "abc123",
  "transcript_path": "/.../transcript.jsonl",
  "cwd": "/home/jeffrey/my-project",
  "permission_mode": "default",

  "hook_event_name": "UserPromptSubmit",

  "prompt": "继续处理 PAY-1842,看看登录超时的问题。"
}

脚本提取"prompt"后,提取 “PAY-1842”,并得到 its details。

用户真正输入:
继续处理 PAY-1842,看看登录超时的问题。
        +
Hook 自动补充:
Current Ticket Context:

Ticket: PAY-1842
Title: Login request occasionally times out
Status: In Progress
Priority: High
Owner: Jeffrey
Description:
Login API occasionally exceeds the 5 second timeout.
        ↓

一起交给 Claude
 
 
 

==》所以 Hook 其实有四种非常典型的价值

如果一定要总结,我会分成这四种:

1. Enforcement
   强制执行 / 阻止
   例:禁止 rm -rf

2. Validation
   自动验证
   例:Stop 前必须 tests pass

3. Automation
   固定动作自动做
   例:Edit 后 formatter

4. Observability
   自动记录
   例:所有 Tool Call 写 Audit Log

 

这四种比“打印一句提醒”更能体现 Hook。

而从你以后自己设计 Harness 的角度,我尤其建议记住这个判断:

如果一件事情“LLM可以选择做不做”,就更像 Skill / Instruction;如果一件事情“不管 LLM 怎么想,系统都必须在那个时间点执行”,就应该考虑 Hook / Middleware / Graph Node / Guardrail。

比如银行里:

调查欺诈案件应该遵循什么步骤
→ Skill

任何 Rule 上线前必须 Backtest
→ Hook / Workflow Gate

任何客户数据访问必须 Audit
→ Hook / Middleware

转账超过某额度必须人工批准
→ Guardrail / Human Gate

  

所以 Hook 真正迷人的地方,不是“自动执行 command”,而是:

它让 Harness 在 LLM 的概率性行为周围,建立确定性的控制点。

这才是它值得你迁移到其他业务 Agent 的核心思想。

 

 

进入真正的 Agent Loop 后,又是另一套顺序。

比如你输入:帮我修改这个 Python 文件。

User Prompt
    │
    ▼
UserPromptSubmit Hook
    │
    ▼
Claude Model
    │
    │ 决定调用 Edit
    ▼
PreToolUse Hook
    │
    ▼
Permission Check
    │
    ├── Allow
    ├── Ask
    └── Deny
    │
    ▼
Tool Execution
    │
    ▼
PostToolUse Hook
    │
    ▼
Tool Result → Claude
    │
    ▼
Claude继续思考
    │
    └──────────────┐
                   │
              Agent Loop
                   │
    ◀──────────────┘

 

posted @ 2026-08-12 19:18  郝壹贰叁  阅读(10)  评论(0)    收藏  举报