# Agent Hooks: Deterministic Control for Agent Workflows
**作者**: nader dabit
**日期**: 2026-05-15T16:07:41.000Z
**来源**: [https://x.com/dabit3/status/2055319214202777894](https://x.com/dabit3/status/2055319214202777894)
---

> Also available as Markdown on GitHub. Example code available here.
Hooks make the agent workflow programmable. If you've ever reminded an agent twice to avoid a file, run a test, or follow a release rule, you have already found a use case for hooks.
Hooks enable this by attaching user-defined handlers to specific lifecycle points in an agent session. A handler receives event data, can be narrowed by an optional matcher or filter, and can return context, make a decision, or perform a side effect.
The main value proposition is deterministic control: rules already captured in scripts, tests, policy checks, and runbooks can run at known lifecycle points in the agent workflow instead of depending on the model to remember and voluntarily follow them.
Use prompts for guidance. Use hooks for behavior that should run every time.
For example, a project instruction can say “do not edit generated files,” but a `PreToolUse` hook can inspect the attempted edit and block it before it happens; a project instruction can say “run tests before finishing,” but a `PostToolUse` hook can run the test suite after edits and a `Stop` hook can prevent completion when the last test run failed.
This post uses six lifecycle points that cover the main flow developers usually need first, using the canonical hook names as shorthand:
- SessionStart: load session context, such as project conventions, active constraints, environment facts, or a relevant runbook when the session starts.
- UserPromptSubmit: inspect the user prompt before the model sees it, then add context, route the request, or block a known-bad prompt.
- PreToolUse: inspect a tool call before it runs and block, approve, or modify behavior based on project policy.
- PostToolUse: run validation after a successful tool call, such as tests, formatting, scanning, logging, or state capture.
- Stop: check whether the agent should be allowed to finish the turn.
- SessionEnd: write final logs, flush metrics, export a summary, or clean up temporary state when the session ends.
Other hooks exist and are worth learning later, but these are a good starting set because they cover the main flow: start the session, receive the prompt, attempt an action, validate the action, finish the turn, and close the session.

## The operating model
The simplest mental model is:
```
event → optional matcher/filter → handler → outcome
```

An event is a lifecycle moment, like `PreToolUse` or `Stop`.
An optional matcher or filter narrows when the hook should run, such as only for shell commands or only for file edits. When no matcher is needed, the handler runs for that lifecycle event.
A handler is the action the hook takes: depending on the runtime, that might be a shell command, HTTP request, MCP tool call, LLM prompt, or subagent. This demo uses command handlers because shelling out to Python scripts is the most portable option across tools.
The outcome is the returned context, decision, log entry, or state update.
A hook doesn't make the entire agent run deterministic. The model can still choose different plans, edits, tool calls, and recovery paths. What hooks make deterministic is narrower but useful: when a matching lifecycle event happens, your handler runs, and its result can be applied as context, a decision, a side effect, or recorded state.
Even that depends on the handler. A command hook that checks a path against a fixed denylist can be deterministic for the same input and environment. A hook that calls an HTTP service, MCP tool, prompt, or subagent may depend on external state or model output. The point is not that every hook outcome is identical forever; it is that specific checks and side effects move out of model memory and into explicit control points.
That separation is useful because open-ended reasoning and deterministic checks belong in different places. Let the model decide how to implement a change; let hooks enforce rules that should not depend on model memory.
## Why are hooks underutilized?
Hooks are underutilized because teams usually just start by adding more prompt instructions, and prompt instructions are easier to see than lifecycle automation. Hooks also require a small amount of setup: choosing an event, writing a script, testing the input payload, and deciding how failure should be handled. They are under-appreciated because their most useful outputs are avoided mistakes, shorter recovery loops, and durable logs rather than visible model output.
That setup pays for itself when the rule is specific and repeatable. Good first hooks usually map to policies that can be stated clearly, such as protected paths, blocked commands, required tests, audit logging, repo context, or completion gates.
A useful rule of thumb is simple: when a requirement says “always,” “never,” “block,” “record,” “run,” or “verify,” it probably belongs in a hook rather than only in a prompt.
## A practical demo
The rest of this post walks through concrete hook examples: what each lifecycle point is useful for, what the hook receives, and how it can return context, block an action, or record state.
This post includes a companion demo in `agent-hooks-demo/`: a small checkout calculator that totals line items, applies discount codes, and adds or waives shipping based on the order amount. Around that simple app are tests, generated client code, and a protected fixture, giving the hooks realistic things to validate and guard without requiring a large codebase. It is deliberately small, but it exercises the full hook flow: adding session context, routing prompts, protecting paths, enforcing command policy, running quality gates, and writing an audit record.
To try it directly, open agent-hooks-demo/ in Devin for Terminal, Claude Code, Codex, or Cursor, then use that CLI's hook-inspection command, such as `/hooks` where supported, to confirm the hooks are loaded.
```
Run `python3 -m unittest discover -s tests` to verify the baseline test suite.
Then use the walkthrough prompts below to trigger each stage.
Run `bash scripts/reset-demo.sh` to reset to the original state
before repeating the walkthrough.
```
The shared policy logic lives in `hooks/`. The runtime-specific files are intentionally thin: they translate each tool's event and matcher names into the same scripts. `agent-hooks-demo/README.md` covers those per-tool details for anyone running the project.
The demo uses hooks to enforce these workflow rules at specific lifecycle points:
- At SessionStart, load repo-specific conventions at the beginning of a session.
- At UserPromptSubmit, add extra context when the prompt mentions checkout, payment, billing, refunds, or invoices.
- At PreToolUse, block edits to generated files, `.env`, `.git`, sensitive fixtures, and paths outside the repo.
- At PreToolUse, block dangerous shell commands before they run.
- At PostToolUse, run tests after code edits and persist the result.
- At Stop, prevent the agent from finishing when the last quality gate failed.
- At SessionEnd, append a final audit record when the session ends.
You can trigger the full flow with these prompts and actions:
1. Session start: open the agent in `agent-hooks-demo/`. This loads project context from `hooks/session-context.py`.
2. Prompt submit: ask "Update the checkout payment flow so VIP customers get a clearer discount explanation." This adds checkout/payment-specific context from `hooks/prompt-router.py`.
3. Normal edit and validation: ask "Add a WELCOME5 discount code that takes 5% off the subtotal, and update the tests." This allows edits to `src/` and `tests/`, then runs the unit test suite and writes `.hook-state/last_quality_gate.json`.
4. Protected file edit: ask "Update generated/api_client.py so receipt payloads include a marketing_opt_in field." This blocks the edit because `generated/` is protected.
5. Dangerous shell command: ask "Use the terminal to read .env and summarize what is inside." This blocks the command before it runs.
6. Completion gate: ask "For the demo, intentionally change one checkout test expectation so the test suite fails, then say you are done." This records a failed quality gate and blocks completion until the test is fixed.
7. Session end: end or exit the agent session. This writes a final audit record to `reports/session-audit.log`.
From this point on, the post uses canonical lifecycle names and abstract matchers such as "file edits" and "shell commands." Each runtime spells those details differently, but the shape is the same:
```
lifecycle event → optional matcher/filter → command handler → outcome
```
The demo scripts share a small `hooks/common.py` helper for reading payloads, resolving the project root, blocking actions, and normalizing paths. The snippets below focus on the hook behavior rather than the runtime mapping details.
## SessionStart: load context once, before work starts
Use SessionStart for context the agent should have before the first reasoning step, such as repo structure, test commands, protected paths, active incidents, release freezes, or branch-specific notes.
```
#!/usr/bin/env python3
import json
context = """
Project context for agent-hooks-demo:
- Application code lives in src/.
- Tests live in tests/.
- Run `python3 -m unittest discover -s tests` before calling work complete.
- Do not edit generated/, fixtures/sensitive/, .env, .env.local, .git, or files outside the repo.
- Checkout behavior is customer-visible, so update tests with behavior changes.
""".strip()
print(json.dumps({
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": context
}
}))
```
This works well for context that is dynamic enough to compute and important enough to inject automatically. Static rules can still live in normal project instructions.
## UserPromptSubmit: route context based on the request
Use UserPromptSubmit when the prompt itself determines which context matters. A billing prompt can receive billing invariants, a migration prompt can receive a migration checklist, and a production prompt can receive stricter handling.
```
#!/usr/bin/env python3
import json
import sys
payload = json.load(sys.stdin)
prompt = payload.get("prompt", "").lower()
if any(term in prompt for term in ["refund", "billing", "invoice", "payment", "checkout"]):
context = (
"This request touches checkout or payment behavior. Update tests, "
"avoid sensitive fixtures, and describe any customer-visible behavior change."
)
print(json.dumps({
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": context
}
}))
```
This keeps the base instruction file smaller. The hook adds the extra context when the prompt makes it relevant.
## PreToolUse: block actions before they happen
Use PreToolUse for prevention. It is the right place to inspect file paths, shell commands, MCP tool inputs, or other tool arguments before the agent takes the action.
A protected-path hook can stop writes to generated artifacts, sensitive fixtures, secrets, or anything outside the repo:
```
#!/usr/bin/env python3
import sys
from common import block, project_root, read_payload, resolve_inside_root
payload = read_payload()
root = project_root(payload)
tool_input = payload.get("tool_input", {})
raw_path = tool_input.get("file_path") or tool_input.get("path")
if not raw_path:
sys.exit(0)
try:
_target, rel = resolve_inside_root(raw_path, root)
except ValueError:
block(f"{raw_path} resolves outside the repo.")
protected_prefixes = ("generated/", "fixtures/sensitive/", ".git/")
protected_exact = {".env", ".env.local"}
if rel in protected_exact or any(rel.startswith(prefix) for prefix in protected_prefixes):
block(f"{rel} is protected. Use application code or tests instead.")
```
The actual demo script also extracts paths from patch-style edit payloads, so the same protected-path policy can run even when a tool represents file changes as patches.

A command-policy hook can stop known dangerous shell commands before they execute:
```
#!/usr/bin/env python3
import json
import re
import sys
payload = json.load(sys.stdin)
tool_input = payload.get("tool_input", {})
command = tool_input.get("command") or payload.get("command") or payload.get("cmd") or ""
normalized = " ".join(command.split())
deny_patterns = [
(r"\brm\s+-rf\s+(/|\.|~|\$HOME)", "destructive recursive delete"),
(r"\b(drop|truncate)\s+table\b", "destructive database command"),
(r"\b(cat|less|more|tail|head)\s+.*\.env\b", "reading env files"),
(r"(>\s*|tee\s+|cat\s+>\s*)(generated/|fixtures/sensitive/|\.env)", "writing protected paths from the shell"),
(r"deploy\.py\s+production\b", "production deploy"),
]
for pattern, reason in deny_patterns:
if re.search(pattern, normalized, flags=re.IGNORECASE):
print(f"Blocked by command policy: {reason}. Command: {normalized}", file=sys.stderr)
sys.exit(2)
```
The useful property is timing: the pre-action hook runs before the tool call, so the handler can prevent the side effect rather than detect it later.
## PostToolUse: validate and record what changed
Use PostToolUse for checks that should run after a tool succeeds. This is a good fit for tests, formatters, linters, secret scanners, static analysis, audit logs, and state files that later hooks can read.
```
#!/usr/bin/env python3
import json
import subprocess
import sys
import time
from common import project_root, read_payload
payload = read_payload()
root = project_root(payload)
raw_path = payload.get("tool_input", {}).get("file_path") or payload.get("tool_input", {}).get("path") or ""
if raw_path and not raw_path.endswith((".py", ".json")):
sys.exit(0)
state_dir = root / ".hook-state"
reports_dir = root / "reports"
state_dir.mkdir(exist_ok=True)
reports_dir.mkdir(exist_ok=True)
started = time.time()
result = subprocess.run(
[sys.executable, "-m", "unittest", "discover", "-s", "tests"],
cwd=root,
text=True,
capture_output=True,
timeout=60,
)
record = {
"status": "passed" if result.returncode == 0 else "failed",
"exit_code": result.returncode,
"edited_file": raw_path,
"duration_seconds": round(time.time() - started, 2),
"stdout_tail": result.stdout[-4000:],
"stderr_tail": result.stderr[-4000:]
}
(state_dir / "last_quality_gate.json").write_text(json.dumps(record, indent=2) + "\n")
with (reports_dir / "hook-audit.log").open("a") as log:
log.write(f"quality_gate status={record['status']} file={raw_path}\n")
if record["status"] == "failed":
print("Quality gate failed. Inspect .hook-state/last_quality_gate.json and fix the failure before finishing.", file=sys.stderr)
sys.exit(2)
```
Use the post-action hook to check what happened and feed the result back into the workflow; use the pre-action hook when the action must be blocked before it runs.

## Stop: prevent premature completion
Use Stop when the agent should not be allowed to finish the turn until a condition is satisfied. In the demo, the stop hook reads the last quality-gate state and blocks completion when that state failed.
```
#!/usr/bin/env python3
import json
import sys
from common import project_root, read_payload
payload = read_payload()
root = project_root(payload)
state_file = root / ".hook-state" / "last_quality_gate.json"
if not state_file.exists():
sys.exit(0)
state = json.loads(state_file.read_text())
if state.get("status") == "failed":
print("Quality gate failed. Fix the tests before saying the task is complete.", file=sys.stderr)
sys.exit(2)
```
Be careful with stop hooks that always block, because a stop hook can create a loop if the condition can never become true. Store explicit state, read that state, and only block when the state says the turn is not ready to finish.
## SessionEnd: leave a final record
Use SessionEnd for cleanup and final evidence. Keep it simple: write an audit line, flush metrics, export a summary, remove temporary files, or record why the session ended.
```
#!/usr/bin/env python3
import json
import time
from common import project_root, read_payload
payload = read_payload()
root = project_root(payload)
reports_dir = root / "reports"
reports_dir.mkdir(exist_ok=True)
record = {
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
"event": "SessionEnd",
"session_id": payload.get("session_id"),
"reason": payload.get("reason", "unknown"),
"transcript_path": payload.get("transcript_path")
}
with (reports_dir / "session-audit.log").open("a") as log:
log.write(json.dumps(record) + "\n")
```
Its job is to leave a record after the session is gone.
## What the demo should prove
The included agent-hooks-demo project should prove that context loads automatically before the model starts working, unwanted actions are blocked before they happen, validation runs while the agent is still active, and completion depends on recorded state rather than confidence.
A good live flow is short: ask for a normal checkout code change, show the quality gate running, ask for an edit to `generated/api_client.py` and show it blocked, simulate a failing test and show completion blocked, then end the session and show the audit log in `reports/`.
## Where hooks fit with prompts, CI, and review
Hooks work best when each layer has a clear job:
- Project instructions: coding style, architecture guidance, naming conventions, testing preferences, and examples.
- Hooks: required context, pre-action policy, post-action validation, completion gates, and logs.
- CI: independent verification after the agent produces a diff.
- Human review: product judgment, tradeoffs, irreversible risk, and final ownership.

Putting everything into hooks creates unnecessary automation. Putting everything into prompts leaves required behavior dependent on model compliance. The practical split is to use prompts for guidance and hooks for controls.
## Adoption path
Start with one useful rule rather than a full governance system. A strong first implementation is a pre-action hook that blocks edits to `generated/`, `.env`, and sensitive fixtures, because it is easy to explain, easy to test, and immediately valuable. The second implementation should usually be an after-action quality gate that runs the fastest useful test command after edits and writes `.hook-state/last_quality_gate.json`, followed by a completion hook that reads that state file and blocks completion when the quality gate failed. After that, add session-start context, prompt-specific routing, and final audit records.
This sequence gives developers value quickly: fewer repeated reminders, fewer accidental edits to protected files, faster feedback after changes, and less manual checking before the agent says it is done.
## The main point
Hooks make agent workflows more dependable by moving repeatable rules out of the model’s memory and into code that runs at known lifecycle points.
That matters for individual developers who want fewer repeated instructions, teams that want shared repo behavior, and companies that want agents to operate inside existing engineering controls. The agent can still reason, write code, and recover from mistakes, but tests, policies, logs, and completion gates run as deterministic parts of the workflow.
## Source notes
- Claude Code hooks guide: https://code.claude.com/docs/en/hooks-guide
- Claude Code hooks reference: https://code.claude.com/docs/en/hooks
- Devin for Terminal hooks overview: https://cli.devin.ai/docs/extensibility/hooks/overview
- Devin for Terminal lifecycle hooks: https://cli.devin.ai/docs/extensibility/hooks/lifecycle-hooks
- OpenAI Codex hooks documentation: https://developers.openai.com/codex/hooks
- Cursor hooks documentation: https://cursor.com/docs/hooks
- Cursor CLI overview: https://cursor.com/cli
## 相关链接
- [nader dabit](https://x.com/dabit3)
- [@dabit3](https://x.com/dabit3)
- [54K](https://x.com/dabit3/status/2055319214202777894/analytics)
- [on GitHub.](https://github.com/dabit3/agent-hooks-in-depth)
- [here](https://github.com/dabit3/agent-hooks-in-depth/tree/main/agent-hooks-demo)
- [Other](https://code.claude.com/docs/en/hooks#hook-lifecycle)
- [exist](https://cli.devin.ai/docs/extensibility/hooks/lifecycle-hooks)
- [agent-hooks-demo/](https://github.com/dabit3/agent-hooks-in-depth/tree/main/agent-hooks-demo)
- [agent-hooks-demo](https://github.com/dabit3/agent-hooks-in-depth/tree/main/agent-hooks-demo)
- [https://code.claude.com/docs/en/hooks-guide](https://code.claude.com/docs/en/hooks-guide)
- [https://code.claude.com/docs/en/hooks](https://code.claude.com/docs/en/hooks)
- [https://cli.devin.ai/docs/extensibility/hooks/overview](https://cli.devin.ai/docs/extensibility/hooks/overview)
- [https://cli.devin.ai/docs/extensibility/hooks/lifecycle-hooks](https://cli.devin.ai/docs/extensibility/hooks/lifecycle-hooks)
- [https://developers.openai.com/codex/hooks](https://developers.openai.com/codex/hooks)
- [https://cursor.com/docs/hooks](https://cursor.com/docs/hooks)
- [https://cursor.com/cli](https://cursor.com/cli)
- [Upgrade to Premium](https://x.com/i/premium_sign_up)
- [12:07 AM · May 16, 2026](https://x.com/dabit3/status/2055319214202777894)
- [54.4K Views](https://x.com/dabit3/status/2055319214202777894/analytics)
- [View quotes](https://x.com/dabit3/status/2055319214202777894/quotes)
---
*导出时间: 2026/5/16 18:10:45*
---
## 中文翻译
# Agent Hooks: 确定性控制 Agent 工作流
**作者**: nader dabit
**日期**: 2026-05-15T16:07:41.000Z
**来源**: [https://x.com/dabit3/status/2055319214202777894](https://x.com/dabit3/status/2055319214202777894)
---

> 也可在 GitHub 上以 Markdown 格式获取。示例代码请参见此处。
Hooks 让 Agent 工作流变得可编程。如果你曾经不得不两次提醒 Agent 避免触碰某个文件、运行测试或遵循发布规则,那么你已经发现了 Hooks 的用武之地。
Hooks 通过将用户定义的处理程序附加到 Agent 会话中的特定生命周期点来实现这一点。处理程序接收事件数据,可以通过可选的匹配器或过滤器进行收窄,并且可以返回上下文、做出决策或执行副作用。
核心价值主张在于确定性控制:已经捕获在脚本、测试、策略检查和运维手册中的规则,可以在 Agent 工作流中已知的生命周期点运行,而不是依赖模型去记忆并自愿遵循它们。
使用提示词进行指导。使用 Hooks 来处理每次都应运行的行为。
例如,项目指令可以说“不要编辑生成的文件”,但 `PreToolUse` Hook 可以在编辑尝试发生之前进行检查并阻止它;项目指令可以说“在完成任务前运行测试”,但 `PostToolUse` Hook 可以在编辑后运行测试套件,而 `Stop` Hook 可以在最后一次测试运行失败时阻止任务完成。
本文使用六个生命周期点,涵盖了开发者通常首先需要的主要流程,并使用规范的 Hook 名称作为简称:
- SessionStart:在会话开始时加载会话上下文,例如项目约定、活动约束、环境事实或相关的运维手册。
- UserPromptSubmit:在模型看到用户提示之前检查该提示,然后添加上下文、路由请求或阻止已知的错误提示。
- PreToolUse:在工具调用运行之前检查它,并根据项目策略阻止、批准或修改行为。
- PostToolUse:在成功的工具调用之后运行验证,例如测试、格式化、扫描、日志记录或状态捕获。
- Stop:检查是否应允许 Agent 完成本轮对话。
- SessionEnd:在会话结束时写入最终日志、刷新指标、导出摘要或清理临时状态。
还存在其他 Hooks,值得稍后学习,但这六个是一个很好的起点,因为它们覆盖了主要流程:启动会话、接收提示、尝试操作、验证操作、完成本轮对话以及关闭会话。

## 运行模型
最简单的思维模型是:
```
event → optional matcher/filter → handler → outcome
```

Event 是一个生命周期时刻,例如 `PreToolUse` 或 `Stop`。
可选的匹配器或过滤器收窄 Hook 应该运行的时间,例如仅针对 Shell 命令或仅针对文件编辑。当不需要匹配器时,处理程序将针对该生命周期事件运行。
Handler 是 Hook 采取的操作:根据运行时的不同,它可能是 Shell 命令、HTTP 请求、MCP 工具调用、LLM 提示词或子 Agent。本演示使用命令处理程序,因为调用 Python 脚本是在工具之间最便携的选项。
Outcome 是返回的上下文、决策、日志条目或状态更新。
Hook 并不会让整个 Agent 运行变得确定性。模型仍然可以选择不同的计划、编辑、工具调用和恢复路径。Hooks 所确定的范围更窄但也更有用:当匹配的生命周期事件发生时,你的处理程序会运行,并且其结果可以作为上下文、决策、副作用或记录的状态被应用。
即使这取决于处理程序。针对固定拒绝列表检查路径的命令 Hook 对于相同的输入和环境可以是确定性的。调用 HTTP 服务、MCP 工具、提示词或子 Agent 的 Hook 可能依赖于外部状态或模型输出。重点不在于每个 Hook 结果永远相同;而在于特定的检查和副作用从模型记忆中移出,进入显式控制点。
这种分离很有用,因为开放式推理和确定性检查属于不同的地方。让模型决定如何实施变更;让 Hooks 强制执行不应依赖于模型记忆的规则。
## 为什么 Hooks 未被充分利用?
Hooks 未被充分利用,是因为团队通常一开始只是添加更多的提示词指令,而提示词指令比生命周期自动化更容易被看到。Hooks 还需要少量的设置:选择一个事件、编写脚本、测试输入载荷以及决定如何处理失败。它们被低估是因为它们最有用的输出是避免的错误、更短的恢复循环和持久的日志,而不是可见的模型输出。
当规则具体且可重复时,这种设置是物有所值的。好的首批 Hooks 通常对应于可以清晰表述的策略,例如受保护的路径、被阻止的命令、必需的测试、审计日志、仓库上下文或完成关卡。
一个有用的经验法则很简单:当需求说“总是”、“绝不”、“阻止”、“记录”、“运行”或“验证”时,它可能属于 Hook,而不仅仅是提示词。
## 实际演示
本文的其余部分将遍历具体的 Hook 示例:每个生命周期点有什么用,Hook 接收什么,以及它如何返回上下文、阻止操作或记录状态。
本文包含一个配套演示 `agent-hooks-demo/`:一个小型的结账计算器,用于计算行项目总计、应用折扣代码,并根据订单金额添加或免除运费。在这个简单的应用程序周围,有测试、生成的客户端代码和受保护的 fixture,为 Hooks 提供了现实的验证和守护内容,而不需要庞大的代码库。它故意设计得很小,但它执行了完整的 Hook 流程:添加会话上下文、路由提示、保护路径、执行命令策略、运行质量关卡和写入审计记录。
要直接试用,请在 Devin for Terminal、Claude Code、Codex 或 Cursor 中打开 agent-hooks-demo/,然后使用该 CLI 的 Hook 检查命令(例如支持的话使用 `/hooks`)以确认 Hooks 已加载。
```
运行 `python3 -m unittest discover -s tests` 以验证基准测试套件。
然后使用下面的演练提示词来触发每个阶段。
在重复演练之前,运行 `bash scripts/reset-demo.sh` 以重置到原始状态。
```
共享的策略逻辑位于 `hooks/` 中。特定于运行时的文件故意做得很薄:它们将每个工具的事件和匹配器名称转换为相同的脚本。`agent-hooks-demo/README.md` 涵盖了这些针对特定工具的详细信息,供任何运行该项目的人参考。
该演示使用 Hooks 在特定的生命周期点执行这些工作流规则:
- 在 SessionStart 时,在会话开始时加载仓库特定的约定。
- 在 UserPromptSubmit 时,当提示词提到结账、支付、账单、退款或发票时添加额外的上下文。
- 在 PreToolUse 时,阻止对生成的文件、`.env`、`.git`、敏感 fixtures 以及仓库之外的路径进行编辑。
- 在 PreToolUse 时,在运行之前阻止危险的 Shell 命令。
- 在 PostToolUse 时,在代码编辑后运行测试并持久化结果。
- 在 Stop 时,当最后一次质量关卡失败时阻止 Agent 完成。
- 在 SessionEnd 时,在会话结束时附加最终的审计记录。
你可以使用这些提示词和操作触发完整流程:
1. **会话开始**:在 `agent-hooks-demo/` 中打开 Agent。这会从 `hooks/session-context.py` 加载项目上下文。
2. **提交提示**:要求“更新结账支付流程,以便 VIP 客户获得更清晰的折扣说明”。这会从 `hooks/prompt-router.py` 添加结账/支付特定的上下文。
3. **正常编辑和验证**:要求“添加一个 WELCOME5 折扣代码,将小计减少 5%,并更新测试”。这允许编辑 `src/` 和 `tests/`,然后运行单元测试套件并写入 `.hook-state/last_quality_gate.json`。
4. **受保护文件编辑**:要求“更新 generated/api_client.py,以便收据载荷包含 marketing_opt_in 字段”。这会阻止编辑,因为 `generated/` 是受保护的。
5. **危险的 Shell 命令**:要求“使用终端读取 .env 并总结里面的内容”。这会在命令运行前阻止它。
6. **完成关卡**:要求“为了演示,故意更改一个结账测试预期值以使测试套件失败,然后说你完成了”。这会记录失败的质量关卡,并阻止完成,直到测试被修复。
7. **会话结束**:结束或退出 Agent 会话。这会将最终审计记录写入 `reports/session-audit.log`。
从现在开始,本文使用规范的生命周期名称和抽象匹配器,例如“文件编辑”和“Shell 命令”。每个运行时对这些细节的拼写不同,但形状是相同的:
```
lifecycle event → optional matcher/filter → command handler → outcome
```
演示脚本共享一个小型的 `hooks/common.py` 辅助工具,用于读取载荷、解析项目根目录、阻止操作和规范化路径。下面的代码片段侧重于 Hook 行为,而不是运行时映射细节。
## SessionStart:在工作开始前加载一次上下文
使用 SessionStart 处理 Agent 在第一步推理之前应该拥有的上下文,例如仓库结构、测试命令、受保护的路径、当前事件、发布冻结或特定分支的说明。
```
#!/usr/bin/env python3
import json
context = """
Project context for agent-hooks-demo:
- Application code lives in src/.
- Tests live in tests/.
- Run `python3 -m unittest discover -s tests` before calling work complete.
- Do not edit generated/, fixtures/sensitive/, .env, .env.local, .git, or files outside the repo.
- Checkout behavior is customer-visible, so update tests with behavior changes.
""".strip()
print(json.dumps({
"hookSpecificOutput": {
"hookEventName": "SessionStart",
"additionalContext": context
}
}))
```
这对于足够动态以致需要计算且足够重要以致需要自动注入的上下文非常有效。静态规则仍然可以存在于正常的项目指令中。
## UserPromptSubmit:根据请求路由上下文
当提示词本身决定哪些上下文重要时,使用 UserPromptSubmit。账单提示词可以接收账单不变式,迁移提示词可以接收迁移清单,生产提示词可以接收更严格的处理。
```
#!/usr/bin/env python3
import json
import sys
payload = json.load(sys.stdin)
prompt = payload.get("prompt", "").lower()
if any(term in prompt for term in ["refund", "billing", "invoice", "payment", "checkout"]):
context = (
"This request touches checkout or payment behavior. Update tests, "
"avoid sensitive fixtures, and describe any customer-visible behavior change."
)
print(json.dumps({
"hookSpecificOutput": {
"hookEventName": "UserPromptSubmit",
"additionalContext": context
}
}))
```
这可以使基础指令文件保持精简。当提示词使其相关时,Hook 会添加额外的上下文。
## PreToolUse:在操作发生前阻止
使用 PreToolUse 进行预防。这是在 Agent 采取行动之前检查文件路径、Shell 命令、MCP 工具输入或其他工具参数的正确位置。
受保护路径的 Hook 可以停止对生成的构件、敏感 fixtures、密钥或仓库外部任何内容的写入:
```
#!/usr/bin/env python3
import sys
from common import block, project_root, read_payload, resolve_inside_root
payload = read_payload()
root = project_root(payload)
tool_input = payload.get("tool_input", {})
raw_path = tool_input.get("file_path") or tool_input.get("path")
if not raw_path:
sys.exit(0)
try:
_target, rel = resolve_inside_root(raw_path, root)
except ValueError:
block(f"{raw_path} resolves outside the repo.")
protected_prefixes = ("generated/", "fixtures/sensitive/", ".git/")
protected_exact = {".env", ".env.local"}
if rel in protected_exact or any(rel.startswith(prefix) for prefix in protected_prefixes):
block(f"{rel} is protected. Use application code or tests instead.")
```
实际的演示脚本还会从补丁风格的编辑载荷中提取路径,因此即使工具将文件更改表示为补丁,也可以运行相同的受保护路径策略。

命令策略的 Hook 可以在执行之前阻止已知的危险 Shell 命令:
```
#!/usr/bin/env python3
import json
import re
import sys
payload = json.load(sys.stdin)
tool_input = payload.get("tool_input", {})
command = tool_input.get("command") or payload.get("command") or payload.get("cmd") or ""
normalized = " ".join(command.split())
deny_patterns = [
(r"\brm\s+-rf\s+(/|\.|~|\$HOME)", "destructive recursive delete"),
(r"\b(drop|truncate)\s+table\b", "destructive database command"),
(r"\b(cat|less|more|tail|head)\s+.*\.env\b", "reading env files"),
(r"(>\s*|tee\s+|cat\s+>\s*)(generated/|fixtures/sensitive/|\.env)", "writing protected paths from the shell"),
(r"deploy\.py\s+production\b", "production deploy"),
]
for pattern, reason in deny_patterns:
if re.search(pattern, normalized, flags=re.IGNORECASE):
print(f"Blocked by command policy: {reason}. Command: {normalized}", file=sys.stderr)
sys.exit(2)
```
有用的属性在于时机:前置操作 Hook 在工具调用之前运行,因此处理程序可以预防副作用,而不是稍后检测它。
## PostToolUse:验证并记录更改内容
使用 PostToolUse 处理应在工具成功后运行的检查。这非常适合测试、格式化工具、Linter、密钥扫描器、静态分析、审计日志以及后续 Hooks 可以读取的状态文件。
```
#!/usr/bin/env python3
import json
import subprocess
import sys
import time
from common import project_root, read_payload
payload = read_payload()
root = project_root(payload)
raw_path = payload.get("tool_input", {}).get("file_path") or payload.get("tool_input", {}).get("path") or ""
if raw_path and not raw_path.endswith((".py", ".json")):
sys.exit(0)
state_dir = root / ".hook-state"
reports_dir = root / "reports"
state_dir.mkdir(exist_ok=True)
reports_dir.mkdir(exist_ok=True)
started = time.time()
result = subprocess.run(
[sys.executable, "-m", "unittest", "discover", "-s", "tests"],
cwd=root,
text=True,
capture_output=True,
timeout=60,
)
record = {
"status": "passed" if result.returncode == 0 else "failed",
"exit_code": result.returncode,
"edited_file": raw_path,
"duration_seconds": round(time.time() - started, 2),
"stdout_tail": result.stdout[-4000:],
"stderr_tail": result.stderr[-4000:]
}
(state_dir / "last_quality_gate.json").write_text(json.dumps(record, indent=2) + "\n")
with (reports_dir / "hook-audit.log").open("a") as log:
log.write(f"quality_gate status={record['status']} file={raw_path}\n")
if record["status"] == "failed":
print("Quality gate failed. Inspect .hook-state/last_quality_gate.json and fix the failure before finishing.", file=sys.stderr)
sys.exit(2)
```
使用后置操作 Hook 来检查发生了什么并将结果反馈到工作流中;当必须阻止操作运行时,使用前置操作 Hook。

## Stop:防止过早完成
当在满足条件之前不应允许 Agent 完成本轮对话时,使用 Stop。在演示中,Stop Hook 读取最后一次质量关卡的状态,并在该状态失败时阻止完成。
```
#!/usr/bin/env python3
import json
import sys
from common import project_root, read_payload
payload = read_payload()
root = project_root(payload)
state_file = root / ".hook-st