Skip to content

Commit 8237bd0

Browse files
committed
fix: update hook response types and improve command formatting
- Refactored PreToolUse and PostToolUse handlers to use more descriptive response types. - Updated the handling of dangerous commands in PreToolUse to return a more informative response. - Added a new SubagentStop handler for improved task management. - Cleaned up formatting in init.ts for consistency. This enhances type safety and clarity in the hook system.
1 parent c981b74 commit 8237bd0

File tree

4 files changed

+163
-31
lines changed

4 files changed

+163
-31
lines changed

CLAUDE.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Commands
6+
7+
### Building and Development
8+
- `bun run build` - Compile TypeScript to JavaScript
9+
- `bun run lint` - Run Biome linter
10+
- `bun run lint:fix` - Run Biome linter with auto-fix
11+
- `bun run format` - Format code with Biome
12+
13+
### Testing
14+
- `bun test` - Run all tests
15+
- `bun run test:unit` - Run unit tests only
16+
- `bun run test:integration` - Run integration tests
17+
- `bun run test:smoke` - Run smoke tests
18+
- `bun run test:coverage` - Run tests with coverage report
19+
20+
### Clean Up
21+
- `bun run clean` - Remove all build artifacts and test outputs
22+
- `bun run clean:test` - Remove only test output directories
23+
24+
## Architecture
25+
26+
This is an oclif-based CLI tool written in TypeScript that generates a hook system for Claude Code.
27+
28+
### Key Components
29+
30+
1. **Command Structure**: Commands live in `src/commands/`. Currently, there's only the main `init` command that sets up the hook system.
31+
32+
2. **Template System**: Hook templates are stored in `templates/` and copied to the user's `.claude/` directory when initialized.
33+
34+
3. **Hook Types**: The system supports four hook types:
35+
- `PreToolUse` - Intercept tool usage before execution
36+
- `PostToolUse` - React to tool execution results
37+
- `Notification` - Handle Claude notifications
38+
- `Stop` - Handle session stop events
39+
40+
4. **Generated Structure**: Running the CLI creates:
41+
```
42+
.claude/
43+
├── settings.json # Hook configuration
44+
└── hooks/
45+
├── index.ts # Main hook handlers (user edits this)
46+
├── lib.ts # Type definitions and utilities
47+
└── session.ts # Session tracking utilities
48+
```
49+
50+
### Testing Strategy
51+
52+
- **Unit Tests**: Test individual commands and components
53+
- **Integration Tests**: Test the full CLI behavior
54+
- **Smoke Tests**: Validate generated files work correctly
55+
- **CI/CD**: Tests run on Ubuntu, Windows, and macOS with Node 18 & 20
56+
57+
### Development Workflow
58+
59+
1. Work on feature branches, never directly on main
60+
2. Use conventional commits (e.g., `feat:`, `fix:`, `chore:`)
61+
3. Create pull requests to merge into main
62+
4. Semantic Release handles versioning and npm publishing automatically
63+
64+
### Important Notes
65+
66+
- Hooks are executed using Bun runtime (required dependency)
67+
- The project uses ESM modules (`"type": "module"`)
68+
- TypeScript strict mode is enabled
69+
- Session logs are written to the system temp directory

src/commands/init.ts

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
import * as path from 'node:path'
2-
import {fileURLToPath} from 'node:url'
3-
import {Command, Flags} from '@oclif/core'
2+
import { fileURLToPath } from 'node:url'
3+
import { Command, Flags } from '@oclif/core'
44
import chalk from 'chalk'
55
import fs from 'fs-extra'
66
import ora from 'ora'
77

