or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

configuration-options.mdcustom-tools.mderror-handling.mdhook-system.mdindex.mdinteractive-client.mdmessage-types.mdsimple-queries.mdtransport-system.md

hook-system.mddocs/

0

# Hook System

1

2

Python functions that the Claude Code application invokes at specific points of the Claude agent loop. Hooks provide deterministic processing and automated feedback for Claude, enabling custom validation, permission checks, and automated responses.

3

4

## Capabilities

5

6

### Hook Events

7

8

Supported hook event types in the Python SDK.

9

10

```python { .api }

11

HookEvent = (

12

Literal["PreToolUse"]

13

| Literal["PostToolUse"]

14

| Literal["UserPromptSubmit"]

15

| Literal["Stop"]

16

| Literal["SubagentStop"]

17

| Literal["PreCompact"]

18

)

19

```

20

21

**Hook Events:**

22

- `"PreToolUse"`: Before tool execution - can block or modify tool usage

23

- `"PostToolUse"`: After tool execution - process tool results

24

- `"UserPromptSubmit"`: When user submits a prompt - validate or modify input

25

- `"Stop"`: When Claude stops processing - cleanup or logging

26

- `"SubagentStop"`: When a subagent stops - subagent lifecycle management

27

- `"PreCompact"`: Before message compaction - control conversation history

28

29

### Hook Callback Function

30

31

Function signature for hook callbacks that process hook events.

32

33

```python { .api }

34

HookCallback = Callable[

35

[dict[str, Any], str | None, HookContext],

36

Awaitable[HookJSONOutput]

37

]

38

```

39

40

**Parameters:**

41

- `input`: Hook input data (varies by hook event type)

42

- `tool_use_id`: Tool use identifier (None for non-tool events)

43

- `context`: Hook execution context with signal support

44

45

### Hook Context

46

47

Context information provided to hook callbacks during execution.

48

49

```python { .api }

50

@dataclass

51

class HookContext:

52

"""Context information for hook callbacks."""

53

signal: Any | None = None # Future: abort signal support

54

```

55

56

### Hook Matcher

57

58

Configuration for matching specific events and associating them with callback functions.

59

60

```python { .api }

61

@dataclass

62

class HookMatcher:

63

"""Hook matcher configuration."""

64

65

matcher: str | None = None

66

hooks: list[HookCallback] = field(default_factory=list)

67

```

68

69

**Matcher Patterns:**

70

- Tool names: `"Bash"`, `"Write"`, `"Read"`

71

- Multiple tools: `"Write|MultiEdit|Edit"`

72

- All events: `None` or omit matcher

73

74

### Hook Output

75

76

Response structure that hooks return to control Claude Code behavior.

77

78

```python { .api }

79

class HookJSONOutput(TypedDict):

80

# Whether to block the action related to the hook

81

decision: NotRequired[Literal["block"]]

82

# Optionally add a system message that is not visible to Claude but saved in

83

# the chat transcript

84

systemMessage: NotRequired[str]

85

# See each hook's individual "Decision Control" section in the documentation

86

# for guidance

87

hookSpecificOutput: NotRequired[Any]

88

```

89

90

## Usage Examples

91

92

### Basic Tool Validation Hook

93

94

```python

95

from claude_code_sdk import ClaudeSDKClient, ClaudeCodeOptions, HookMatcher

96

97

async def validate_bash_commands(input_data, tool_use_id, context):

98

"""Validate bash commands before execution."""

99

tool_name = input_data.get("tool_name")

100

tool_input = input_data.get("tool_input", {})

101

102

if tool_name != "Bash":

103

return {} # Only validate Bash commands

104

105

command = tool_input.get("command", "")

106

dangerous_patterns = ["rm -rf", "format", "dd if=", "> /dev/"]

107

108

for pattern in dangerous_patterns:

109

if pattern in command:

110

return {

111

"decision": "block",

112

"systemMessage": f"Blocked dangerous command: {command[:50]}...",

113

"hookSpecificOutput": {

114

"hookEventName": "PreToolUse",

115

"permissionDecision": "deny",

116

"permissionDecisionReason": f"Command contains dangerous pattern: {pattern}",

117

}

118

}

119

120

return {} # Allow command

121

122

async def main():

123

options = ClaudeCodeOptions(

124

allowed_tools=["Bash"],

125

hooks={

126

"PreToolUse": [

127

HookMatcher(matcher="Bash", hooks=[validate_bash_commands])

128

]

129

}

130

)

131

132

async with ClaudeSDKClient(options=options) as client:

133

# This will be blocked

134

await client.query("Run the bash command: rm -rf /important-data")

135

136

async for msg in client.receive_response():

137

print(msg)

138

```

139

140

### Multiple Hook Events

141

142

