Claude Code Hooks: Permission Hooks, Context Hooks, and Custom Hooks
Hooks Let You Shape Claude Code's Behavior Without Modifying It
Every time Claude Code does something — runs a command, writes a file, starts a session — there are points in that workflow where a script can run. Hooks are those points. You write a script, point Claude Code at it, and the script runs at the right moment in the right context.
This is the extensibility model that makes Claude Code work for specialized workflows. The default behavior is sensible for general use. Hooks let you adapt it to specific needs without forking the tool or waiting for a feature request.
There are three categories of hooks, each doing something different.
Permission Hooks: Gatekeeping Before Actions Run
Permission hooks fire before Claude Code executes a tool call. They receive the full context of what Claude Code intends to do — which tool, with what arguments, in which directory. The hook returns either allow or deny. If it denies, the tool call does not execute.
This is how you enforce project-specific rules. A permission hook can block file writes outside a certain directory, prevent shell commands that modify system state, or require confirmation for any operation touching production credentials.
Configure a permission hook in claude_settings.json:
{
"hooks": {
"preTool": "./scripts/permission-check.sh"
}
}
The script receives environment variables describing the operation: CLAUDE_TOOL, CLAUDE_TOOL_ARGS, CLAUDE_CWD. It returns 0 to allow, non-zero to deny. On denial, it should print a message explaining why.
#!/bin/bash
# permission-check.sh example
if [[ "$CLAUDE_TOOL" == "Write" ]]; then
# Block writes outside the project src
if [[ ! "$CLAUDE_TOOL_ARGS" =~ ^./src/ ]]; then
echo "ERROR: Write operations are only allowed in ./src/"
exit 1
fi
fi
exit 0
Permission hooks run synchronously. If the hook takes time to run, Claude Code waits. This makes them suitable for running actual security checks — linting, scanning for secrets, verifying credentials — as long as those checks are fast. A slow permission hook makes every operation feel sluggish.
Context Hooks: Injecting Information Into Sessions
Context hooks run at session boundaries — before a session starts and after it ends. They let you inject information into Claude Code's context without modifying your prompts. A context hook can read your issue tracker, check the current git branch state, pull in relevant documentation, and make all of that available to the session automatically.
The pre-session hook receives nothing and outputs context text to stdout. Whatever it prints becomes part of the session's initial context. This happens before you type your first prompt, so Claude Code sees it as background context.
{
"hooks": {
"preSession": "./scripts/session-context.sh"
}
}
#!/bin/bash
# session-context.sh example
echo "Current branch: $(git branch --show-current)"
echo "Open issues:"
./scripts/list-issues.sh
echo "Recent commits:"
git log --oneline -5
The output from this script appears in Claude Code's context at session start. You do not have to ask for it — it is just there. This is useful for getting Claude Code oriented before you start describing what you want to do.
Post-session hooks run when the session ends. They receive session metadata — duration, tools used, files changed — which you can use for logging, billing, or audit trails. A team can track how Claude Code is being used across an organization by aggregating what post-session hooks report.
Custom Hooks: Triggers for Specific Operations
Custom hooks fire on specific operations rather than general boundaries. The ones most people use: preCommand runs before a shell command Claude Code executes, and postCommit runs after a git commit completes.
{
"hooks": {
"preCommand": "./scripts/pre-command.sh",
"postCommit": "python3 ./scripts/notify.py"
}
}
The pre-command hook is where quality gates live. Before Claude Code runs a shell command, the hook can run linting, run tests, check formatting. If the checks fail, the hook exits non-zero and the command does not execute. You have to fix the issues and retry.
#!/bin/bash
# pre-command.sh — quality gate example
if [[ "$CLAUDE_COMMAND" =~ ^npm run ]]; then
echo "Running pre-commit checks..."
npm run lint --silent
if [[ $? -ne 0 ]]; then
echo "Linting failed. Fix issues before continuing."
exit 1
fi
npm run test --silent
if [[ $? -ne 0 ]]; then
echo "Tests failed. Fix issues before continuing."
exit 1
fi
fi
This means Claude Code will not run your build or test commands unless the code passes your quality gates. For teams that maintain code quality standards, this keeps Claude Code from introducing things that would fail CI anyway.
The post-commit hook is for automation that should happen after git operations. Posting to Slack, updating issue status, triggering a deployment — whatever fits your workflow. The hook receives the commit hash and message as environment variables.
Writing Hooks That Hold Up
Hooks run in your security context. A pre-command hook that runs rm -rf will execute with your permissions. Write hooks defensively — assume bad inputs, validate everything, fail closed rather than open.
Keep them fast. A permission hook that takes two seconds to run makes every tool call feel two seconds slower. If you need expensive operations, run them asynchronously and report back through a side channel rather than blocking the tool call.
Make them idempotent. Hooks can fire multiple times in a session. A hook that appends to a log file every time it runs should check whether the entry already exists. A hook that posts a Slack message should check whether the message was already posted. Idempotent hooks are reliable hooks.
Debugging Hooks
When a hook is not working, the debug flag tells you what is happening:
claude --debug
Look for lines mentioning hooks — whether they were found, whether they executed, what they returned. The debug output is verbose but it removes the guesswork.
If a hook runs but does not do what you expect, check the environment variables it receives. A hook that assumes a certain directory structure will break in projects that do not have it. Print what you are receiving at the top of the script while debugging, then remove the prints when it works.
Get Started with Claude Code
Start building with Claude Code today. Free to download, powerful enough for production.