Skip to content

claude-toolkit/hooks

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

7 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@claude-toolkit/hooks

The missing TypeScript toolkit for Claude Code hooks.

Claude Code supports 27 hook events — shell scripts that fire when Claude edits a file, runs a command, finishes a response, and more. But writing them is painful: no types, no autocomplete, silent failures, and you're staring at a JSON config hoping you got the exit codes right.

@claude-toolkit/hooks fixes that.

npm install -g @claude-toolkit/hooks

What you can do

# Add a pre-built hook in one command
claude-hooks add notify-on-stop
claude-hooks add lint-on-edit --command "eslint ."
claude-hooks add block-main-branch

# See everything that's installed
claude-hooks list

# Test a hook locally — no Claude needed
claude-hooks run PostToolUse --input '{"tool_name":"Edit","tool_input":{"file_path":"app.ts"}}'

Or build your config in TypeScript:

import { HooksBuilder } from '@claude-toolkit/hooks'

new HooksBuilder()
  .on('PostToolUse', { match: 'Edit' })
    .command('npm run lint', { async: true })
  .on('Stop')
    .command('osascript -e "beep"')
  .on('UserPromptSubmit')
    .http('https://myserver.com/log', {
      headers: { Authorization: 'Bearer $MY_TOKEN' },
      allowedEnvVars: ['MY_TOKEN'],
    })
  .installToProject()  // writes to .claude/settings.json

Or write your hook logic in TypeScript with full type safety:

// hooks/post-tool-use.ts
import { createHookHandler } from '@claude-toolkit/hooks/runner'

createHookHandler('PostToolUse', async (input) => {
  // input is fully typed — tool_name, tool_input, tool_response...
  if (input.tool_name === 'Edit') {
    console.log(`File changed: ${(input.tool_input as any).file_path}`)
  }
  // return nothing  → exit 0 (success)
  // return { block: true, message: '...' }  → exit 2 (block + tell the model)
  // throw new Error('...')  → exit 1 (show error to user)
})

Why this exists

Claude Code's hook system is genuinely powerful. You can intercept every tool call, block dangerous operations, run linters automatically, send webhooks, and more. But the developer experience is rough:

  • No TypeScript types. You write a shell command and hope the JSON input looks like what you expect.
  • Exit code semantics vary per event. Exit 2 means "block" for PreToolUse but "show to model" for PostToolUse. Good luck remembering that at 2am.
  • Silent failures. A misconfigured hook just doesn't run. No error, no warning.
  • No way to test hooks. You have to trigger Claude Code, wait for the event, and observe behavior.

@claude-toolkit/hooks solves all of this.


Installation

# Global install (recommended for CLI use)
npm install -g @claude-toolkit/hooks

# Or as a project dependency (for programmatic API)
npm install @claude-toolkit/hooks

Requirements: Node.js 18+


CLI Reference

claude-hooks add <preset>

Add a pre-built hook preset to your project.

claude-hooks add notify-on-stop           # ring bell when Claude finishes
claude-hooks add lint-on-edit             # run npm run lint after every file edit
claude-hooks add lint-on-edit --command "eslint . --fix"  # custom command
claude-hooks add block-main-branch        # prevent git push to main/master
claude-hooks add auto-commit              # auto-commit on session end

# Install to user settings (~/.claude/settings.json) instead of project
claude-hooks add notify-on-stop --scope user

Available presets:

Preset What it does
notify-on-stop Rings the system bell when Claude finishes a response
lint-on-edit Runs a lint command after every file edit (async, non-blocking)
block-main-branch Blocks git push to main or master
auto-commit Auto-commits all changes when the session ends

claude-hooks list

List all installed hooks across project and user scopes.

claude-hooks list           # show all
claude-hooks list --scope project
claude-hooks list --scope user

Example output:

Project hooks (/your/project/.claude/settings.json):

  PostToolUse:
    command [match: Edit]: eslint . (async)

  Stop:
    command: osascript -e "beep" (async)

claude-hooks run <event>

Test a hook event with mock input — no Claude Code required.

# Basic
claude-hooks run Stop

# With input
claude-hooks run PostToolUse --input '{"tool_name":"Edit","tool_input":{"file_path":"app.ts"}}'

# Test blocking behavior
claude-hooks run PreToolUse --input '{"tool_name":"Bash","tool_input":{"command":"git push origin main"}}'

Shows the command that ran, its output, and whether it would block Claude.


claude-hooks install <file>

Install a hook script into .claude/settings.json.

# TypeScript file (compiled automatically via esbuild)
claude-hooks install ./hooks/my-hook.ts --event PostToolUse

# Shell script
claude-hooks install ./hooks/notify.sh --event Stop

# With matcher
claude-hooks install ./hooks/lint.ts --event PostToolUse --match Edit

# Install to user scope
claude-hooks install ./hooks/my-hook.ts --event Stop --scope user

The filename is used to infer the event if --event is not provided:

  • post-tool-use.tsPostToolUse
  • session-end.shSessionEnd

Programmatic API

HooksBuilder