```python

143

async def log_tool_use(input_data, tool_use_id, context):

144

"""Log all tool usage for monitoring."""

145

tool_name = input_data.get("tool_name")

146

print(f"Tool used: {tool_name} (ID: {tool_use_id})")

147

148

return {

149

"systemMessage": f"Logged tool usage: {tool_name}"

150

}

151

152

async def log_tool_result(input_data, tool_use_id, context):

153

"""Log tool results."""

154

tool_name = input_data.get("tool_name")

155

success = input_data.get("success", False)

156

print(f"Tool {tool_name} {'succeeded' if success else 'failed'}")

157

158

return {}

159

160

async def validate_user_input(input_data, tool_use_id, context):

161

"""Validate user prompts."""

162

content = input_data.get("content", "")

163

164

if "secret" in content.lower():

165

return {

166

"decision": "block",

167

"systemMessage": "Blocked prompt containing sensitive information"

168

}

169

170

return {}

171

172

async def main():

173

options = ClaudeCodeOptions(

174

allowed_tools=["Read", "Write", "Bash"],

175

hooks={

176

"PreToolUse": [

177

HookMatcher(hooks=[log_tool_use]) # All tools

178

],

179

"PostToolUse": [

180

HookMatcher(hooks=[log_tool_result]) # All tools

181

],

182

"UserPromptSubmit": [

183

HookMatcher(hooks=[validate_user_input])

184

]

185

}

186

)

187

188

async with ClaudeSDKClient(options=options) as client:

189

await client.query("Create a Python file with my secret password")

190

191

async for msg in client.receive_response():

192

print(msg)

193

```

194

195

### Conditional Tool Modification

196

197

```python

198

async def modify_file_operations(input_data, tool_use_id, context):

199

"""Modify file operations to add safety checks."""

200

tool_name = input_data.get("tool_name")

201

tool_input = input_data.get("tool_input", {})

202

203

if tool_name == "Write":

204

file_path = tool_input.get("file_path", "")

205

206

# Prevent overwriting important files

207

if file_path.endswith((".env", "config.json", ".env.local")):

208

return {

209

"hookSpecificOutput": {

210

"hookEventName": "PreToolUse",

211

"modifiedInput": {

212

**tool_input,

213

"file_path": file_path + ".backup"

214

}

215

},

216

"systemMessage": f"Redirected write to backup file: {file_path}.backup"

217

}

218

219

return {}

220

221

async def main():

222

options = ClaudeCodeOptions(

223

allowed_tools=["Read", "Write"],

224

hooks={

225

"PreToolUse": [

226

HookMatcher(matcher="Write", hooks=[modify_file_operations])

227

]

228

}

229

)

230

231

async with ClaudeSDKClient(options=options) as client:

232

await client.query("Create a .env file with database credentials")

233

234

async for msg in client.receive_response():

235

print(msg)

236

```

237

238

### Complex Workflow Hook

239

240

```python

241

import json

242

from datetime import datetime

243

244

class WorkflowTracker:

245

def __init__(self):

246

self.operations = []

247

self.start_time = None

248

249

def log_operation(self, operation):

250

self.operations.append({

251

"timestamp": datetime.now().isoformat(),

252

"operation": operation

253

})

254

255

tracker = WorkflowTracker()

256

257

async def track_workflow_start(input_data, tool_use_id, context):

258

"""Track workflow initiation."""

259

if tracker.start_time is None:

260

tracker.start_time = datetime.now()

261

262

content = input_data.get("content", "")

263

tracker.log_operation({"type": "user_prompt", "content": content[:100]})

264

265

return {

266

"systemMessage": "Workflow tracking initiated"

267

}

268

269

async def track_tool_execution(input_data, tool_use_id, context):

270

"""Track all tool executions in workflow."""

271

tool_name = input_data.get("tool_name")

272

tool_input = input_data.get("tool_input", {})

273

274

tracker.log_operation({

275

"type": "tool_use",

276

"tool": tool_name,

277

"input_preview": str(tool_input)[:100]

278

})

279

280

return {}

281

282

async def finalize_workflow(input_data, tool_use_id, context):

283

"""Generate workflow summary on completion."""

284

duration = datetime.now() - tracker.start_time if tracker.start_time else None

285

286

summary = {

287

"duration_seconds": duration.total_seconds() if duration else 0,

288

"total_operations": len(tracker.operations),

289

"tools_used": list(set(

290

op["operation"]["tool"]

291

for op in tracker.operations

292

if op["operation"].get("type") == "tool_use"

293

))

294

}

295

296

return {

297

"systemMessage": f"Workflow completed: {json.dumps(summary, indent=2)}"

298

}

299

300

async def main():

301

options = ClaudeCodeOptions(

302

allowed_tools=["Read", "Write", "Bash", "Edit"],

303

hooks={

304

"UserPromptSubmit": [

305

HookMatcher(hooks=[track_workflow_start])

306

],

307

"PreToolUse": [

308

HookMatcher(hooks=[track_tool_execution])

309

],

310

"Stop": [

311

HookMatcher(hooks=[finalize_workflow])

312

]

313

}

314

)

315

316

async with ClaudeSDKClient(options=options) as client:

317

await client.query("Create a web server, write tests, and run them")

318

319

async for msg in client.receive_response():

320

print(msg)

321

```

