TS Package for Defining Claude Code Hooks

4 months ago 4

Type-safe hook definitions for Claude Code with automatic settings management.

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

2. Add a package.json script

Add this script to your package.json to easily update your hooks:

{ "scripts": { "claude:hooks": "define-claude-code-hooks" } }

You can create hooks in three different files within .claude/hooks/:

  • hooks.ts - Project hooks (updates .claude/settings.json)
  • hooks.local.ts - Local hooks (updates .claude/settings.local.json)
  • hooks.user.ts - User hooks (updates ~/.claude/settings.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:

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

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)
  • hooks.user.ts - User-specific hooks (updates ~/.claude/settings.json)

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}`); }, ], });

2. Update your Claude Code settings:

# 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)
  • hooks.user.ts → ~/.claude/settings.json (user settings, absolute paths)

Note: If your global Claude settings.json is not in the default location (~/.claude/settings.json), use the --global-settings-path option to specify the correct path.

defineHooks(hooks: HookDefinition)

Define multiple hooks. Returns the hook definition object.

  • For PreToolUse and PostToolUse: pass an array of objects with matcher and handler
  • For other hooks: pass an array of handler functions

defineHook(type: HookType, definition)

Define a single hook (for advanced use cases).

  • For PreToolUse and PostToolUse: pass an object with matcher and handler
  • 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 approve
  • PostToolUse: Runs after tool execution
  • Notification: Handles Claude Code notifications
  • Stop: Runs when main agent stops
  • SubagentStop: 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 }
  1. The CLI scans for hook files (hooks.ts, hooks.local.ts, hooks.user.ts)
  2. For each file found, it updates the corresponding settings.json with commands that use ts-node to execute TypeScript directly
  3. 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.

Predefined Hook Utilities

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", }), });

Environment File Protection

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, and MultiEdit tools
  • Provides clear error messages when access is blocked
import { defineHooks, logStopEvents, logPreToolUseEvents, logPostToolUseEvents, blockEnvFiles, } 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")], });

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" } } ]
Read Entire Article