diffox
Blog

What are hooks in coding agents, and how do you write them?

A coding agent doesn’t always follow a rule in its rules file, but a hook runs every time. Here is how to write hooks in five tools, with examples.

ShareXLinkedIn

Summary. A hook is a command you write that a coding agent runs on its own at set points. Whether it runs is not up to the agent, so you use it for rules that must always hold. Claude Code, Codex, Cursor, Gemini CLI and GitHub Copilot all have hooks. We checked every fact in each tool’s own documentation on 18 October 2026; the examples come from the repository we work in every day. We added Google’s Antigravity CLI on 28 October 2026.

  • A rules file alone is not enough. A rule you write in AGENTS.md or CLAUDE.md is information for the agent, with no guarantee that it will be followed. All five tools let a hook step in before the agent runs a command.
  • Blocking works the same way everywhere; errors do not. If a hook exits with code 2, all five tools block the action. On another error code, such as 1, Claude Code, Cursor and Gemini CLI go ahead with the action, while Copilot blocks it. Codex’s documentation does not say.
  • A hook that hangs switches the rule off. If a hook times out, Claude Code, Cursor and Copilot go ahead with the action. You can change that with the onFailure setting in Claude Code and failClosed in Cursor. The Codex and Gemini CLI documentation does not say.
  • We use three hooks in our own repository. One reports the state of the working directory when a session opens, one blocks a merge made without approval, and one warns the agent when the branch changes.

Why isn’t a rules file enough?

5tools, read in their own documentation
2exit code that blocks the action in all five tools
33events you can attach a hook to (Claude Code)
3hooks we use in our own repository

There are two ways to give a coding agent a rule. The first is a rules file: you write “ask me before you merge” in AGENTS.md, CLAUDE.md or GEMINI.md. The second is a hook: you turn the same rule into a small script that the agent runs on its own at a set point.

Claude Code’s documentation puts the difference most clearly: “Claude treats them as context, not enforced configuration. To block an action regardless of what Claude decides, use a PreToolUse hook instead.” In other words, a rules file is only information for the agent, and the agent may not always act on it. If you want an action blocked whatever the agent decides, you use a hook.

As of 18 October 2026, all five coding tools have hooks: Claude Code, OpenAI’s Codex, Cursor, Google’s Gemini CLI and GitHub Copilot (the CLI and the cloud agent). They all work the same way: a hook is attached to an event, receives the event’s details as JSON and, when it needs to, blocks a command before it runs.

The examples come from the repository we use every day to build diffox. We use Claude Code there; what we describe is how the team works, not a feature of the product.

Where do you put hooks in each of the five tools?

In every tool, hooks go in a settings file; the tools mostly differ in the name of the file and of the event. The event most often used to enforce a rule is the one that fires just before the agent uses a tool (to run a command or edit a file, for example):