8-
const __filename = fileURLToPath(import.meta.url)
9-
const __dirname = path.dirname(__filename)
10-
118
export default class Init extends Command {
129
static description = `Initialize Claude Code hooks in your project
1310
@@ -37,16 +34,16 @@ This command sets up basic Claude Code hooks in your project:
3734
}
3835

3936
public async run(): Promise<void> {
40-
const {flags} = await this.parse(Init)
37+
const { flags } = await this.parse(Init)
4138

4239
console.log(chalk.blue.bold('\n🪝 Claude Hooks Setup\n'))
4340

4441
// Check if Bun is installed
45-
const {spawn} = await import('node:child_process')
42+
const { spawn } = await import('node:child_process')
4643
const isWindows = process.platform === 'win32'
4744
const command = isWindows ? 'where' : 'which'
4845
const checkBun = await new Promise<boolean>((resolve) => {
49-
const child = spawn(command, ['bun'], {shell: false})
46+
const child = spawn(command, ['bun'], { shell: false })
5047
child.on('error', () => resolve(false))
5148
child.on('exit', (code) => resolve(code === 0))
5249
})

templates/hooks/index.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,18 @@
22

33
import {
44
type BashToolInput,
5-
type HookResponse,
65
type NotificationPayload,
76
type PostToolUsePayload,
87
type PreToolUsePayload,
8+
type PreToolUseResponse,
99
runHook,
1010
type StopPayload,
11+
type SubagentStopPayload,
1112
} from './lib'
1213
import {saveSessionData} from './session'
1314

1415
// PreToolUse handler - called before Claude uses any tool
15-
async function preToolUse(payload: PreToolUsePayload): Promise<HookResponse> {
16+
async function preToolUse(payload: PreToolUsePayload): Promise<PreToolUseResponse> {
1617
// Save session data (optional - remove if not needed)
1718
await saveSessionData('PreToolUse', payload)
1819

@@ -31,14 +32,14 @@ async function preToolUse(payload: PreToolUsePayload): Promise<HookResponse> {
3132
// Block dangerous commands
3233
if (command.includes('rm -rf /') || command.includes('rm -rf ~')) {
3334
console.error('❌ Dangerous command detected! Blocking execution.')
34-
return {action: 'reject', message: 'Dangerous command detected'}
35+
return {decision: 'block', reason: `Dangerous command detected: ${command}`}
3536
}
3637
}
3738

3839
// Add your custom logic here!
3940
// You have full TypeScript support and can use any npm packages
4041

41-
return {action: 'continue'}
42+
return {} // Empty object means continue with default behavior
4243
}
4344

4445
// PostToolUse handler - called after Claude uses a tool
@@ -47,7 +48,7 @@ async function postToolUse(payload: PostToolUsePayload): Promise<void> {
4748
await saveSessionData('PostToolUse', payload)
4849

4950
// Example: React to successful file writes
50-
if (payload.tool_name === 'Write' && payload.success) {
51+
if (payload.tool_name === 'Write' && payload.tool_response) {
5152
console.log(`✅ File written successfully!`)
5253
}
5354

@@ -67,7 +68,21 @@ async function stop(payload: StopPayload): Promise<void> {
6768
await saveSessionData('Stop', payload)
6869

6970
// Example: Summary or cleanup logic
70-
console.log(`👋 Session ended: ${payload.reason}`)
71+
console.log(`👋 Session ended`)
72+
}
73+
74+
// SubagentStop handler - called when a Claude subagent (Task tool) stops
75+
async function subagentStop(payload: SubagentStopPayload): Promise<void> {
76+
await saveSessionData('SubagentStop', payload)
77+
78+
// Example: Log subagent completion
79+
console.log(`🤖 Subagent task completed`)
80+
81+
// Add your custom subagent cleanup logic here
82+
// Note: Be careful with stop_hook_active to avoid infinite loops
83+
if (payload.stop_hook_active) {
84+
console.log('⚠️ Stop hook is already active, skipping additional processing')
85+
}
7186
}
7287

7388
// Run the hook with our handlers
@@ -76,4 +91,5 @@ runHook({
7691
postToolUse,
7792
notification,
7893
stop,
94+
subagentStop,
7995
})

templates/hooks/lib.ts

Lines changed: 67 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,37 +3,76 @@
33
import * as fs from 'node:fs/promises'
44
import * as path from 'node:path'
55

6-
// Types
6+
// Input payload types based on official Claude Code schemas
77
export interface PreToolUsePayload {
8-
hook_type: 'PreToolUse'
98
session_id: string
9+
transcript_path: string
1010
tool_name: string
1111
tool_input: Record<string, any>
1212
}
1313

1414
export interface PostToolUsePayload {
15-
hook_type: 'PostToolUse'
1615
session_id: string
16+
transcript_path: string
1717
tool_name: string
1818
tool_input: Record<string, any>
19-
tool_result: any
20-
tool_error: string | null
19+
tool_response: Record<string, any> & {
20+
success?: boolean
21+
}
2122
}
2223

2324
export interface NotificationPayload {
24-
hook_type: 'Notification'
2525
session_id: string
26+
transcript_path: string
2627
message: string
27-
level: 'info' | 'warning' | 'error'
28+
title: string
2829
}
2930

3031
export interface StopPayload {
31-
hook_type: 'Stop'
3232
session_id: string
33+
transcript_path: string
34+
stop_hook_active: boolean
3335
}
3436

35-
export type HookPayload = PreToolUsePayload | PostToolUsePayload | NotificationPayload | StopPayload
37+
export interface SubagentStopPayload {
38+
session_id: string
39+
transcript_path: string
40+
stop_hook_active: boolean
41+
}
42+
43+
export type HookPayload =
44+
| (PreToolUsePayload & {hook_type: 'PreToolUse'})
45+
| (PostToolUsePayload & {hook_type: 'PostToolUse'})
46+
| (NotificationPayload & {hook_type: 'Notification'})
47+
| (StopPayload & {hook_type: 'Stop'})
48+
| (SubagentStopPayload & {hook_type: 'SubagentStop'})
49+
50+
// Base response fields available to all hooks
51+
export interface BaseHookResponse {
52+
continue?: boolean
53+
stopReason?: string
54+
suppressOutput?: boolean
55+
}
56+
57+
// PreToolUse specific response
58+
export interface PreToolUseResponse extends BaseHookResponse {
59+
decision?: 'approve' | 'block'
60+
reason?: string
61+
}
3662

63+
// PostToolUse specific response
64+
export interface PostToolUseResponse extends BaseHookResponse {
65+
decision?: 'block'
66+
reason?: string
67+
}
68+
69+
// Stop/SubagentStop specific response
70+
export interface StopResponse extends BaseHookResponse {
71+
decision?: 'block'
72+
reason?: string // Required when decision is 'block'
73+
}
74+
75+
// Legacy simple response for backward compatibility
3776
export interface HookResponse {
3877
action: 'continue' | 'block'
3978
stopReason?: string
@@ -46,16 +85,18 @@ export interface BashToolInput {
4685
}
4786

4887
// Hook handler types
49-
export type PreToolUseHandler = (payload: PreToolUsePayload) => Promise<HookResponse> | HookResponse
88+
export type PreToolUseHandler = (payload: PreToolUsePayload) => Promise<PreToolUseResponse> | PreToolUseResponse
5089
export type PostToolUseHandler = (payload: PostToolUsePayload) => Promise<void> | void
5190
export type NotificationHandler = (payload: NotificationPayload) => Promise<void> | void
5291
export type StopHandler = (payload: StopPayload) => Promise<void> | void
92+
export type SubagentStopHandler = (payload: SubagentStopPayload) => Promise<void> | void
5393

5494
export interface HookHandlers {
5595
preToolUse?: PreToolUseHandler
5696
postToolUse?: PostToolUseHandler
5797
notification?: NotificationHandler
5898
stop?: StopHandler
99+
subagentStop?: SubagentStopHandler
59100
}
60101

61102
// Session management utilities
@@ -106,45 +147,54 @@ export function runHook(handlers: HookHandlers): void {
106147
process.stdin.on('data', async (data) => {
107148
try {
108149
const inputData = JSON.parse(data.toString())
150+
// Add hook_type for internal processing (not part of official input schema)
109151
const payload: HookPayload = {
110152
...inputData,
111153
hook_type: hook_type as any,
112154
}
113155

114-
switch (hook_type) {
156+
switch (payload.hook_type) {
115157
case 'PreToolUse':
116158
if (handlers.preToolUse) {
117159
const response = await handlers.preToolUse(payload)
118160
console.log(JSON.stringify(response))
119161
} else {
120-
console.log(JSON.stringify({action: 'continue'}))
162+
console.log(JSON.stringify({}))
121163
}
122164
break
123165

124166
case 'PostToolUse':
125167
if (handlers.postToolUse) {
126168
await handlers.postToolUse(payload)
127169
}
128-
console.log(JSON.stringify({action: 'continue'}))
170+
console.log(JSON.stringify({}))
129171
break
130172

131173
case 'Notification':
132174
if (handlers.notification) {
133175
await handlers.notification(payload)
134176
}
135-
console.log(JSON.stringify({action: 'continue'}))
177+
console.log(JSON.stringify({}))
136178
break
137179

138180
case 'Stop':
139181
if (handlers.stop) {
140182
await handlers.stop(payload)
141183
}
142-
console.log(JSON.stringify({action: 'continue'}))
184+
console.log(JSON.stringify({}))
143185
process.exit(0)
144-
break
186+
return // Unreachable but satisfies linter
187+
188+
case 'SubagentStop':
189+
if (handlers.subagentStop) {
190+
await handlers.subagentStop(payload)
191+
}
192+
console.log(JSON.stringify({}))
193+
process.exit(0)
194+
return // Unreachable but satisfies linter
145195

146196
default:
147-
console.log(JSON.stringify({action: 'continue'}))
197+
console.log(JSON.stringify({}))
148198
}
149199
} catch (error) {
150200
console.error('Hook error:', error)

0 commit comments

Comments
 (0)