Hooks are shell commands that receive tool input as JSON on stdin and must output JSON on stdout. You can create custom hooks to enforce project-specific rules and automate repetitive checks.
Basic Hook Structure
let data = '' ;
process . stdin . on ( 'data' , chunk => data += chunk );
process . stdin . on ( 'end' , () => {
const input = JSON . parse ( data );
// Access tool info
const toolName = input . tool_name ; // "Edit", "Bash", "Write", etc.
const toolInput = input . tool_input ; // Tool-specific parameters
const toolOutput = input . tool_output ; // Only available in PostToolUse
// Warn (non-blocking): write to stderr
console . error ( '[Hook] Warning message shown to Claude' );
// Block (PreToolUse only): exit with code 2
// process.exit(2);
// Always output the original data to stdout
console . log ( data );
});
Exit Codes
Success — continue execution
Block the tool call (PreToolUse only)
Error — logged but does not block
interface HookInput {
tool_name : string ; // "Bash", "Edit", "Write", "Read", etc.
tool_input : {
command ?: string ; // Bash: the command being run
file_path ?: string ; // Edit/Write/Read: target file
old_string ?: string ; // Edit: text being replaced
new_string ?: string ; // Edit: replacement text
content ?: string ; // Write: file content
};
tool_output ?: { // PostToolUse only
output ?: string ; // Command/tool output
};
}
Common Hook Recipes
{
"matcher" : "Edit" ,
"hooks" : [{
"type" : "command" ,
"command" : "node -e \" let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{const i=JSON.parse(d);const ns=i.tool_input?.new_string||'';if(/TODO|FIXME|HACK/.test(ns)){console.error('[Hook] New TODO/FIXME added - consider creating an issue')}console.log(d)}) \" "
}],
"description" : "Warn when adding TODO/FIXME comments"
}
Block Large File Creation
hooks.json
block-large-files.js
{
"matcher" : "Write" ,
"hooks" : [{
"type" : "command" ,
"command" : "node block-large-files.js"
}],
"description" : "Block creation of files larger than 800 lines"
}
hooks.json
format-python.js
{
"matcher" : "Edit" ,
"hooks" : [{
"type" : "command" ,
"command" : "node format-python.js"
}],
"description" : "Auto-format Python files with ruff after edits"
}
Require Test Files Alongside New Source Files
hooks.json
require-tests.js
{
"matcher" : "Write" ,
"hooks" : [{
"type" : "command" ,
"command" : "node require-tests.js"
}],
"description" : "Remind to create tests when adding new source files"
}
Async Hooks
For hooks that should not block the main flow (e.g., background analysis):
{
"type" : "command" ,
"command" : "node my-slow-hook.js" ,
"async" : true ,
"timeout" : 30
}
Async hooks run in the background. They cannot block tool execution.
Example: Background Build Analysis
const { exec } = require ( 'child_process' );
let data = '' ;
process . stdin . on ( 'data' , chunk => data += chunk );
process . stdin . on ( 'end' , () => {
const input = JSON . parse ( data );
const command = input . tool_input ?. command || '' ;
// Check if it's a build command
if ( / ( npm run build | pnpm build | yarn build ) / . test ( command )) {
console . error ( '[Hook] Build completed - async analysis running in background' );
// Run analysis in background (async hook)
exec ( 'node analyze-bundle.js' , ( error , stdout , stderr ) => {
if ( error ) {
console . error ( '[Hook] Analysis failed:' , error . message );
} else {
console . error ( '[Hook] Bundle analysis complete' );
}
});
}
console . log ( data );
});
Customizing Hooks
Disabling a Hook
Remove or comment out the hook entry in hooks.json. If installed as a plugin, override in your ~/.claude/settings.json:
{
"hooks" : {
"PreToolUse" : [
{
"matcher" : "Write" ,
"hooks" : [],
"description" : "Override: allow all .md file creation"
}
]
}
}
Environment Variables
Use environment variables for hook configuration:
const threshold = parseInt ( process . env . MAX_FILE_LINES || '800' , 10 );
const enableCheck = process . env . ENABLE_TEST_CHECK === 'true' ;
All hooks should use Node.js (node -e or node script.js) for maximum compatibility across Windows, macOS, and Linux. Avoid bash-specific syntax in hooks.
File Path Handling
const path = require ( 'path' );
// WRONG: Unix-specific paths
if ( filePath . includes ( 'src/' )) { ... }
// CORRECT: Cross-platform path handling
if ( filePath . includes ( path . join ( 'src' , '' ))) { ... }
// OR use regex that matches both separators
if ( / [ \\ / ] src [ \\ / ] / . test ( filePath )) { ... }