Build Deterministic Lifecycle Hooks for Coding-Agent Tool Calls

Last reviewed: 2026-09-01

Direct answer

Coding agent lifecycle hooks are small commands that run at predictable points around an agent action. A pre-tool hook can inspect a proposed file write, shell command, or MCP call before it executes. A post-tool hook can record the result, run a formatter, or launch a focused check. Session and completion hooks can load context, emit notifications, or confirm that the run left the evidence a reviewer needs.

The important design choice is to make the hook a policy adapter rather than another autonomous assistant. The hook should receive a defined event, normalize the fields it needs, return an explicit decision, and finish within a bounded time. Keep model judgment out of the hard gate whenever a deterministic rule is enough. A path classification, command allowlist, file-size limit, or test-result requirement is easier to explain and replay than a second model deciding whether the first model looked safe.

Use a four-stage flow:

  1. Start. Create a run identifier, record the hook-policy version, and load only the context the agent needs. Do not copy prompts, environment variables, or file contents into the log by default.
  2. Before the tool. Match on the tool and operation class. Allow read-only work, deny known destructive patterns, and route ambiguous changes to a human decision when your runtime supports that result. Return the reason code with the decision.
  3. After the tool. Capture the exit status, duration, and a digest of the normalized input and output. Run a formatter or focused test only when the event contract says the tool actually changed something.
  4. Stop. Verify that required postconditions were met: the hook itself returned valid output, the expected checks ran, and the run record is complete enough to review.

A portable event envelope can be as small as this:

event=before_tool
tool_name=write_file
operation_class=workspace_write
decision=allow|deny|review
reason_code=policy_rule_id
policy_version=policy-2026-09
run_id=[RUN_ID]
input_digest=[HASH]

The happy path is straightforward: a coding agent proposes a change, the before-tool hook classifies it, the action is allowed, the after-tool hook records a sanitized result, and the stop hook confirms that the run is reviewable. The error path must be just as deliberate. If input is malformed, a matcher is wrong, a hook times out, or a downstream scanner is unavailable, return a clear reason and choose a documented default. Fail closed for destructive operations; allow a narrowly defined fail-open mode only for advisory telemetry. Then surface the failure to the operator instead of silently letting the agent continue.

This pattern is supported across the three current runtimes checked for this guide. Anthropic’s Claude Code hooks guide describes deterministic lifecycle commands that can format files, block commands, inject context, and enforce project rules. Google’s Gemini CLI hooks documentation exposes before- and after-tool events, explicit JSON output, and exit-code behavior. Cursor’s hooks documentation describes JSON-over-stdio hooks for tools, shell commands, files, MCP, subagents, and cloud agents. The names differ, but the operational contract is the same: intercept, decide, observe, and preserve evidence.

Who this is for

This guide is for platform engineers, repository maintainers, security engineers, and team leads who let coding agents edit real workspaces. It is especially useful when the same policy must apply to interactive sessions and unattended or cloud runs, or when reviewers need to understand why a tool call was allowed or blocked.

It is not a replacement for sandboxing, branch protection, or least-privilege credentials. Hooks run inside the agent’s execution environment and inherit the permissions that environment grants. Treat them as a deterministic control layer that complements those boundaries. A hook cannot make an over-privileged runtime safe if the runtime can simply be bypassed.

Key takeaways

  • Define one normalized event contract even when each agent uses different event names.
  • Put hard policy in command hooks and reserve model-based checks for cases that genuinely need judgment.
  • Log decisions and digests, not raw prompts, source files, command arguments, headers, or environment values.
  • Decide fail-open and fail-closed behavior per operation class before production use.
  • Test matcher coverage, malformed output, timeouts, duplicate hooks, and read-only startup conditions.
  • Version the hook policy and include that version in every run record so a later reviewer can reproduce the decision.

Sources checked

  • Automate actions with hooks - Claude Code Docs explains that user-defined shell commands run at lifecycle points and gives examples for post-edit formatting, pre-command blocking, notifications, context injection, and project-rule enforcement.
  • Gemini CLI hooks documents synchronous hooks, BeforeTool and AfterTool events, matcher behavior, strict JSON on standard output, exit code 2 for a system block, configuration precedence, and the risk of arbitrary code running with user privileges.
  • Hooks - Cursor Docs documents JSON-over-stdio hooks, pre- and post-tool stages, shell and MCP controls, subagent events, cloud-agent support, and the distinction between command-based and prompt-based hooks.

These are public vendor documentation pages, refetched for this article on 2026-09-01. They are used for runtime behavior and configuration semantics. The workflow recommendations below are implementation guidance derived from those documented capabilities, not claims that every runtime behaves identically.

Contract details to verify

Before committing a hook to a shared repository, verify six details in the runtime you actually deploy.

Event timing and names. Claude Code uses names such as Notification and PostToolUse. Gemini CLI separates BeforeAgent, BeforeModel, BeforeToolSelection, BeforeTool, AfterTool, and other lifecycle events. Cursor uses names such as preToolUse, beforeShellExecution, afterFileEdit, beforeMCPExecution, and stop. Build a small translation table in your adapter and test the timing with a harmless operation. A hook that runs after a write cannot prevent that write.

Input and output. Gemini CLI requires clean JSON on standard output and directs debugging to standard error. Cursor also uses JSON over standard input and output for command hooks. Keep diagnostics on standard error, validate the response before returning it, and make an empty or malformed response an explicit error. Claude Code supports structured hook output as well, so the same discipline keeps behavior portable.