Hooks in five coding tools: the settings file, the event that fires before a tool is used, and how to block the action
ToolSettings file (project · user)Before a tool is usedTo block
Claude Code.claude/settings.json · ~/.claude/settings.jsonPreToolUseexit 2 or permissionDecision: "deny"
Codex.codex/hooks.json · ~/.codex/hooks.jsonPreToolUseexit 2 or permissionDecision: "deny"
Cursor.cursor/hooks.json · ~/.cursor/hooks.jsonpreToolUse; for shell commands, beforeShellExecutionexit 2 or permission: "deny"
Gemini CLI.gemini/settings.json · ~/.gemini/settings.jsonBeforeToolexit 2 or decision: "deny"
GitHub Copilot.github/hooks/*.json · ~/.copilot/hooks/*.jsonpreToolUseexit 2 or permissionDecision: "deny"

Source: each tool’s own hooks documentation, 18 October 2026. A JSON decision is given in the output the hook prints to stdout. Codex also reads the same settings from config.toml. In Copilot the user file applies only to the CLI; the cloud agent reads the repository’s file from the default branch.

When the event fires, your hook receives the details as JSON: which tool is being called, and with what input. In Claude Code, the input before a Bash command looks like this:

EXAMPLE. The hook’s input is a JSON object. Claude Code · stdin · PreToolUse: { /   "session_id": "abc123", /   "cwd": "/home/dev/project", /   "hook_event_name": "PreToolUse", /   "tool_name": "Bash", /   "tool_input": { /     "command": "gh pr merge 42 --squash" /   } / }. tool_name: The tool the matcher checks. tool_input.command: The command to run. Exit code: The decision: 2 blocks.
A shortened example of the JSON a PreToolUse hook receives before Claude Code’s Bash tool runs. Field names from Claude Code’s hooks documentation, 18 October 2026; in the other tools the fields have other names.

The tools can also read each other’s files: Copilot also reads the repository’s .claude/settings.json, Cursor can load Claude Code hooks, and Gemini CLI sets the CLAUDE_PROJECT_DIR variable so that scripts written for Claude Code keep working. So you can write a hook once and use it in several tools. Still, event names and output fields differ, so test the hook in each tool on its own.

On 18 June 2026, Google stopped serving Gemini CLI to free users and to Google AI Pro and Ultra subscribers and pointed them to Antigravity CLI; with a paid API key or a company licence you can keep using Gemini CLI. Google’s announcement says Antigravity CLI has hooks too; we read its documentation on 28 October 2026. Hooks go in .agents/hooks.json for a project and in ~/.gemini/config/hooks.json for the user. The event that fires before a tool is used is called PreToolUse, as in Claude Code. To block the action, the hook returns decision: "deny" in JSON. The timeout is 30 seconds by default; the documentation does not say what exit codes do.

How does a hook block an action?

A hook gives its decision in one of two ways: with its exit code, or with JSON it prints to stdout. In all five tools, if a hook exits with code 2, the action is blocked and the reason the hook wrote to stderr is passed to the agent. The tools behave differently when a hook exits with another error code or times out:

What happens when a hook that runs before a tool is used exits with an error code other than 2, or times out
ToolError code other than 2TimeoutSetting that blocks instead
Claude CodeThe action goes aheadThe action goes ahead"onFailure": "block"
CodexNot stated in the documentationNot stated in the documentation—
CursorThe action goes aheadThe action goes ahead"failClosed": true
Gemini CLIA warning, and the action goes aheadNot stated in the documentation—
GitHub CopilotThe action is blockedThe action goes ahead—

Source: each tool’s hooks documentation, 18 October 2026. “Not stated in the documentation”: in the pages we read that day, we found no sentence that describes this case. The Claude Code setting needs version 2.1.295 or later. On Windows without Git Bash, a shell-form PreToolUse command hook that fails can block the action even without onFailure.

Start with the error-code column. On Unix, 1 is the usual code for a failure. Claude Code, Cursor and Gemini CLI treat it as an error that does not block the action; Copilot blocks on the same code. That is why Claude Code’s documentation tells you to use exit 2 in hooks that enforce a rule. Gemini CLI, on the other hand, recommends exiting 0 and printing JSON even when you block on purpose; if the hook prints anything to stdout that is not JSON, it lets the action through.

The problem in the timeout column is harder to spot: if the hook’s path is wrong or the script hangs, it is as if the rule did not exist. Where the action must be blocked no matter what, use the onFailure setting in Claude Code and failClosed in Cursor.

The example below is a simplified Claude Code version of the merge gate in our repository. First, the settings file:

.claude/settings.json
{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/merge-gate.sh"
          }
        ]
      }
    ]
  }
}

Then the script:

.claude/hooks/merge-gate.sh
#!/bin/bash
# The input arrives on stdin as JSON. If it cannot be read, the action is blocked.
COMMAND=$(jq -r '.tool_input.command // empty') || {
  echo "The hook could not read its input; the action was blocked." >&2
  exit 2
}

case "$COMMAND" in
  *"MERGE_APPROVED=1 gh pr merge"*) exit 0 ;;  # the approval is in the command
  *"gh pr merge"*)
    echo "Merging is the project owner's call. Ask first." >&2
    exit 2 ;;                                 # the action is blocked
esac
exit 0                                        # no objection

When the script exits with 2, Claude Code does not run the command and passes the sentence you wrote to stderr on to the agent. You can give the same decision by exiting 0 and printing JSON: the permissionDecision field takes deny, ask or allow. PreToolUse hooks run before the permission settings are checked; a deny holds even in bypassPermissions mode, which skips every permission prompt. Codex does not support ask yet: the hook counts as failed and the action goes ahead.

Which hooks do we use in our own repository?

Our repository’s .claude/settings.json registers 3 hooks. We wrote each of them after a mistake that had already happened to us, so that it would not happen again.

The three hooks in our repository. One informs, one blocks, one warns. Git runs the fourth. SessionStart Directory status: Reports the branch, uncommitted files and worktrees. PreToolUse · Bash Merge gate: Blocks the merge command without approval. PreToolUse · Edit|Write Branch change warning: Warns the agent if the branch or commit moved. git · pre-push Git hook: Runs before a push; git runs it, not the agent.
The three Claude Code hooks and one git hook in our repository. The third runs before the Edit|Write|NotebookEdit tools. Source: the repository’s settings file and hook scripts.

Session start. When a session starts, the SessionStart hook reports the current branch, how many files are uncommitted and which worktrees are open, then gives a verdict: “you can work in this directory” or “open a separate worktree first”. The rule was already in CLAUDE.md. But one session read it, did not apply it to its own situation, and planned changes in a directory that switched branches three times in forty minutes. A rule that depends on the agent remembering it protects nothing; the hook asks the question for the agent.

Merge gate. In our repository, no pull request is merged until the project owner approves it. But once the tests pass, an agent may go on to run gh pr merge as the next step. The PreToolUse hook that runs before the Bash tool blocks that command with code 2 and tells the agent to ask for approval first. Once approval comes, the command is run again with an environment variable in front of it, so the approval sits in the session transcript right next to the merge. The hook also blocks when it cannot read its input: its first version exited 0 and let through every command it could not read.

Branch change warning. The verdict given at session start describes that moment, and it can go stale: if one of two sessions opened on the same branch switches to a new branch, the other one’s working directory changes under it. The second PreToolUse hook runs before the file-editing tools and, before every edit, compares the branch and commit with what the session was last told. If they differ, it sends the agent a note that begins with this line: “drift: this checkout MOVED since the session was last told what it is”. It does not block the action; it passes the note in the additionalContext field and exits 0.

A git hook is a different thing. The same repository also has a pre-push hook, which refuses a direct push to the protected branches. Git runs it, not the coding agent, so it covers everyone who pushes from that clone, agents included, whatever tool they use. Both kinds run only on your own machine: --no-verify skips pre-push, so neither is a substitute for checks on the server.

How is a hook different from a skill or a rules file?

All three shape how the agent behaves, but they come into play at different times. Here is the comparison in Claude Code’s documentation:

CLAUDE.md, skills and hooks: when each comes into play, whether it applies every time, how much context it takes and what it is for
CLAUDE.mdSkillHook
When it comes into playAt the start of every sessionWhen you type /name, or when Claude finds it fits the taskEvery time its event fires
Applied every time?No, it is information for the agentNo, Claude interprets itYes, it runs every time the event fires
Context it takesAll of it, on every requestIts description every session, all of it when usedNone, unless it returns output
What it is for“Always do X” rules, commandsReusable workflows, reference materialFormatting, blocking a dangerous command, notifications

Source: Claude Code’s documentation, the “Extend Claude Code” page, 18 October 2026.

In short: use a skill when the agent should decide how to apply the steps, and a hook when the action must happen the same way every time. A release checklist is a skill; formatting every file you save is a hook. As the same page puts it, an instruction in a rules file or a skill is a request, not a guarantee. Don’t leave rules you can’t undo once they are broken (a push to the wrong branch, an early merge) to an instruction alone.

Hooks are not only defined in settings files; skills and subagents can carry hooks in their own definitions too. In Claude Code, a subagent’s hooks apply only while that subagent runs; a skill’s hooks keep running until the session ends, once the skill has been invoked. In Copilot, a custom agent’s hooks run only while that agent is selected.

Don’t turn every rule into a hook

A hook runs every time, and a badly written hook goes wrong every time. Before you write one, check these:

  • Keep information that doesn’t change in the rules file. Claude Code’s documentation recommends CLAUDE.md for static information. A hook is better suited to passing on the current state (which branch you are on, what the last CI run said).
  • Don’t leave decisions that need judgement to a hook. A simple text match cannot answer “does this change need a test?”; a step like that belongs in a skill.
  • Don’t block without need. Two of our three hooks only inform; they do not block. A hook that blocks work for no good reason soon gets switched off instead of fixed.
  • A hook that matches text is not a security control. Our merge gate cannot catch a merge command hidden in a variable or inside eval. It stops an agent that forgot to ask, not one that sets out to get around the rule. Codex’s documentation also calls hooks “a useful guardrail, not a complete enforcement boundary”. For a hard allow or deny, Claude Code’s documentation recommends permission rules.
  • Review someone else’s hook like any other code. Command hooks run with your user’s permissions. Codex does not run a hook that comes with a repository until you have reviewed and trusted it; Cursor runs project hooks only in workspaces you trust; and Gemini CLI treats a hook that has changed as a new, untrusted hook.

Hooks run on your machine, before an action happens; they do not check whether the code being written is correct. That is the job of code review: when diffox reviews a pull request, it also reads the rules that must be followed in the repository’s rules file (AGENTS.md, or CLAUDE.md if there is none) as review rules.

Frequently asked questions

What is a hook, and which coding tools have them?

A hook is a command you write that a coding agent runs on its own at set points. As of 18 October 2026, Claude Code, Codex, Cursor, Gemini CLI and GitHub Copilot all have them. You write in a settings file which command runs on which event. Whether it runs is not up to the agent, so the rule is applied every time.

How does a hook block a command in Claude Code?

If a hook on the PreToolUse event exits with code 2, Claude Code does not run the command and passes the reason the hook wrote to stderr on to the agent. Exiting 0 means “no objection”; any other code, such as 1, counts as an error that does not block the action. A hook can also exit 0 and print JSON: the permissionDecision field is deny, ask or allow.

How do you use hooks in Cursor?

Hooks go in .cursor/hooks.json for a project and ~/.cursor/hooks.json for your user. preToolUse runs before the agent uses a tool, and beforeShellExecution before it runs a shell command. If the hook exits with 2 or returns permission: "deny" in JSON, the action is blocked. On any other error code the action goes ahead; you can change that with failClosed: true. Project hooks run only in workspaces you trust.

Do Codex and Gemini CLI have hooks?

Yes. In Codex, hooks go in .codex/hooks.json or config.toml and are on by default; Codex does not run a hook that comes with a repository until you have reviewed and trusted it. In Gemini CLI, hooks live in settings.json, and the event that fires before a tool is used is called BeforeTool. In both, a hook that exits with code 2 blocks the action.

How do you define hooks in GitHub Copilot?

You put a JSON file in the repository’s .github/hooks/ folder or, for personal hooks in Copilot CLI, in ~/.copilot/hooks/. The file has version: 1 and a hooks object. preToolUse runs before a tool is used; if the hook prints permissionDecision: "deny" to stdout or exits with 2, the action is blocked. The cloud agent reads the hooks file from the repository’s default branch.

What happens if a hook crashes or times out?

In most tools the action goes ahead. In Claude Code, a hook that fails or times out does not block the action; in Cursor, a failed hook lets the action through too; in Copilot, a timeout always lets the action through. To change this, Claude Code has the "onFailure": "block" setting and Cursor has "failClosed": true. In Copilot, though, a preToolUse hook that exits with an error code other than 2 blocks the action.

What is the difference between a hook and a skill?

A hook is a command the tool runs when a given event happens, and it runs every time. A skill is a file of instructions the agent reads and follows (SKILL.md in Claude Code): it loads when you call it by name or when the agent finds it fits the task, and the agent decides what to do. Use a hook when the action must happen the same way every time, and a skill when the agent needs to adapt the steps to the situation.