import { HooksBuilder } from '@claude-toolkit/hooks'

const builder = new HooksBuilder()

// Shell command hook
builder
  .on('PostToolUse', { match: 'Edit' })
  .command('npm run lint', {
    async: true,            // run in background
    timeout: 30,            // seconds
    statusMessage: 'Linting...',
    once: false,
  })

// HTTP webhook
builder
  .on('Stop')
  .http('https://api.example.com/notify', {
    headers: { 'X-API-Key': '$MY_API_KEY' },
    allowedEnvVars: ['MY_API_KEY'],
  })

// LLM prompt hook
builder
  .on('PostToolUse')
  .prompt('Did the last tool call look safe? Reply YES or NO.')

// Agent verification hook
builder
  .on('Stop')
  .agent('Verify that all changed files have passing tests.')

// Merge multiple builders
const a = new HooksBuilder().on('Stop').command('beep')
const b = new HooksBuilder().on('SessionStart').command('echo start')
new HooksBuilder().use(a).use(b).installToProject()

// Export as plain object
const config = builder.toJSON()
// → { PostToolUse: [{ matcher: 'Edit', hooks: [...] }], ... }

// Write to settings.json
builder.installToProject()        // .claude/settings.json
builder.installToUser()           // ~/.claude/settings.json

createHookHandler

Write hook logic in TypeScript. Your handler receives fully typed input and returns a typed result.

// hooks/my-hook.ts
import { createHookHandler } from '@claude-toolkit/hooks/runner'

createHookHandler('PreToolUse', async (input) => {
  // Block dangerous commands
  const cmd = (input.tool_input as { command?: string }).command ?? ''

  if (cmd.includes('rm -rf /')) {
    return {
      block: true,
      message: 'Nice try. That command is not allowed.',
    }
  }

  // Log to a file
  const { appendFileSync } = await import('fs')
  appendFileSync('/tmp/claude-log.txt', `${new Date().toISOString()} ${input.tool_name}\n`)

  // Return nothing for success
})

Compile and install:

claude-hooks install ./hooks/my-hook.ts --event PreToolUse

Exit code behavior

Your return value maps to exit codes automatically:

Return value Exit code Effect
void (return nothing) 0 Success
{ block: true, message } 2 Block + show message to model
throw new Error(message) 1 Show error to user
{ hookSpecificOutput: ... } 0 JSON output for special events

Types

All 27 hook event types are exported:

import type {
  HookEvent,
  PreToolUseInput,
  PostToolUseInput,
  PostToolUseFailureInput,
  PermissionDeniedInput,
  UserPromptSubmitInput,
  SessionStartInput,
  SessionEndInput,
  StopInput,
  StopFailureInput,
  NotificationInput,
  SubagentStartInput,
  SubagentStopInput,
  PreCompactInput,
  PostCompactInput,
  PermissionRequestInput,
  PermissionRequestOutput,
  SetupInput,
  ConfigChangeInput,
  InstructionsLoadedInput,
  CwdChangedInput,
  FileChangedInput,
  ElicitationInput,
  ElicitationOutput,
  // ... and more
} from '@claude-toolkit/hooks'

Presets API

Use presets programmatically:

import { HooksBuilder } from '@claude-toolkit/hooks'
import { lintOnEdit, notifyOnStop, blockMainBranch } from '@claude-toolkit/hooks/presets'

new HooksBuilder()
  .use(lintOnEdit({ command: 'prettier --write .' }))
  .use(notifyOnStop())
  .use(blockMainBranch())
  .installToProject()

Hook Events Reference

Claude Code fires these events. All types are available from @claude-toolkit/hooks.

Event When it fires Can block?
PreToolUse Before any tool runs ✅ exit 2
PostToolUse After tool succeeds Shows to model
PostToolUseFailure After tool fails Shows to model
UserPromptSubmit When you submit a prompt ✅ exit 2
Stop Before Claude finishes response Shows to model
StopFailure When API error ends the turn ❌ fire-and-forget
SessionStart New session begins
SessionEnd Session is ending
PermissionRequest Permission dialog shown ✅ programmatic
PermissionDenied Auto-mode denies a tool Can retry
Notification Claude sends a notification
SubagentStart Subagent spawned
SubagentStop Subagent finishing Can continue
PreCompact Before context compaction ✅ exit 2
PostCompact After context compaction
Setup Repo init/maintenance
ConfigChange Settings file changed ✅ exit 2
InstructionsLoaded CLAUDE.md loaded ❌ observability only
CwdChanged Working directory changed
FileChanged Watched file changed
WorktreeCreate Create a worktree
WorktreeRemove Remove a worktree
TeammateIdle Teammate going idle Can prevent
TaskCreated Task being created ✅ exit 2
TaskCompleted Task being completed ✅ exit 2
Elicitation MCP server requests input ✅ programmatic
ElicitationResult User responds to elicitation Can override

License

MIT

About

TypeScript toolkit for Claude Code hooks — types, builder, runner, and CLI

Topics

Resources

License

Stars

Watchers

Forks

Contributors