Agent hooks in Claude Code, Cursor, and Codex explained Claude Code, Cursor and Codex each support agent hooks — scripts the tool runs automatically at fixed moments such as before a shell command or when the agent finishes — and hooks are more reliable than instructions in AGENTS.md files because they execute around the model rather than through it, according to a comparison by Antoine van der Lee. The three tools share the same event-trigger-script-action model but differ in configuration: Claude Code uses ~/.claude/settings.json and .claude/settings.json with PreToolUse, PostToolUse and Stop events, Cursor uses ~/.cursor/hooks.json and .cursor/hooks.json with preToolUse, beforeShellExecution, postToolUse, afterFileEdit and stop, and Codex uses ~/.codex/hooks.json or config.toml with PreToolUse, PostToolUse and Stop. All three block a call via exit code 2 or a JSON deny decision, and a failing hook lets the call proceed unless Claude Code's onFailure is set to "block" or Cursor's failClosed is true. Agent hooks are small scripts that your agent tool runs automatically when something happens, like right before a shell command or when the agent finishes. Claude Code hooks, Cursor hooks, and Codex hooks all work this way, and they are the most reliable way to make sure a rule is followed every time. I use hooks to keep Xcode build logs out of my agent’s context, or to compound learnings automatically. Before, I relied on AGENTS.md files, but in my experience, hooks are way more reliable. In this article, I’ll explain how agent hooks work, how the three tools differ, and how you can set one up yourself. Not long ago, I relied on a rule in my global instructions that told my agent to filter Xcode build output, so it would only see errors, warnings, and test results. It’s a good rule, but it’s still a sentence the agent reads. An agent can forget what it reads, and I experienced that myself: it wasn’t reliable at all. Everything in AGENTS.md , agent skills, and your global instructions ends up in the context of your agent. The agent decides what to do with it, and it usually follows it, but it doesn’t have to. I wrote about moving rules into reusable skills https://www.avanderlee.com/ai-development/agent-skills-replacing-agents-md-with-reusable-ai-knowledge/ before, and the guide on writing an AGENTS.md https://aifundamentals.dev/articles/how-to-write-agents-md/ explains what belongs in there. Neither of them can guarantee a rule is followed. A hook works differently: - An instruction. The agent reads it and can ignore or forget it. It works through the model. - A hook. Your tool runs a script at a fixed moment, every time. It works around the model. This means hooks are for the rules that only pay off when they are followed every time. The instruction still explains what you want, and the hook makes sure it happens. If you know webhooks, the idea is similar: something happens, and something you defined runs. A webhook sends an HTTP request to a URL, while an agent hook most commonly runs a script on your own machine. Every hook has four parts: - The event. A defined moment in the agent’s work, like the agent being about to run a shell command, a turn that ended, or a session that started. - The trigger. A filter on that event. A hook that only cares about shell commands ignores everything else. - The hook. Your script. It receives the details of the event as JSON on stdin. - The action. What your script asks the tool to do with the result, like adding context, changing the command, or denying it. A rule of thumb: a hook that runs before something happens can usually block or change it. A hook that runs after can’t undo what already happened. This makes sense if you think about it, but it’s a great distinction that helps me remember the differences. The idea is the same in all three tools, but names and configuration files differ. Learn the idea once, and check the documentation of your tool for the exact details: Claude Code hooks https://code.claude.com/docs/en/hooks , Cursor hooks https://cursor.com/docs/hooks , and Codex hooks https://developers.openai.com/codex/hooks . | | Claude Code | Cursor | Codex | |---|---|---|---| | User configuration | ~/.claude/settings.json | ~/.cursor/hooks.json | ~/.codex/hooks.json or config.toml | | Project configuration | .claude/settings.json | .cursor/hooks.json | .codex/hooks.json or config.toml | | Before a tool runs | PreToolUse | preToolUse , beforeShellExecution | PreToolUse | | After a tool ran | PostToolUse | postToolUse , afterFileEdit | PostToolUse | | Agent finished | Stop | stop | Stop | | Block a call | Exit code 2 or JSON permissionDecision: "deny" | Exit code 2 or JSON permission: "deny" | Exit code 2 or JSON permissionDecision: "deny" | | Hook fails | Call goes ahead, unless onFailure is "block" | Call goes ahead, unless failClosed is true | Reported as a hook failure, so test what your event does | There are a few more differences worth knowing: - Claude Code and Codex use the same configuration shape: an event, a matcher, and a list of handlers. Cursor uses a flatter hooks.json with a version and a command per hook. - The tool you filter on is named Bash in Claude Code and Codex, and Shell in Cursor. - Codex asks you to review and trust a hook before it runs, and it only loads project hooks from a trusted project. Cursor also runs project hooks only in a trusted workspace. - All three tools have more events than the table shows, like the start and end of a session, before a prompt is sent, and before and after a subagent runs. Raw xcodebuild output is long, and your agent rarely needs all of it. I covered the numbers in How to reduce token usage in Claude Code, Codex, and Cursor https://www.avanderlee.com/ai-development/reduce-token-usage-claude-code-codex-cursor/ . Piping the build through xcsift https://github.com/ldomaradzki/xcsift keeps errors, warnings, and test results and drops the rest. I started creating this hook after I found out that my raw build logs were often still ending up in my context. I needed a more reliable way to filter xcodebuild output, without maintaining scripts across all my projects. My own hook rewrites the command in Cursor and Claude Code. The version below is simplified: it blocks a raw build and tells the agent how to run it instead, which works the same way in all three tools. Save it as ~/.agents/hooks/require-xcsift.sh and make it executable with chmod +x : bash /bin/bash input=$ cat command=$ jq -r '.tool input.command // empty' <<< "$input" Only act on raw build and test commands that aren't filtered yet. if "$command" == xcodebuild \ && "$command" =~ ^| :space: build|test :space: |$ \ && "$command" = xcsift ; then message="Run it again as: set -o pipefail; $command 2 &1 | xcsift -f toon -w" Cursor answers with JSON, Claude Code and Codex read stderr after exit code 2. if jq -e 'has "cursor version" ' <<< "$input" /dev/null; then jq -n --arg message "$message" '{permission: "deny", agent message: $message}' exit 0 fi echo "$message" &2 exit 2 fi exit 0 The script leaves everything else alone, including commands that already use xcsift and xcodebuild -list . If jq is missing or the input looks unexpected, it exits without blocking anything. That’s called failing open , and it’s the right choice for a convenience hook. A broken hook shouldn’t stop you from working. Register it in Claude Code in ~/.claude/settings.json . Codex uses the same shape in ~/.codex/hooks.json : { "hooks": { "PreToolUse": { "matcher": "Bash", "hooks": { "type": "command", "command": "~/.agents/hooks/require-xcsift.sh", "timeout": 5 } } } } In Cursor, add it to ~/.cursor/hooks.json : { "version": 1, "hooks": { "preToolUse": { "command": "~/.agents/hooks/require-xcsift.sh", "matcher": "Shell", "timeout": 5 } } } You don’t need to write this configuration by hand. Your agent can read the documentation of your tool and do the setup. Ask it to explain before it writes: I want to add a hook to the agent tool I'm using right now. First read the hooks documentation for this tool and tell me which events it supports. Then help me with this idea: