Type-safe hook definitions for Claude Code with automatic settings management.
npx @timoaus/define-claude-code-hooks --init
This interactive command will:
- Let you choose between project or local hooks
- Install predefined hooks (logging, security, announcements)
- Install the package as a dev dependency
- Add the
claude:hooks
script to your package.json - Set up your hooks automatically
npm install --save-dev @timoaus/define-claude-code-hooks
# or
yarn add --dev @timoaus/define-claude-code-hooks
# or
pnpm add --save-dev @timoaus/define-claude-code-hooks
# or
bun add --dev @timoaus/define-claude-code-hooks
Add this script to your package.json
to easily update your hooks:
{
"scripts": {
"claude:hooks": "define-claude-code-hooks"
}
}
You can create hooks in two different files within .claude/hooks/
:
hooks.ts
- Project hooks (updates.claude/settings.json
)hooks.local.ts
- Local hooks (updates.claude/settings.local.json
)
For example, create .claude/hooks/hooks.ts
:
import { defineHooks } from "@timoaus/define-claude-code-hooks";
const preventEditingEnvFile = defineHook("PreToolUse", {
matcher: "Write|Edit|MultiEdit",
handler: async (input) => {
const filePath = input.tool_input.file_path;
if (filePath && filePath.endsWith(".env")) {
return {
decision: "block",
reason:
"Direct editing of .env files is not allowed for security reasons",
};
}
},
});
export default defineHooks({
PreToolUse: [preventEditingEnvFile],
});
Extend your hooks with built-in logging utilities:
import {
defineHooks,
logPreToolUseEvents,
} from "@timoaus/define-claude-code-hooks";
export default defineHooks({
PreToolUse: [logPreToolUseEvents({ maxEventsStored: 100 })],
});
Creates a log file in your project root: hook-log.tool-use.json
Run the script to update your settings:
npm run claude:hooks
The CLI will automatically detect which hook files exist and update the corresponding settings files. Your hooks are now active! Claude Code will respect your rules and log tool usage.
The library includes several predefined hook utilities for common logging scenarios:
Hook Function | Options |
---|---|
logPreToolUseEvents Logs tool uses before execution |
• Optional first param: matcher (regex pattern, defaults to '.*' for all tools)• maxEventsStored (default: 100)• logFileName (default: 'hook-log.tool-use.json')• includeToolInput (default: true) |
logPostToolUseEvents Logs tool uses after execution |
• Optional first param: matcher (regex pattern, defaults to '.*' for all tools)• maxEventsStored (default: 100)• logFileName (default: 'hook-log.tool-use.json')• includeToolInput (default: true)• includeToolResponse (default: true) |
logStopEvents Logs main agent stop events |
• maxEventsStored (default: 100)• logFileName (default: 'hook-log.stop.json') |
logSubagentStopEvents Logs subagent stop events |
• maxEventsStored (default: 100)• logFileName (default: 'hook-log.stop.json') |
logNotificationEvents Logs notification messages |
• maxEventsStored (default: 100)• logFileName (default: 'hook-log.notification.json') |
blockEnvFiles Blocks access to .env files |
No options - blocks all .env file variants except example files |
announceStop Announces task completion via TTS |
• message (default: 'Task completed')• voice (system-specific voice name)• rate (speech rate in WPM)• customCommand (custom TTS command)• suppressOutput (default: false) |
announceSubagentStop Announces subagent completion via TTS |
Same options as announceStop |
announcePreToolUse Announces before tool execution |
• First param: matcher (regex pattern, defaults to '.*')• message (default: 'Using {toolName}')• voice , rate , customCommand , suppressOutput |
announcePostToolUse Announces after tool execution |
• First param: matcher (regex pattern, defaults to '.*')• message (default: '{toolName} completed')• voice , rate , customCommand , suppressOutput |
announceNotification Speaks notification messages |
• message (default: '{message}')• voice , rate , customCommand , suppressOutput |
All predefined hooks:
- Create JSON log files in your current working directory
- Automatically rotate logs when reaching
maxEventsStored
limit (keeping most recent events) - Include timestamps, session IDs, and transcript paths in log entries
- Handle errors gracefully without interrupting Claude Code
Example usage:
import {
defineHooks,
logPreToolUseEvents,
logStopEvents,
} from "@timoaus/define-claude-code-hooks";
export default defineHooks({
PreToolUse: [
logPreToolUseEvents({ maxEventsStored: 200, logFileName: "my-tools.json" }),
],
Stop: [logStopEvents()],
});
Choose where to create your hooks based on your needs (all in .claude/hooks/
):
hooks.ts
- Project-wide hooks (committed to git)hooks.local.ts
- Local-only hooks (not committed)
Example:
import { defineHooks } from "define-claude-code-hooks";
export default defineHooks({
PreToolUse: [
// Block grep commands and suggest ripgrep
{
matcher: "Bash",
handler: async (input) => {
if (input.tool_input.command?.includes("grep")) {
return {
decision: "block",
reason: "Use ripgrep (rg) instead of grep for better performance",
};
}
},
},
// Log all file writes
{
matcher: "Write|Edit|MultiEdit",
handler: async (input) => {
console.error(`Writing to file: ${input.tool_input.file_path}`);
},
},
],
PostToolUse: [
// Format TypeScript files after editing
{
matcher: "Write|Edit",
handler: async (input) => {
if (input.tool_input.file_path?.endsWith(".ts")) {
const { execSync } = require("child_process");
execSync(`prettier --write "${input.tool_input.file_path}"`);
}
},
},
],
Notification: [
// Custom notification handler
async (input) => {
console.log(`Claude says: ${input.message}`);
},
],
});
# Automatically detect and update all hook files
npx define-claude-code-hooks
# Remove all managed hooks
npx define-claude-code-hooks --remove
# Use a custom global settings path (if not in ~/.claude/settings.json)
npx define-claude-code-hooks --global-settings-path /path/to/settings.json
The CLI automatically detects which hook files exist and updates the corresponding settings:
hooks.ts
→.claude/settings.json
(project settings, relative paths)hooks.local.ts
→.claude/settings.local.json
(local settings, relative paths)
Define multiple hooks. Returns the hook definition object.
- For PreToolUse and PostToolUse: pass an array of objects with
matcher
andhandler
- For other hooks: pass an array of handler functions
Define a single hook (for advanced use cases).
- For PreToolUse and PostToolUse: pass an object with
matcher
andhandler
- For other hooks: pass just the handler function
Example:
// Tool hook
const bashHook = defineHook("PreToolUse", {
matcher: "Bash",
handler: async (input) => {
/* ... */
},
});
// Non-tool hook
const stopHook = defineHook("Stop", async (input) => {
/* ... */
});
PreToolUse
: Runs before tool execution, can block or approvePostToolUse
: Runs after tool executionNotification
: Handles Claude Code notificationsStop
: Runs when main agent stopsSubagentStop
: Runs when subagent stops
Hooks can return structured responses:
interface HookOutput {
// Common fields
continue?: boolean; // Whether Claude should continue
stopReason?: string; // Message when continue is false
suppressOutput?: boolean; // Hide output from transcript
// PreToolUse specific
decision?: "approve" | "block";
reason?: string; // Reason for decision
}
- The CLI scans for hook files (hooks.ts, hooks.local.ts)
- For each file found, it updates the corresponding settings.json with commands that use ts-node to execute TypeScript directly
- Marks managed hooks so they can be safely removed later
This library is written in TypeScript and provides full type safety for all hook inputs and outputs.
The library includes several predefined hook utilities for common logging scenarios:
import {
defineHooks,
logStopEvents,
logSubagentStopEvents,
} from "@timoaus/define-claude-code-hooks";
export default defineHooks({
Stop: [logStopEvents("hook-log.stop.json")],
SubagentStop: [logSubagentStopEvents("hook-log.subagent.json")],
});
import {
defineHooks,
logNotificationEvents,
} from "@timoaus/define-claude-code-hooks";
export default defineHooks({
Notification: [logNotificationEvents("hook-log.notifications.json")],
});
import {
defineHooks,
logPreToolUseEvents,
logPostToolUseEvents,
} from "@timoaus/define-claude-code-hooks";
export default defineHooks({
// Log all tool use
PreToolUse: logPreToolUseEvents(), // Logs all tools by default
PostToolUse: logPostToolUseEvents(), // Logs all tools by default
});
// Or log specific tools only
export default defineHooks({
PreToolUse: logPreToolUseEvents("Bash|Write|Edit", {
maxEventsStored: 200,
logFileName: "tool-use.json",
}),
PostToolUse: logPostToolUseEvents("Bash|Write|Edit", {
maxEventsStored: 200,
logFileName: "tool-use.json",
}),
});
import {
defineHooks,
blockEnvFiles,
} from "@timoaus/define-claude-code-hooks";
export default defineHooks({
PreToolUse: [
blockEnvFiles, // Blocks access to .env files while allowing .env.example
],
});
The blockEnvFiles
hook:
- Blocks reading or writing to
.env
files and variants (.env.local
,.env.production
, etc.) - Allows access to example env files (
.env.example
,.env.sample
,.env.template
,.env.dist
) - Works with
Read
,Write
,Edit
, andMultiEdit
tools - Provides clear error messages when access is blocked
import {
defineHooks,
announceStop,
announceSubagentStop,
announcePreToolUse,
announcePostToolUse,
announceNotification,
} from "@timoaus/define-claude-code-hooks";
// Basic usage - announce all events
export default defineHooks({
Stop: [announceStop()],
SubagentStop: [announceSubagentStop()],
PreToolUse: [announcePreToolUse()], // Announces all tools
PostToolUse: [announcePostToolUse()], // Announces all tools
Notification: [announceNotification()],
});
// Announce specific tools only
export default defineHooks({
PreToolUse: [
announcePreToolUse('Bash|Write|Edit', {
message: "Running {toolName}"
})
],
PostToolUse: [
announcePostToolUse('Bash|Write|Edit', {
message: "{toolName} finished"
})
],
});
// With custom voices and messages
export default defineHooks({
Stop: [
announceStop({
message: "Claude has finished the task for session {sessionId}",
voice: "Samantha", // macOS voice
rate: 200,
})
],
Notification: [
announceNotification({
message: "Claude says: {message}",
voice: "Daniel"
})
],
});
// With custom TTS command (for Linux/Windows)
export default defineHooks({
Stop: [
announceStop({
customCommand: "espeak -s 150 '{message}'", // Linux
// or for Windows PowerShell:
// customCommand: "powershell -Command \"(New-Object -ComObject SAPI.SpVoice).Speak('{message}')\""
})
],
});
The announcement hooks:
- Use text-to-speech to announce various Claude Code events
- Support macOS (say), Linux (espeak), and Windows (PowerShell SAPI)
- Allow custom messages with template variables:
{sessionId}
- The session ID{timestamp}
- Current timestamp{toolName}
- Tool name (for PreToolUse/PostToolUse){message}
- Notification message (for Notification hook)
- Support voice selection and speech rate customization
- Can use custom TTS commands for other systems
- Run asynchronously without blocking Claude Code
import {
defineHooks,
logStopEvents,
logPreToolUseEvents,
logPostToolUseEvents,
blockEnvFiles,
announceStop,
} from "@timoaus/define-claude-code-hooks";
export default defineHooks({
PreToolUse: [
blockEnvFiles, // Security: prevent .env file access
logPreToolUseEvents({ logFileName: "hook-log.tool-use.json" }),
// Add your custom hooks here
{
matcher: "Bash",
handler: async (input) => {
// Custom logic
},
},
],
PostToolUse: logPostToolUseEvents({ logFileName: "hook-log.tool-use.json" }),
Stop: [
logStopEvents("hook-log.stop.json"),
announceStop({ message: "Task completed successfully!" }),
],
});
The predefined hooks create JSON log files with the following structure:
[
{
"timestamp": "2025-01-07T10:30:00.000Z",
"event": "PreToolUse",
"sessionId": "abc-123",
"transcriptPath": "/path/to/transcript.jsonl",
"toolName": "Bash",
"toolInput": {
"command": "ls -la"
}
}
]