322

323

### Custom Permission Logic

324

325

```python

326

class PermissionManager:

327

def __init__(self):

328

self.allowed_files = {".py", ".js", ".html", ".css", ".md"}

329

self.blocked_commands = ["curl", "wget", "ssh"]

330

331

async def check_file_permission(self, input_data, tool_use_id, context):

332

"""Check file operation permissions."""

333

tool_name = input_data.get("tool_name")

334

tool_input = input_data.get("tool_input", {})

335

336

if tool_name in ["Write", "Edit", "MultiEdit"]:

337

file_path = tool_input.get("file_path", "")

338

file_ext = Path(file_path).suffix if file_path else ""

339

340

if file_ext not in self.allowed_files:

341

return {

342

"decision": "block",

343

"systemMessage": f"File type {file_ext} not allowed",

344

"hookSpecificOutput": {

345

"hookEventName": "PreToolUse",

346

"permissionDecision": "deny",

347

"permissionDecisionReason": f"File extension {file_ext} not in allowed list"

348

}

349

}

350

351

elif tool_name == "Bash":

352

command = tool_input.get("command", "")

353

for blocked_cmd in self.blocked_commands:

354

if blocked_cmd in command:

355

return {

356

"decision": "block",

357

"systemMessage": f"Command '{blocked_cmd}' blocked",

358

"hookSpecificOutput": {

359

"hookEventName": "PreToolUse",

360

"permissionDecision": "deny",

361

"permissionDecisionReason": f"Command contains blocked executable: {blocked_cmd}"

362

}

363

}

364

365

return {} # Allow operation

366

367

async def main():

368

permission_manager = PermissionManager()

369

370

options = ClaudeCodeOptions(

371

allowed_tools=["Read", "Write", "Edit", "Bash"],

372

hooks={

373

"PreToolUse": [

374

HookMatcher(hooks=[permission_manager.check_file_permission])

375

]

376

}

377

)

378

379

async with ClaudeSDKClient(options=options) as client:

380

await client.query("Download a file using curl and save it as data.exe")

381

382

async for msg in client.receive_response():

383

print(msg)

384

```

385

386

## Hook Event Input Data

387

388

Different hook events receive different input data structures:

389

390

### PreToolUse / PostToolUse

391

```python

392

{

393

"tool_name": "Bash",

394

"tool_input": {"command": "ls -la"},

395

"tool_use_id": "toolu_123456",

396

# PostToolUse additionally includes:

397

"success": True,

398

"result": "...",

399

"error": None

400

}

401

```

402

403

### UserPromptSubmit

404

```python

405

{

406

"content": "Create a web server",

407

"session_id": "default",

408

"timestamp": "2024-01-01T12:00:00Z"

409

}

410

```

411

412

### Stop / SubagentStop

413

```python

414

{

415

"reason": "completed",

416

"session_id": "default",

417

"duration_ms": 5000,

418

"num_turns": 3

419

}

420

```

421

422

### PreCompact

423

```python

424

{

425

"message_count": 25,

426

"total_tokens": 4000,

427

"compact_threshold": 3500

428

}

429

```

430

431

## Hook Execution Flow

432

433

1. **Event Trigger**: Claude Code reaches a hook point in the agent loop

434

2. **Matcher Evaluation**: Check if any matchers match the current event/tool

435

3. **Hook Execution**: Call all matching hook functions in registration order

436

4. **Decision Aggregation**: Combine all hook responses

437

5. **Action Application**: Apply decisions (block, modify, log, etc.)

438

439

## Hook Limitations

440

441

**Python SDK Restrictions:**

442

- SessionStart, SessionEnd, and Notification hooks are not supported

443

- No support for "continue", "stopReason", and "suppressOutput" controls

444

- Hook execution is synchronous within the Claude Code process

445

446

**Performance Considerations:**

447

- Hooks execute synchronously and can impact response times

448

- Complex hook logic should be optimized for speed

449

- Avoid blocking I/O operations in hook functions

450

451

## Integration with Permission System

452

453

Hooks work alongside the permission system and can:

454

- Override permission decisions through `hookSpecificOutput`

455

- Provide custom permission validation logic

456

- Log permission decisions for audit purposes

457

- Modify tool inputs before permission checks

458

459

For permission callback configuration, see [Configuration and Options](./configuration-options.md).

460

461

For hook usage with custom tools, see [Custom Tools](./custom-tools.md).