Hooks for Claude Code and Codex
Claude Code hooks and Codex hooks on a Mac. What hooks do, with examples for notifications, formatting code and protecting files. How to use hooks safely.
AI coding agents provide a feature named "hooks" to automatically run commands. Claude Code and Codex both support hooks. Use a hook when you need to be sure a command runs every time, without depending on an agent's judgment. Anthropic calls this "deterministic control." An instruction in CLAUDE.md or AGENTS.md is a request the agent can overlook, but a hook is applied programmatically by the AI harness so it can't be skipped.
I'll provide an example of a hook that notifies you when the agent needs attention, with examples for Claude Code and Codex. I also explain the risks, because a hook runs with all the permissions of your user account. I tested with macOS 27 using Claude Code 2.1.296, October 2026.
Hooks are one part of setting up a Mac for AI coding. See the complete Mac setup checklist for the rest.
Before you get started
I recommend using our free app to install Claude Code and Codex. It is a FREE Mac app that installs the developer tools and AI coding agents you need, configures your shell, and verifies your setup is working. Here are all the details about the app before you download. Then download it:
First steps for hooks
You'll need Claude Code or Codex. Follow the steps to Install Claude Code on Mac, or install Codex with Homebrew.
You'll edit settings files in a text editor. Their folders start with a dot, so the Finder hides them until you show hidden files on Mac.
Many hook examples use jq, a small tool that picks out values from the JSON (structured text) that a hook receives. macOS 15 or later includes jq in /usr/bin. Check that you have it:
$ jq --version
You'll see a version such as jq-1.7.1-apple. If you don't have it, install it using the Homebrew command brew install jq.
What hooks can do
Use a hook for something that must happen every time, or must never happen. These are the most common uses I found in public hook files on GitHub:
- Block dangerous commands and protect secret files - stop the agent from running
rm -rf(a command that deletes files and folders without asking) or changing a.envfile of passwords and API keys. - Format or check code - run a formatter (a tool that tidies the layout of code) such as Prettier on each file the agent changes.
- Add context - give the agent the current Git status or project notes when a session starts.
- Keep a log - record every command the agent runs.
- Run the tests - keep the agent from saying it is done while tests fail.
- Send a notification - tell you when the agent finishes or needs your permission.
A hook can also stop the agent. When a hook's command ends with exit code 2 (a program's signal when it finishes), the agent cancels what it was about to do and reads the reason the hook gives.
Hooks or instructions or skills
You can control and guide an AI agent with agent instructions or skills or hooks. Use an agent instruction for guidance, a skill for repeatable tasks or procedures, and a hook for anything that must happen every time.
- Agent instructions (
CLAUDE.mdandAGENTS.md) - guidance the agent reads in every session and usually follows. Developer Setup for Agent Instructions explains how to write them. - Skills - instructions for one kind of task, which the agent loads when that task comes up. Learn when to use one in Developer Setup for Agent Skills.
- MCP servers - connections to other apps and services, described in MCP Servers on Mac.
- Hooks - commands that run at set moments, whatever the agent decides.
A hook runs outside the AI model, so it adds nothing to the agent's context (its working memory) unless it sends text back.
Add a Claude Code notification
Here's an example of a hook to try.
If you use Claude Code in the Code tab of the Claude desktop app, you don't need the hooks in this section. The hooks below are for Claude Code in a terminal application (Claude Code CLI). The desktop app shows its own notification, with a sound, when Claude needs your permission, asks you a question or finishes a task, and you can change these alerts in the app's settings.
In a terminal application, Claude Code often stops to ask your permission before it runs a command or edits a file. If you've switched to another app, the agent waits and you don't know it, unless a notification hook tells you.
Before you add the hook, allow Script Editor to send notifications. macOS sends these notifications as the Script Editor app, and until Script Editor is allowed to send them, nothing appears and nothing tells you why (Anthropic's hooks guide documents this). To fix it, run this test once in a terminal application. It uses osascript, the Mac's built-in command for running AppleScript, to show a notification:
$ osascript -e 'display notification "test"'
Then open System Settings > Notifications, find Script Editor, and turn on Allow Notifications.
Next, open ~/.claude/settings.json in your text editor, or create it if it doesn't exist. Add this hook from Anthropic's guide, which runs the same kind of osascript command:
{
"hooks": {
"Notification": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude Code needs your attention\" with title \"Claude Code\"'"
}
]
}
]
}
}
If the file already has other settings, add the "hooks" section beside them, with a comma between. If it already has a "hooks" section, add "Notification" inside it. One missing or extra comma breaks the file, and every hook in it stops working.
Type /hooks in Claude Code CLI to check that the hook is listed under Notification. If it isn't, restart Claude Code. The /hooks command doesn't work in Claude Desktop.
To test the hook, ask Claude Code to do something that needs your permission, then switch to another app. The notification arrives about six seconds after Claude Code asks. When Claude Code has finished and is waiting for your next prompt, it waits about a minute before it notifies you.
Play a sound when Claude Code finishes
To hear a sound each time Claude Code finishes a reply, add a Stop hook beside the Notification hook. It runs afplay, the Mac's built-in command for playing an audio file:
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "afplay /System/Library/Sounds/Glass.aiff"
}
]
}
]
The Mac's alert sounds are in /System/Library/Sounds/, from Basso to Tink. You can also add a sound to the notification itself, with sound name "Glass" at the end of the AppleScript, or hear a spoken message with the say command, as in say "Claude is done".
Where Claude Code keeps hooks
Claude Code reads hooks from several settings files and adds them all together:
~/.claude/settings.json- your own hooks, for every project..claude/settings.jsonin a project - hooks for that project, shared through Git with everyone who works on it..claude/settings.local.jsonin a project - hooks for that project, just for you.
Hooks can also come from plugins, skills and subagents (helper agents for tasks), and on a company Mac, from managed settings you can't change. Claude's desktop app reads the same settings files, and Anthropic says hooks in them run there too (for the differences between the two apps, see Claude Code and Claude Desktop Compared). The desktop app's Code tab runs its own copy of Claude Code, which the app keeps updated, so its version can differ from the claude command in your terminal application.
Hook events, matchers and commands
Every hook has three parts, nested inside the "hooks" section:
- The event - when the hook runs, such as
PreToolUse, before the agent uses a tool. - The matcher - which tool the hook is for, such as
Bashfor terminal commands orEdit|Writefor file changes. An empty matcher matches everything. - The command - what runs.
Claude Code has 33 events, all listed in Anthropic's hooks reference. These are the ones you'll use most:
SessionStart- a session starts or resumes.UserPromptSubmit- you send a prompt.PreToolUse- the agent is about to use a tool. The hook can block it.PermissionRequest- Claude Code is about to ask your permission.PostToolUse- a tool has finished.Notification- Claude Code needs your attention.Stop- the agent finishes its reply.PreCompact- Claude Code is about to shorten a long conversation.SessionEnd- the session ends.
Matchers are case-sensitive and match a tool's exact name, so bash matches nothing. A matcher of Edit alone misses new files, which the agent creates with its Write tool, so use Edit|Write.
When a hook runs, Claude Code sends it the details as JSON on its standard input (the channel a command reads its input from). The details include the folder, the event, the tool and what the tool is about to do. Hooks often read them with jq. The command jq -r '.tool_input.file_path' gets the file being edited, and jq -r '.tool_input.command' gets the command about to run.
Format files after every edit
This hook from Anthropic's guide runs Prettier, a popular formatter for JavaScript, CSS and similar files, on each file the agent changes. It needs Node.js, which provides npx, and if the project doesn't include Prettier, npx downloads it the first time. Put it in a project's .claude/settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
}
]
}
]
}
}
For other languages, use your project's formatter, such as Ruff for Python.
Block a command or protect a file
A PreToolUse hook can stop the agent before it acts, but only when its script ends with exit code 2. Any other exit code lets the action go ahead, including exit code 1, the usual code for an error. This script, adapted from Anthropic's guide, blocks edits to .env files and to Git's own folder:
#!/bin/bash
# Block edits to .env files and anything in the .git folder
file=$(jq -r '.tool_input.file_path // empty')
case "$file" in
*/.env | */.env.* | */.git/*)
echo "Blocked: $file is a protected file" >&2
exit 2
;;
esac
exit 0
Save it in your project as .claude/hooks/protect-files.sh, and make it executable so it can run as a program:
$ chmod +x .claude/hooks/protect-files.sh
Then register it in the project's .claude/settings.json. $CLAUDE_PROJECT_DIR stands for the project's top folder, so Claude Code finds the script wherever it is working:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
}
]
}
]
}
}
With exit code 2, Claude Code passes the message the script writes to standard error (the >&2 part, which sends text to the error channel) to the agent, so the agent knows why it was blocked and changes course. When I tested this hook, the agent stopped at the .env file, reported that the hook had blocked it, and went on to the next file. With exit 1 in place of exit 2, the hook reported "Blocked" and Claude Code wrote the file anyway.
Log every command
A log of every terminal command shows you what the agent does, which helps you decide what to block. This PreToolUse hook from Anthropic's guide adds each command to a log file before the command runs:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "jq -r '.tool_input.command' >> ~/.claude/bash.log"
}
]
}
]
}
}
Test your hooks
Type /hooks in Claude Code to see every hook it has loaded, grouped by event, with the file each one came from. The list is read-only, so to change a hook, edit its settings file. The /hooks command works only in a terminal application, not in the Code tab of the Claude desktop app.
To test a script without the agent, send it sample JSON, then check its exit code:
$ echo '{"tool_name":"Write","tool_input":{"file_path":"/tmp/test/.env"}}' | .claude/hooks/protect-files.sh
$ echo $?
You'll see the "Blocked" message, then 2. If a hook doesn't seem to run, start Claude Code in a terminal application with claude --debug to see each hook's output and errors. The desktop app has no way to start Claude Code with this option, but it reads the same hooks, so you can check them in a terminal application even if you work in the desktop app.
Codex hooks
Codex hooks use the same format as Claude Code's, but Codex runs a hook only after you review and trust it, a step Claude Code doesn't have. Of Codex's 12 events, 11 have the same names as Claude Code's (Codex adds Interrupt and has no Notification event). A hook gets the same kind of JSON details on standard input, and exit code 2 blocks an action. Codex runs command hooks and hooks that call a tool on an MCP server.
Codex's tools differ from Claude Code's, so a hook that reads a tool's details may need changes to work in both. Codex's terminal tool is also called Bash, so the command log works in both. But Codex edits files with a tool called apply_patch, which passes a patch (a list of changes) rather than a file path, so in Codex the file-protection script above runs but blocks nothing.
Codex reads hooks from these files and runs all of them:
~/.codex/hooks.jsonor~/.codex/config.toml- your own hooks..codex/hooks.jsonor.codex/config.tomlin a project - hooks for that project, loaded only after you trust the project.
In config.toml, hooks are written in TOML, the settings format the rest of that file uses. Here is the command log as a Codex hook:
[[hooks.PreToolUse]]
matcher = "^Bash$"
[[hooks.PreToolUse.hooks]]
type = "command"
command = "jq -r '.tool_input.command' >> ~/.codex/bash.log"
To trust a hook, type /hooks in Codex, choose the event, and trust the hook. If the hook changes later, Codex skips it until you trust it again. OpenAI documents this review in the Codex command-line tool. The ChatGPT desktop app reads the same settings, but OpenAI doesn't say how it shows hooks waiting for review.
Codex notifications
Codex has no Notification event, but it has built-in alerts, and they're on unless you turn them off. When a turn (one reply to your prompt) finishes or Codex needs your approval while you're in another window, Codex asks your terminal application to show a notification. It sends that request to Warp, iTerm2 and Ghostty, and rings the terminal bell in Apple's Terminal instead. To turn the alerts off, add notifications = false under the [tui] heading in ~/.codex/config.toml:
[tui]
notifications = false
If the file already has a [tui] heading, add the line under it. Codex won't start if the file has two.
To hear a sound as well, use the notify setting, which runs a program each time Codex finishes a turn. On a Mac, this line plays a sound (a workaround Codex users share on GitHub). Put it near the top of the file, above any heading in square brackets such as [tui] or [projects], or TOML treats it as part of that section:
notify = ["bash", "-lc", "afplay /System/Library/Sounds/Glass.aiff"]
Codex reads notify only from your own config.toml.
Share hooks with Codex, Cursor and Copilot
If you've written hooks for Claude Code, Codex can import them. Type /import in Codex (choose Claude Code if Codex asks), or use Settings > Import in the ChatGPT desktop app. Codex copies your hooks once, along with your instructions, skills and MCP servers, and leaves Claude Code's files as they are. OpenAI warns that imported hooks may behave differently, so check each one, and trust it in /hooks before it runs. Codex leaves out Notification hooks, because it has no such event. It copies each command as it is, so a command that writes to a file in ~/.claude still does. Hooks you add to Claude Code later don't come across, even if you run /import again, so add them to Codex yourself.
Cursor runs your Claude Code hooks by default, from your own settings file and from a project's. GitHub Copilot's command-line tool runs the hooks in a project's .claude/settings.json files, and VS Code runs Claude Code hooks only after you turn on its chat.useClaudeHooks setting.
Hook safety
A hook runs with all the permissions of your user account, so it can read, change or delete anything you can. Anthropic and OpenAI both warn that hooks are not a security boundary (a reliable barrier against harmful actions). Read every hook before you add it, including one you copy from a web page.
The bigger risk is a project you didn't write. A project's .claude/settings.json or .codex folder can contain hooks, and anyone who can change the project can add one that runs on your Mac. Claude Code holds back a project's hooks until you trust the folder, then runs them. When you run Claude Code without its interface, as claude -p, it treats the folder as trusted and runs the project's hooks without asking.
The trust step has been bypassed before. In 2025 and 2026, security researchers found several ways a cloned project could run hooks or other commands without a clear warning, in both Claude Code and Codex (Check Point Research described the first). Each was fixed in an update. To protect yourself:
- Update Claude Code and Codex regularly, so you have the fixes.
- Before you trust a cloned project, read its
.claude/settings.json,.claude/hooks/,.codex/hooks.jsonand.codex/config.toml, and treat changes to them like changes to the code. - Read a plugin's hooks before you install it. Claude Code's
/plugindetails show that a plugin has hooks, but not what they run, so look at the plugin'shooks/hooks.jsonfile. - Keep approval hooks narrow. A
PermissionRequesthook can answer permission prompts for you, and Anthropic warns that one with an empty matcher "would auto-approve every tool permission prompt", including file writes and shell commands. A hook that approves "safe" commands by matching their text can be tricked into approving others.
Use a blocking hook to catch mistakes, and don't rely on it to stop an agent that is determined to get past it. If its script is missing or crashes, the action goes ahead. A hook that blocks commands by matching their text can also be worked around. In a video from the Pretty Printed channel, an agent blocked from running ls listed the files with find instead.
Turn off or remove a hook
To remove a hook, delete its entry from the settings file. To turn off every Claude Code hook without deleting them, add "disableAllHooks": true to your settings file. Claude Code can't turn off a single hook, but Codex can. Type /hooks in Codex, choose the event, select the hook and press the space bar to turn it off. To turn off all Codex hooks, add hooks = false under the [features] heading in ~/.codex/config.toml, or add the heading if the file doesn't have one:
[features]
hooks = false
Codex won't start if the file has two [features] headings.
Ready-made hooks
You don't have to write every hook yourself. Anthropic's security-guidance plugin is a ready-made set of hooks that checks each file Claude Code edits for common security mistakes. It also has Claude review its changes when it finishes a reply and after a git commit or git push. It warns rather than blocks, so Claude gets the findings and fixes them.
The plugin needs Python and a Git repository, and its reviews count toward your Claude plan's usage. Install it by typing this in Claude Code:
/plugin install security-guidance@claude-plugins-official
In the Claude desktop app, install plugins from the app's plugin manager instead.
You'll also find hooks shared on GitHub. I picked these popular collections by their GitHub stars (checked October 16, 2026):
- awesome-claude-code (hesreallyhim) - a long list of Claude Code resources, with hook projects in its Security, Notifications and Observability sections. About 55,800 GitHub stars; updated October 2026.
- claude-code-hooks-mastery (disler) - Python examples for 13 events, including hooks that block
rm -rfand.envaccess. About 3,900 stars; last changed February 2026. - cc-safety-net (kenryu42) - one hook that blocks destructive Git and file commands, for Claude Code, Codex, Cursor and other agents. About 1,600 stars; updated October 2026.
Individual developers publish these collections, so read every hook before you add it, and judge a collection the way you'd judge a skill (see Agent Skills Directories and Marketplaces).
Common hook problems
- A Homebrew tool isn't found. A hook gets the
PATH(the list of folders searched for commands) that Claude Code started with. If thatPATHleaves out/opt/homebrew/bin, a hook can't find Homebrew tools and may quietly do nothing. The Claude desktop app reads your shell startup files to set itsPATH, so its hooks find Homebrew tools if your~/.zprofilesets up Homebrew. To be safe, use full paths, such as/opt/homebrew/bin/prettier, or set the Mac PATH at the top of the script. Thejqthat comes with macOS is in/usr/bin, so a hook always finds it. - No notification appears. Allow Script Editor to send notifications, as described above.
- A blocking hook doesn't block. Exit with code 2 (code 1 doesn't block), and write the reason to standard error.
- A hook never runs. Check the matcher's spelling and capitals (
Bash,Edit|Write) and the settings file's commas. Make the script executable withchmod +x, and give the full path to it. - JSON from a hook is ignored. A hook can send JSON back to Claude Code for finer control, but Claude Code reads it only when the hook exits with 0 and outputs nothing else. A shell startup file that displays text, such as a greeting in
~/.zshrc, can break it. - A session hangs. A hook that never finishes can hold up the agent for up to 10 minutes. Set a shorter
"timeout", in seconds, on a slow hook. - A Codex hook doesn't run. Trust it in
/hooks. Codex skips new and changed hooks until you do.
Recommended reading
- Anthropic's Automate actions with hooks - a step-by-step guide with examples.
- Anthropic's Hooks reference - every event, setting and option.
- OpenAI's Hooks - hooks in Codex.
- Developer Setup for Agent Instructions - instructions, a less strict way to guide an AI agent.
- MCP or Skills or CLI? - other ways to give an AI agent new abilities.
Continue setting up your Mac
Don't miss the full visual roadmap and checklist that shows how to set up a Mac for software development, with all the essential tools and settings you might not yet know about.