Exit codes and decisions. Confirm which exit code blocks an action, which code is advisory, and whether a returned decision can rewrite arguments or results. Gemini CLI and Cursor both document exit code 2 as a blocking result, while other nonzero outcomes can be warnings or failures depending on the hook type. Encode this mapping in tests instead of relying on memory.

Matchers and precedence. Tool matchers may be regular expressions while lifecycle matchers may be exact strings. Empty or wildcard matchers can unintentionally cover every operation. Also check how project, user, system, extension, team, and enterprise settings merge. Gemini CLI fingerprints changed project hooks and warns before running an untrusted variant; Cursor documents separate project, user, team, and enterprise sources. Record the effective configuration hash in your run evidence.

Runtime availability. Cursor cloud agents do not run every IDE lifecycle hook and can begin in a read-only environment. A session-start control that works locally may not protect the first cloud write. Gemini CLI executes hooks with a sanitized environment and exposes a limited set of paths and session variables. Test from the actual execution surface, not only from a developer laptop.

Evidence shape. Keep a compact, sanitized record that lets a reviewer answer what happened without recovering sensitive content. A useful minimum is:

timestamp=[UTC]
run_id=[RUN_ID]
hook_name=policy-gate
event=before_tool
tool_name=write_file
operation_class=workspace_write
decision=deny
reason_code=protected_path
policy_version=policy-2026-09
duration_ms=[INTEGER]
exit_code=2
input_digest=[HASH]
output_digest=[HASH]

Do not include raw prompts, file bodies, shell arguments, authorization headers, environment dumps, or credential-related values. If an operator needs the underlying artifact, store it in the repository’s existing protected evidence system and reference it with a short record identifier. The site’s operational decision ledger gives a useful companion pattern for recording why a decision was made, while telemetry and log review covers how to inspect run records without losing their provenance.

Failure modes

The hook never fires. A tool name, event name, or matcher may be wrong. Start with a wildcard in a disposable workspace, print only a short diagnostic to standard error, and then narrow the matcher after observing the real event envelope.

The hook blocks everything. A strict parser may treat an absent field as dangerous, or two hooks may disagree. Make the reason code visible, test each hook in isolation, and document precedence. Avoid broad deny rules that do not identify the protected resource.

Standard output is polluted. A debug echo, formatter banner, or stack trace can make a JSON response invalid. Send diagnostics to standard error, wrap the final response in a schema check, and add a test that injects an unexpected warning.

A timeout becomes a silent allow. Network calls and package scanners can stall. Set a short timeout, return a deterministic timeout reason, and fail closed for writes, deployments, permission changes, and other side effects. If advisory logging is allowed to fail open, mark the record as incomplete so the operator can see the gap.

The hook creates a loop. A post-edit formatter can trigger another edit event, or a retrying stop hook can schedule itself repeatedly. Add an event depth or correlation field, cap retries, and ignore events generated by the hook’s own temporary files.

Sensitive data leaks into evidence. Tool input often contains paths, snippets, or environment-derived values. Hash or classify those fields before logging, keep retention short, and review the log schema like an API. Never assume that a private repository path is harmless in a shared telemetry system.

Cloud and local behavior diverge. A hook available in an IDE may not run in a cloud agent’s read-only phase, and user-level files may not be mounted in the cloud VM. Check the runtime’s supported-event table and keep the critical policy in a project-level configuration that the execution surface actually loads.

FAQ

Are lifecycle hooks the same as repository instructions?

No. Instructions tell the model what to consider; hooks execute deterministic code at a lifecycle boundary. Use instructions for intent and context, and hooks for checks that must happen even when the model forgets or misinterprets a rule.

Should every tool call be blocked until a person approves it?

Usually not. That creates approval fatigue and encourages broad permanent exceptions. Allow low-risk reads and reversible local checks, then apply stronger gates to destructive writes, production operations, permission changes, and external side effects. Keep the classification explicit and review it as the repository changes.

When should a prompt-based hook be used?

Use it when the condition genuinely requires judgment, such as reviewing a nuanced policy explanation. Keep deterministic checks in command hooks, set a timeout, and define what happens when the evaluator is unavailable. A model-based hook should not be the only barrier around an irreversible operation.

What is the smallest useful test suite?

Test one allowed read, one allowed reversible write, one denied protected path, one malformed input, one timeout, one duplicate configuration, and one run on the real cloud or headless surface. Assert the decision, reason code, exit behavior, and sanitized log fields.

How often should the policy be revisited?

Review it when the agent runtime changes its event schema, when a new tool or MCP server is added, and after every blocked-operation incident. Keep the policy version in the evidence so an older decision can be interpreted against the rules that were active at the time.

Reader next step

Create a disposable branch and implement one dry-run before-tool hook for a single high-impact operation, such as writes under a protected directory. Have it emit the sanitized fields above, return an explicit allow or deny decision, and write all diagnostics to standard error. Then add a post-tool hook that records duration and exit status without copying content.

Exercise the happy path with a harmless file change. Exercise the error path by sending malformed input, forcing a timeout, and running the same configuration from the cloud or headless surface your team uses. Record each result in the operational decision ledger and inspect it with the telemetry and log review checklist . Once those checks are repeatable, expand the matcher set one operation class at a time and keep the policy versioned with the repository.