AI Weekly

Guides ·

4 Claude Code hooks examples — permission alerts, blocking risky commands, logging, and a browser view

How to add Claude Code hooks to settings.json, with 4 examples you can copy. A macOS notification when Claude asks for permission, blocking commands like rm -rf with exit 2, logging every tool call to a file, and sending events to a browser view like the open-source eojeon does.

Key takeaways

Claude Code hooks run your own command at set moments, such as before and after a tool call, on a permission request, or when Claude finishes a reply. You add them under "hooks" in ~/.claude/settings.json or a project's .claude/settings.json, keyed by event name. The command receives JSON on stdin (session_id, cwd, hook_event_name, tool_name, tool_input and more); exit 0 passes, exit 2 blocks (on events like PreToolUse), and any other code only logs a warning.

Claude Code: A Hands-On Guide · Part 3 of 10

A hook is a setting that says "when Claude Code does X, run this command." A few lines in a settings file let you get a notification when Claude asks for permission, block risky commands before they run, and keep a log of every action. This guide shows where hooks go, what they receive and return, and 4 examples you can copy. The examples are the approach hiio actually used while building the open-source eojeon. It was written by AI (Claude), and the facts are checked against the official Claude Code hooks docs.

Where hooks go

File Applies to
~/.claude/settings.json All your projects
.claude/settings.json This project (shared with your team)
.claude/settings.local.json This project, only you (not shared)

The structure is always the same: hooks → event name → array of groups (matcher + hooks) → command.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{ "type": "command", "command": "~/.claude/hooks/guard.sh", "timeout": 5 }]
      }
    ]
  }
}

What a hook receives and returns

The command gets one JSON object on stdin. The common fields are session_id, transcript_path, cwd, permission_mode and hook_event_name. Tool events add tool_name, tool_input and tool_use_id. Events from a subagent add agent_id and agent_type.

Exit code Meaning
exit 0 Pass. If stdout is a single JSON object, it is parsed. Otherwise it is treated as plain text
exit 2 Block. On events like PreToolUse and UserPromptSubmit, the action stops and stderr is sent to Claude as the reason
Anything else (including exit 1) Does not block. A warning is logged and the action continues

exit 1 does not block. A blocking hook must end with exit 2.

Example 1. Get a macOS notification when Claude asks for permission

If Claude is waiting on "Can I do this?" while you are in another window, you may not notice for a long time. Attach a notification to PermissionRequest.

{
  "hooks": {
    "PermissionRequest": [
      { "hooks": [{ "type": "command", "command": "osascript -e 'display notification \"Permission requested\" with title \"Claude Code\"'", "timeout": 5 }] }
    ]
  }
}

This is for macOS. On Linux, swap in notify-send. On Windows, use a PowerShell notification command.

Example 2. Block risky commands before they run

Use PreToolUse with matcher: "Bash" to inspect the command first, and block it with exit 2 if it matches. You need jq (brew install jq).

#!/bin/sh
# ~/.claude/hooks/guard.sh  (run chmod +x on it)
cmd=$(jq -r '.tool_input.command // ""')
case "$cmd" in
  *"rm -rf /"*|*"git push --force"*|*"git push -f"*)
    echo "Blocked: $cmd — check it and run it yourself" >&2
    exit 2 ;;
esac
exit 0

The settings are exactly the example under "Where hooks go" above. When a command is blocked, Claude receives the stderr line as the reason and looks for another way.

Example 3. Log every tool call to a file

A log of what ran and when lets you answer "what command did it run earlier?" right away.

{
  "hooks": {
    "PreToolUse": [
      { "hooks": [{ "type": "command", "command": "jq -c '{t: now|todate, s: .session_id[0:8], tool: .tool_name, in: (.tool_input.command // .tool_input.file_path // \"\")}' >> ~/.claude/tool-log.jsonl", "timeout": 5 }] }
    ]
  }
}

Careful: commands and file paths can contain secrets (tokens, passwords). Keep the log file readable only by you, and never upload it anywhere.

Example 4. Send events to a browser view (the eojeon approach)

This is how eojeon uses hooks:

  1. It launches Claude Code with claude --settings <temporary settings file>. Your own ~/.claude/settings.json is untouched, and the hooks apply only to that run.
  2. The temporary file attaches the same hook command to 13 events: SessionStart, UserPromptSubmit, PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied, SubagentStart, SubagentStop, Notification, Stop, StopFailure and SessionEnd.
  3. The hook script shortens the JSON it receives (tool name, first line of the command, relative path, and secret-looking strings replaced with ***). Then it sends it only to a local server on 127.0.0.1. It finishes within 1.5 seconds and always exits 0, so it never blocks Claude Code.
  4. The browser draws a palace scene from these events. A subagent starting shows a minister entering the court. A permission request shows a minister asking the king for approval.

To see it, run npx eojeon-cli demo (it replays the scenes without Claude). To use it for real, run npx eojeon-cli. The code is in server/hook.js and server/sanitize.js on GitHub.

FAQ

Do Claude Code hooks slow down Claude Code?

A hook runs at its moment, and Claude Code waits until it finishes (or hits its timeout). The default timeout is 600 seconds, so for short jobs like notifications or logging, set something like "timeout": 5. eojeon's hook finishes on its own within 1.5 seconds.

Why doesn't my hook block the command when it exits with code 1?

exit 1 is a non-blocking error. The action continues and only a warning is logged. To block, end with exit 2 and write the reason to stderr.

Can I try hooks without editing my settings.json?

Yes. Put only the hooks in a separate JSON file and run claude --settings path/to/that-file. They apply only to that run. eojeon works this way. The official docs also show an example that turns off all hooks for one run (claude --settings '{"disableAllHooks": true}').

How do I see which hooks are active?

Type /hooks inside Claude Code. It shows each hook and which file it came from (user, project, local or plugin).

Sources

#Claude Code#hooks#Claude Code hooks#automation#settings.json#PreToolUse

Written with AI (Claude) and checked against the linked sources.

Lab

Experiment 01

AI Type Test

How do you actually use AI? 15 types, Lv.1–10, and where you rank among people in your line of work.

3 min10 questions15 typesStart

More posts