[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"project-80770":3},{"id":4,"name":5,"fullName":6,"owner":7,"repo":5,"description":8,"homepage":8,"htmlUrl":8,"language":9,"languages":8,"totalLinesOfCode":8,"stars":10,"forks":11,"watchers":12,"openIssues":13,"contributorsCount":13,"subscribersCount":13,"size":13,"stars1d":13,"stars7d":13,"stars30d":14,"stars90d":13,"forks30d":13,"starsTrendScore":13,"compositeScore":15,"rankGlobal":8,"rankLanguage":8,"license":8,"archived":16,"fork":16,"defaultBranch":17,"hasWiki":18,"hasPages":16,"topics":19,"createdAt":8,"pushedAt":8,"updatedAt":20,"readmeContent":21,"aiSummary":22,"trendingCount":13,"starSnapshotCount":13,"syncStatus":14,"lastSyncTime":23,"discoverSource":24},80770,"agent-hooks-in-depth","dabit3\u002Fagent-hooks-in-depth","dabit3",null,"Python",42,7,1,0,2,36.91,false,"main",true,[],"2026-06-12 04:01:30","# Agent Hooks: Deterministic Control for Agent Workflows\n\n![header](.\u002Fheader.png)\n\nHooks 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.\n\nHooks 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.\n\nThe 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.\n\nUse 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.\n\nThis post uses six lifecycle points that cover the main flow developers usually need first, using the canonical hook names as shorthand:\n\n- `SessionStart`: Load session context, such as project conventions, active constraints, environment facts, or a relevant runbook when the session starts.\n- `UserPromptSubmit`: Inspect the user prompt before the model sees it, then add context, route the request, or block a known-bad prompt.\n- `PreToolUse`: Inspect a tool call before it runs and block, approve, or modify behavior based on project policy.\n- `PostToolUse`: Run validation after a successful tool call, such as tests, formatting, scanning, logging, or state capture.\n- `Stop`: Check whether the agent should be allowed to finish the turn.\n- `SessionEnd`: Write final logs, flush metrics, export a summary, or clean up temporary state when the session ends.\n\nOther 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.\n\n![Session lifecycle](.\u002Fsession_lifecycle.png)\n\n## The operating model\n\nThe simplest mental model is:\n\n```text\nevent → optional matcher\u002Ffilter → handler → outcome\n```\n\n![Hook lifecycle](.\u002Fhook_lifecycle.png)\n\nAn **event** is a lifecycle moment, such as `PreToolUse` or `Stop`.\n\nAn 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.\n\nA **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.\n\nThe **outcome** is the returned context, decision, log entry, or state update.\n\nA hook does not 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.\n\nEven 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.\n\nThat 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.\n\n## Why hooks are underutilized\n\nHooks are underutilized because teams often 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 underappreciated because their most useful outputs are avoided mistakes, shorter recovery loops, and durable logs rather than visible model output.\n\nThat setup pays for itself when the rule is specific and repeatable. Good first hooks usually map to policies the team can already state clearly, such as protected paths, blocked commands, required tests, audit logging, repo context, or completion gates.\n\nA 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.\n\n## A practical demo\n\nThe 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.\n\nThis post includes a companion demo in `agent-hooks-demo\u002F`: 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.\n\nTo try it directly, open `agent-hooks-demo\u002F` in Devin for Terminal, Claude Code, Codex, or Cursor, then use that CLI's hook-inspection command, such as `\u002Fhooks` 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.\n\nRun `bash scripts\u002Freset-demo.sh` before repeating the walkthrough.\n\nThe shared policy logic lives in `hooks\u002F`. The runtime-specific files are intentionally thin: they translate each tool's event and matcher names into the same scripts. `agent-hooks-demo\u002FREADME.md` covers those per-tool details for anyone running the project.\n\nThe demo uses hooks to enforce these workflow rules at specific lifecycle points:\n\n- At `SessionStart`, load repo-specific conventions at the beginning of a session.\n- At `UserPromptSubmit`, add extra context when the prompt mentions checkout, payment, billing, refunds, or invoices.\n- At `PreToolUse`, block edits to generated files, `.env`, `.git`, sensitive fixtures, and paths outside the repo.\n- At `PreToolUse`, block dangerous shell commands before they run.\n- At `PostToolUse`, run tests after code edits and persist the result.\n- At `Stop`, prevent the agent from finishing when the last quality gate failed.\n- At `SessionEnd`, append a final audit record when the session ends.\n\nYou can trigger the full flow with these prompts and actions:\n\n1. Session start: open the agent in `agent-hooks-demo\u002F`. This loads project context from `hooks\u002Fsession-context.py`.\n2. Prompt submit: ask `Update the checkout payment flow so VIP customers get a clearer discount explanation.` This adds checkout\u002Fpayment-specific context from `hooks\u002Fprompt-router.py`.\n3. 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\u002F` and `tests\u002F`, then runs the unittest suite and writes `.hook-state\u002Flast_quality_gate.json`.\n4. Protected file edit: ask `Update generated\u002Fapi_client.py so receipt payloads include a marketing_opt_in field.` This blocks the edit because `generated\u002F` is protected.\n5. Dangerous shell command: ask `Use the terminal to read .env and summarize what is inside.` This blocks the command before it runs.\n6. 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.\n7. Session end: end or exit the agent session. This writes a final audit record to `reports\u002Fsession-audit.log`.\n\nFrom 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:\n\n```text\nlifecycle event → optional matcher\u002Ffilter → command handler → outcome\n```\n\nThe demo scripts share a small `hooks\u002Fcommon.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 adapter details.\n\n## `SessionStart`: load context once, before work starts\n\nUse `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.\n\n```python\n#!\u002Fusr\u002Fbin\u002Fenv python3\nimport json\n\ncontext = \"\"\"\nProject context for agent-hooks-demo:\n- Application code lives in src\u002F.\n- Tests live in tests\u002F.\n- Run `python3 -m unittest discover -s tests` before calling work complete.\n- Do not edit generated\u002F, fixtures\u002Fsensitive\u002F, .env, .env.local, .git, or files outside the repo.\n- Checkout behavior is customer-visible, so update tests with behavior changes.\n\"\"\".strip()\n\nprint(json.dumps({\n    \"hookSpecificOutput\": {\n        \"hookEventName\": \"SessionStart\",\n        \"additionalContext\": context\n    }\n}))\n```\n\nThis 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.\n\n## `UserPromptSubmit`: route context based on the request\n\nUse `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.\n\n```python\n#!\u002Fusr\u002Fbin\u002Fenv python3\nimport json\nimport sys\n\npayload = json.load(sys.stdin)\nprompt = payload.get(\"prompt\", \"\").lower()\n\nif any(term in prompt for term in [\"refund\", \"billing\", \"invoice\", \"payment\", \"checkout\"]):\n    context = (\n        \"This request touches checkout or payment behavior. Update tests, \"\n        \"avoid sensitive fixtures, and describe any customer-visible behavior change.\"\n    )\n    print(json.dumps({\n        \"hookSpecificOutput\": {\n            \"hookEventName\": \"UserPromptSubmit\",\n            \"additionalContext\": context\n        }\n    }))\n```\n\nThis keeps the base instruction file smaller. The hook adds the extra context when the prompt makes it relevant.\n\n## `PreToolUse`: block actions before they happen\n\nUse `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.\n\nA protected-path hook can stop writes to generated artifacts, sensitive fixtures, secrets, or anything outside the repo:\n\n```python\n#!\u002Fusr\u002Fbin\u002Fenv python3\nimport sys\n\nfrom common import block, project_root, read_payload, resolve_inside_root\n\npayload = read_payload()\nroot = project_root(payload)\ntool_input = payload.get(\"tool_input\", {})\nraw_path = tool_input.get(\"file_path\") or tool_input.get(\"path\")\n\nif not raw_path:\n    sys.exit(0)\n\ntry:\n    target, rel = resolve_inside_root(raw_path, root)\nexcept ValueError:\n    block(f\"{raw_path} resolves outside the repo.\")\n\nprotected_prefixes = (\"generated\u002F\", \"fixtures\u002Fsensitive\u002F\", \".git\u002F\")\nprotected_exact = {\".env\", \".env.local\"}\n\nif rel in protected_exact or any(rel.startswith(prefix) for prefix in protected_prefixes):\n    block(f\"{rel} is protected. Use application code or tests instead.\")\n```\n\nThe 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.\n\n![PreToolUse](.\u002Fpretooluse.png)\n\nA command-policy hook can stop known dangerous shell commands before they execute:\n\n```python\n#!\u002Fusr\u002Fbin\u002Fenv python3\nimport json\nimport re\nimport sys\n\npayload = json.load(sys.stdin)\ntool_input = payload.get(\"tool_input\", {})\ncommand = tool_input.get(\"command\") or payload.get(\"command\") or payload.get(\"cmd\") or \"\"\nnormalized = \" \".join(command.split())\n\ndeny_patterns = [\n    (r\"\\brm\\s+-rf\\s+(\u002F|\\.|~|\\$HOME)\", \"destructive recursive delete\"),\n    (r\"\\b(drop|truncate)\\s+table\\b\", \"destructive database command\"),\n    (r\"\\b(cat|less|more|tail|head)\\s+.*\\.env\\b\", \"reading env files\"),\n    (r\"(>\\s*|tee\\s+|cat\\s+>\\s*)(generated\u002F|fixtures\u002Fsensitive\u002F|\\.env)\", \"writing protected paths from the shell\"),\n    (r\"deploy\\.py\\s+production\\b\", \"production deploy\"),\n]\n\nfor pattern, reason in deny_patterns:\n    if re.search(pattern, normalized, flags=re.IGNORECASE):\n        print(f\"Blocked by command policy: {reason}. Command: {normalized}\", file=sys.stderr)\n        sys.exit(2)\n```\n\nThe 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.\n\n## `PostToolUse`: validate and record what changed\n\nUse `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.\n\n```python\n#!\u002Fusr\u002Fbin\u002Fenv python3\nimport json\nimport subprocess\nimport sys\nimport time\n\nfrom common import project_root, read_payload\n\npayload = read_payload()\nroot = project_root(payload)\nraw_path = payload.get(\"tool_input\", {}).get(\"file_path\") or payload.get(\"tool_input\", {}).get(\"path\") or \"\"\n\nif raw_path and not raw_path.endswith((\".py\", \".json\")):\n    sys.exit(0)\n\nstate_dir = root \u002F \".hook-state\"\nreports_dir = root \u002F \"reports\"\nstate_dir.mkdir(exist_ok=True)\nreports_dir.mkdir(exist_ok=True)\n\nstarted = time.time()\nresult = subprocess.run(\n    [sys.executable, \"-m\", \"unittest\", \"discover\", \"-s\", \"tests\"],\n    cwd=root,\n    text=True,\n    capture_output=True,\n    timeout=60,\n)\n\nrecord = {\n    \"status\": \"passed\" if result.returncode == 0 else \"failed\",\n    \"exit_code\": result.returncode,\n    \"edited_file\": raw_path,\n    \"duration_seconds\": round(time.time() - started, 2),\n    \"stdout_tail\": result.stdout[-4000:],\n    \"stderr_tail\": result.stderr[-4000:]\n}\n\n(state_dir \u002F \"last_quality_gate.json\").write_text(json.dumps(record, indent=2) + \"\\n\")\nwith (reports_dir \u002F \"hook-audit.log\").open(\"a\") as log:\n    log.write(f\"quality_gate status={record['status']} file={raw_path}\\n\")\n\nif record[\"status\"] == \"failed\":\n    print(\"Quality gate failed. Inspect .hook-state\u002Flast_quality_gate.json and fix the failure before finishing.\", file=sys.stderr)\n    sys.exit(2)\n```\n\nUse 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.\n\n![Two hooks](.\u002Ftwo_hooks.png)\n\n## `Stop`: prevent premature completion\n\nUse `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.\n\n```python\n#!\u002Fusr\u002Fbin\u002Fenv python3\nimport json\nimport sys\n\nfrom common import project_root, read_payload\n\npayload = read_payload()\nroot = project_root(payload)\nstate_file = root \u002F \".hook-state\" \u002F \"last_quality_gate.json\"\n\nif not state_file.exists():\n    sys.exit(0)\n\nstate = json.loads(state_file.read_text())\nif state.get(\"status\") == \"failed\":\n    print(\"Quality gate failed. Fix the tests before saying the task is complete.\", file=sys.stderr)\n    sys.exit(2)\n```\n\nBe 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.\n\n## `SessionEnd`: leave a final record\n\nUse `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.\n\n```python\n#!\u002Fusr\u002Fbin\u002Fenv python3\nimport json\nimport time\n\nfrom common import project_root, read_payload\n\npayload = read_payload()\nroot = project_root(payload)\nreports_dir = root \u002F \"reports\"\nreports_dir.mkdir(exist_ok=True)\n\nrecord = {\n    \"timestamp\": time.strftime(\"%Y-%m-%dT%H:%M:%SZ\", time.gmtime()),\n    \"event\": \"SessionEnd\",\n    \"session_id\": payload.get(\"session_id\"),\n    \"reason\": payload.get(\"reason\", \"unknown\"),\n    \"transcript_path\": payload.get(\"transcript_path\")\n}\n\nwith (reports_dir \u002F \"session-audit.log\").open(\"a\") as log:\n    log.write(json.dumps(record) + \"\\n\")\n```\n\nIts job is to leave a record after the session is gone.\n\n## What the demo should prove\n\nThe included `agent-hooks-demo\u002F` 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\u002Fapi_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\u002F`.\n\n## Where hooks fit with prompts, CI, and review\n\nHooks work best when each layer has a clear job:\n\n- Project instructions: coding style, architecture guidance, naming conventions, testing preferences, and examples.\n- Hooks: required context, pre-action policy, post-action validation, completion gates, and logs.\n- CI: independent verification after the agent produces a diff.\n- Human review: product judgment, tradeoffs, irreversible risk, and final ownership.\n\n![Defense in Depth](.\u002Fdefenseindepth.png)\n\nPutting 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.\n\n## Adoption path\n\nStart with one useful rule rather than a full governance system. A strong first implementation is a pre-action hook that blocks edits to `generated\u002F`, `.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\u002Flast_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.\n\nThis 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.\n\n## The main point\n\nHooks make agent workflows more dependable by moving repeatable rules out of the model’s memory and into code that runs at known lifecycle points.\n\nThat 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.\n\n## Source notes\n\n- Claude Code hooks guide: https:\u002F\u002Fcode.claude.com\u002Fdocs\u002Fen\u002Fhooks-guide\n- Claude Code hooks reference: https:\u002F\u002Fcode.claude.com\u002Fdocs\u002Fen\u002Fhooks\n- Devin for Terminal hooks overview: https:\u002F\u002Fcli.devin.ai\u002Fdocs\u002Fextensibility\u002Fhooks\u002Foverview\n- Devin for Terminal lifecycle hooks: https:\u002F\u002Fcli.devin.ai\u002Fdocs\u002Fextensibility\u002Fhooks\u002Flifecycle-hooks\n- OpenAI Codex hooks documentation: https:\u002F\u002Fdevelopers.openai.com\u002Fcodex\u002Fhooks\n- Cursor hooks documentation: https:\u002F\u002Fcursor.com\u002Fdocs\u002Fhooks\n- Cursor CLI overview: https:\u002F\u002Fcursor.com\u002Fcli\n","该项目提供了一种通过钩子（hooks）机制对代理工作流程进行编程控制的方法。核心功能是允许用户在代理会话的特定生命周期点上附加自定义处理程序，这些处理程序可以接收事件数据、根据可选匹配器或过滤器进行筛选，并执行上下文更新、决策制定或副作用操作。其技术特点在于提供了确定性的控制能力，使得已有的脚本、测试、策略检查等规则可以在预设的生命周期点自动执行，而无需依赖模型的记忆和自愿遵守。适用于需要确保某些行为每次都会被执行的场景，如防止生成文件被编辑、确保修改后运行测试等。","2026-06-11 04:01:57","CREATED_QUERY"]