or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

agent-definitions.mdagents.mdclient.mdconfiguration-options.mdcontent-blocks.mdcore-query-interface.mdcustom-tools.mderror-handling.mderrors.mdhook-system.mdhooks.mdindex.mdmcp-config.mdmcp-server-configuration.mdmessages-and-content.mdmessages.mdoptions.mdpermission-control.mdpermissions.mdquery.mdtransport.md
COMPLETION_SUMMARY.md

hooks.mddocs/

0

# Hooks

1

2

Hooks allow you to inject custom Python functions at specific points in Claude's execution loop. They enable deterministic behavior, logging, validation, and custom control flow based on Claude's actions.

3

4

## Capabilities

5

6

### Hook Events

7

8

Supported hook event types that can trigger custom logic.

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

Supported hook event types.

21

22

Hook events allow you to inject custom logic at specific points in Claude's

23

execution loop:

24

25

- PreToolUse: Before a tool is executed

26

- PostToolUse: After a tool completes execution

27

- UserPromptSubmit: When user submits a prompt

28

- Stop: When conversation stops (main agent)

29

- SubagentStop: When a subagent conversation stops

30

- PreCompact: Before conversation history is compacted

31

32

Note: Due to setup limitations, the Python SDK does not support SessionStart,

33

SessionEnd, and Notification hooks that are available in the TypeScript SDK.

34

"""

35

```

36

37

### Hook Callback

38

39

Type alias for hook callback functions.

40

41

```python { .api }

42

HookCallback = Callable[

43

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

44

Awaitable[HookJSONOutput]

45

]

46

"""

47

Hook callback function type.

48

49

A hook callback is an async function invoked when a hook event occurs.

50

51

Args:

52

input: Hook input data. Structure varies by hook type:

53

- PreToolUse: {"name": tool_name, "input": tool_input}

54

- PostToolUse: {"name": tool_name, "output": tool_output}

55

- UserPromptSubmit: {"prompt": prompt_text}

56

- Stop/SubagentStop: {"result": result_data}

57

- PreCompact: {"messages": messages_to_compact}

58

tool_use_id: Optional tool use ID (relevant for tool-related hooks)

59

context: Hook context with additional information

60

61

Returns:

62

HookJSONOutput dictionary with optional decision, systemMessage,

63

and hookSpecificOutput fields

64

65

See https://docs.anthropic.com/en/docs/claude-code/hooks#hook-input

66

for detailed input structures for each hook type.

67

"""

68

```

69

70

### Hook Context

71

72

Context information passed to hook callbacks.

73

74

```python { .api }

75

@dataclass

76

class HookContext:

77

"""

78

Context information for hook callbacks.

79

80

Provides additional context and control mechanisms for hooks.

81

Future versions may add more fields.

82

83

Attributes:

84

signal: Abort signal support (future feature, currently None)

85

"""

86

87

signal: Any | None = None

88

"""Future: abort signal support.

89

90

Reserved for future use to allow canceling hook execution.

91

Currently always None.

92

"""

93

```

94

95

### Hook Output

96

97

Output structure returned by hook callbacks.

98

99

```python { .api }

100

class HookJSONOutput(TypedDict):

101

"""

102

Hook output structure.

103

104

Hooks return this dictionary to control behavior and communicate results.

105

106

Fields:

107

decision: Optional. Set to "block" to prevent the action

108

systemMessage: Optional. Add a system message to chat transcript

109

hookSpecificOutput: Optional. Hook-specific data

110

111

Note: Currently, "continue", "stopReason", and "suppressOutput" from the

112

TypeScript SDK are not supported in the Python SDK.

113

114

See https://docs.anthropic.com/en/docs/claude-code/hooks#advanced%3A-json-output

115

for detailed documentation.

116

"""

117

118

decision: NotRequired[Literal["block"]]

119

"""Whether to block the action related to the hook.

120

121

When set to "block", the action associated with the hook will be prevented:

122

- PreToolUse: Block tool execution

123

- UserPromptSubmit: Block prompt submission

124

125

If not specified or set to any other value, the action proceeds normally.

126

"""

127

128

systemMessage: NotRequired[str]

129

"""Optional system message.

130

131

Add a system message that is saved in the chat transcript but not visible

132

to Claude. Useful for logging, debugging, or adding context for later review.

133

"""

134

135

hookSpecificOutput: NotRequired[Any]

136

"""Hook-specific output data.

137

138

Each hook type may define specific output fields. See individual hook

139

documentation for guidance on what can be included here.

140

"""

141

```

142

143

### Hook Matcher

144

145

Configuration for matching hooks to specific events.

146

147

```python { .api }

148

@dataclass

149

class HookMatcher:

150

"""

151

Hook matcher configuration.

152

153

Defines which events should trigger which hooks. Matchers filter events

154

based on patterns like tool names or other criteria.

155

156

Attributes:

157

matcher: Optional pattern to filter events

158

hooks: List of callback functions to invoke

159

"""

160

161

matcher: str | None = None

162

"""Matcher pattern string.

163

164

Filters which events trigger the hooks. The format depends on the hook type:

165

166

For PreToolUse/PostToolUse:

167

- Tool name: "Bash" (exact match)

168

- Multiple tools: "Write|Edit|MultiEdit" (OR pattern)

169

- All tools: None or "" (match any tool)

170

171

For other hooks:

172

- Usually None (no filtering)

173

174

See https://docs.anthropic.com/en/docs/claude-code/hooks#structure

175

for detailed matcher syntax.

176

"""

177

178

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

179

"""List of hook callback functions.

180

181

All callbacks in this list will be invoked when the matcher condition

182

is met. Callbacks are executed in order.

183

"""

184

```

185

186

## Usage Examples

187

188

### Basic PreToolUse Hook

189

190

```python

191

from claude_agent_sdk import (

192

ClaudeAgentOptions, HookMatcher, HookContext, HookJSONOutput, query

193

)

194

195

async def log_tool_use(input_data: dict, tool_use_id: str | None, context: HookContext) -> HookJSONOutput:

196

"""Log when tools are used."""

197

tool_name = input_data.get("name")

198

tool_input = input_data.get("input")

199

print(f"Tool called: {tool_name}")

200

print(f"Input: {tool_input}")

201

return {}

202

203

options = ClaudeAgentOptions(

204

hooks={

205

"PreToolUse": [

206

HookMatcher(matcher=None, hooks=[log_tool_use])

207

]

208

}

209

)

210

211

async for msg in query(prompt="List files in current directory", options=options):

212

print(msg)

213

```

214

215

### Blocking Dangerous Commands

216

217

```python

218

from claude_agent_sdk import (

219

ClaudeAgentOptions, HookMatcher, HookContext, HookJSONOutput, query

220

)

221

222

async def block_dangerous_bash(

223

input_data: dict,

224

tool_use_id: str | None,

225

context: HookContext

226

) -> HookJSONOutput:

227

"""Block dangerous bash commands."""

228

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

229

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

230

231

# Check for dangerous commands

232

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

233

234

for keyword in dangerous_keywords:

235

if keyword in command:

236

return {

237

"decision": "block",

238

"systemMessage": f"Blocked dangerous command: {command}"

239

}

240

241

return {}

242

243

options = ClaudeAgentOptions(

244

allowed_tools=["Bash"],

245

hooks={

246

"PreToolUse": [

247

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

248

]

249

}

250

)

251

252

async for msg in query(prompt="Delete all files", options=options):

253

print(msg)

254

```

255

256

### PostToolUse Validation

257

258

```python

259

from claude_agent_sdk import (

260

ClaudeAgentOptions, HookMatcher, HookContext, HookJSONOutput, query

261

)

262

263

async def validate_write_output(

264

input_data: dict,

265

tool_use_id: str | None,

266

context: HookContext

267

) -> HookJSONOutput:

268

"""Validate file write operations."""

269

tool_name = input_data.get("name")

270

tool_output = input_data.get("output", {})

271

272

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

273

# Check if write was successful

274

is_error = tool_output.get("is_error", False)

275

276

if is_error:

277

return {

278

"systemMessage": f"File operation failed: {tool_output.get('content')}"

279

}

280

else:

281

return {

282

"systemMessage": "File operation completed successfully"

283

}

284

285

return {}

286

287

options = ClaudeAgentOptions(

288

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

289

hooks={

290

"PostToolUse": [

291

HookMatcher(matcher="Write|Edit", hooks=[validate_write_output])

292

]

293

}

294

)

295

296

async for msg in query(prompt="Create a new Python file", options=options):

297

print(msg)

298

```

299

300

### Multiple Hooks for Same Event

301

302

```python

303

from claude_agent_sdk import (

304

ClaudeAgentOptions, HookMatcher, HookContext, HookJSONOutput, query

305

)

306

307

async def log_hook(input_data: dict, tool_use_id: str | None, context: HookContext) -> HookJSONOutput:

308

"""Log tool usage."""

309

print(f"[LOG] Tool: {input_data.get('name')}")

310

return {}

311

312

async def audit_hook(input_data: dict, tool_use_id: str | None, context: HookContext) -> HookJSONOutput:

313

"""Audit tool usage."""

314

tool_name = input_data.get("name")

315

tool_input = input_data.get("input")

316

317

# Save to audit log

318

with open("audit.log", "a") as f:

319

f.write(f"Tool: {tool_name}, Input: {tool_input}\n")

320

321

return {}

322

323

async def security_hook(input_data: dict, tool_use_id: str | None, context: HookContext) -> HookJSONOutput:

324

"""Security checks."""

325

tool_name = input_data.get("name")

326

327

# Block certain tools during business hours

328

from datetime import datetime

329

hour = datetime.now().hour

330

331

if 9 <= hour <= 17 and tool_name == "Bash":

332

return {

333

"decision": "block",

334

"systemMessage": "Bash commands blocked during business hours"

335

}

336

337

return {}

338

339

options = ClaudeAgentOptions(

340

hooks={

341

"PreToolUse": [

342

HookMatcher(

343

matcher=None,

344

hooks=[log_hook, audit_hook, security_hook]

345

)

346

]

347

}

348

)

349

350

async for msg in query(prompt="Analyze this project", options=options):

351

print(msg)

352

```

353

354

### User Prompt Validation

355

356

```python

357

from claude_agent_sdk import (

358

ClaudeAgentOptions, HookMatcher, HookContext, HookJSONOutput, query

359

)

360

361

async def validate_prompt(

362

input_data: dict,

363

tool_use_id: str | None,

364

context: HookContext

365

) -> HookJSONOutput:

366

"""Validate user prompts before submission."""

367

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

368

369

# Block prompts with sensitive information

370

sensitive_patterns = ["password", "api_key", "secret", "token"]

371

372

for pattern in sensitive_patterns:

373

if pattern.lower() in prompt.lower():

374

return {

375

"decision": "block",

376

"systemMessage": f"Blocked prompt containing sensitive data: {pattern}"

377

}

378

379

return {}

380

381

options = ClaudeAgentOptions(

382

hooks={

383

"UserPromptSubmit": [

384

HookMatcher(matcher=None, hooks=[validate_prompt])

385

]

386

}

387

)

388

389

async for msg in query(prompt="What is my password?", options=options):

390

print(msg)

391

```

392

393

### Stop Hook for Cleanup

394

395

```python

396

from claude_agent_sdk import (

397

ClaudeAgentOptions, HookMatcher, HookContext, HookJSONOutput, query

398

)

399

400

async def cleanup_on_stop(

401

input_data: dict,

402

tool_use_id: str | None,

403

context: HookContext

404

) -> HookJSONOutput:

405

"""Clean up resources when conversation stops."""

406

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

407

408

print(f"Conversation ended")

409

print(f"Result: {result}")

410

411

# Perform cleanup

412

# - Close database connections

413

# - Save state

414

# - Log metrics

415

416

return {

417

"systemMessage": "Cleanup completed"

418

}

419

420

options = ClaudeAgentOptions(

421

hooks={

422

"Stop": [

423

HookMatcher(matcher=None, hooks=[cleanup_on_stop])

424

]

425

}

426

)

427

428

async for msg in query(prompt="Hello Claude", options=options):

429

print(msg)

430

```

431

432

### Rate Limiting Hook

433

434

```python

435

from datetime import datetime, timedelta

436

from claude_agent_sdk import (

437

ClaudeAgentOptions, HookMatcher, HookContext, HookJSONOutput, query

438

)

439

440

# Rate limiting state

441

tool_usage = {}

442

RATE_LIMIT = 5 # Max 5 calls per minute

443

444

async def rate_limit_tools(

445

input_data: dict,

446

tool_use_id: str | None,

447

context: HookContext

448

) -> HookJSONOutput:

449

"""Rate limit tool usage."""

450

tool_name = input_data.get("name")

451

now = datetime.now()

452

453

# Initialize tracking for this tool

454

if tool_name not in tool_usage:

455

tool_usage[tool_name] = []

456

457

# Remove old entries (older than 1 minute)

458

tool_usage[tool_name] = [

459

ts for ts in tool_usage[tool_name]

460

if now - ts < timedelta(minutes=1)

461

]

462

463

# Check rate limit

464

if len(tool_usage[tool_name]) >= RATE_LIMIT:

465

return {

466

"decision": "block",

467

"systemMessage": f"Rate limit exceeded for {tool_name} (max {RATE_LIMIT}/min)"

468

}

469

470

# Record usage

471

tool_usage[tool_name].append(now)

472

473

return {}

474

475

options = ClaudeAgentOptions(

476

allowed_tools=["Bash"],

477

hooks={

478

"PreToolUse": [

479

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

480

]

481

}

482

)

483

484

async for msg in query(prompt="Run many commands", options=options):

485

print(msg)

486

```

487

488

### Conditional Tool Blocking

489

490

```python

491

from claude_agent_sdk import (

492

ClaudeAgentOptions, HookMatcher, HookContext, HookJSONOutput, query

493

)

494

495

# Application state

496

class AppState:

497

def __init__(self):

498

self.readonly_mode = False

499

500

app_state = AppState()

501

502

async def enforce_readonly(

503

input_data: dict,

504

tool_use_id: str | None,

505

context: HookContext

506

) -> HookJSONOutput:

507

"""Block write operations in readonly mode."""

508

if not app_state.readonly_mode:

509

return {}

510

511

tool_name = input_data.get("name")

512

write_tools = ["Write", "Edit", "MultiEdit", "Bash"]

513

514

if tool_name in write_tools:

515

return {

516

"decision": "block",

517

"systemMessage": f"Blocked {tool_name} in readonly mode"

518

}

519

520

return {}

521

522

options = ClaudeAgentOptions(

523

hooks={

524

"PreToolUse": [

525

HookMatcher(matcher=None, hooks=[enforce_readonly])

526

]

527

}

528

)

529

530

# Enable readonly mode

531

app_state.readonly_mode = True

532

533

async for msg in query(prompt="Modify this file", options=options):

534

print(msg)

535

```

536

537

### Logging and Metrics

538

539

```python

540

import time

541

from claude_agent_sdk import (

542

ClaudeAgentOptions, HookMatcher, HookContext, HookJSONOutput, query

543

)

544

545

# Metrics storage

546

metrics = {

547

"tool_calls": {},

548

"tool_durations": {}

549

}

550

start_times = {}

551

552

async def track_tool_start(

553

input_data: dict,

554

tool_use_id: str | None,

555

context: HookContext

556

) -> HookJSONOutput:

557

"""Track tool execution start."""

558

tool_name = input_data.get("name")

559

560

# Count tool calls

561

metrics["tool_calls"][tool_name] = metrics["tool_calls"].get(tool_name, 0) + 1

562

563

# Record start time

564

if tool_use_id:

565

start_times[tool_use_id] = time.time()

566

567

return {}

568

569

async def track_tool_end(

570

input_data: dict,

571

tool_use_id: str | None,

572

context: HookContext

573

) -> HookJSONOutput:

574

"""Track tool execution completion."""

575

tool_name = input_data.get("name")

576

577

# Calculate duration

578

if tool_use_id and tool_use_id in start_times:

579

duration = time.time() - start_times[tool_use_id]

580

581

if tool_name not in metrics["tool_durations"]:

582

metrics["tool_durations"][tool_name] = []

583

584

metrics["tool_durations"][tool_name].append(duration)

585

586

del start_times[tool_use_id]

587

588

return {}

589

590

async def print_metrics(

591

input_data: dict,

592

tool_use_id: str | None,

593

context: HookContext

594

) -> HookJSONOutput:

595

"""Print metrics when conversation stops."""

596

print("\n=== Tool Usage Metrics ===")

597

print("\nTool Calls:")

598

for tool, count in metrics["tool_calls"].items():

599

print(f" {tool}: {count}")

600

601

print("\nAverage Durations:")

602

for tool, durations in metrics["tool_durations"].items():

603

avg = sum(durations) / len(durations)

604

print(f" {tool}: {avg:.3f}s")

605

606

return {}

607

608

options = ClaudeAgentOptions(

609

hooks={

610

"PreToolUse": [

611

HookMatcher(matcher=None, hooks=[track_tool_start])

612

],

613

"PostToolUse": [

614

HookMatcher(matcher=None, hooks=[track_tool_end])

615

],

616

"Stop": [

617

HookMatcher(matcher=None, hooks=[print_metrics])

618

]

619

}

620

)

621

622

async for msg in query(prompt="Analyze this project", options=options):

623

print(msg)

624

```

625

626

### Hook with Hook-Specific Output

627

628

```python

629

from claude_agent_sdk import (

630

ClaudeAgentOptions, HookMatcher, HookContext, HookJSONOutput, query

631

)

632

633

async def custom_tool_output(

634

input_data: dict,

635

tool_use_id: str | None,

636

context: HookContext

637

) -> HookJSONOutput:

638

"""Modify tool behavior with hook-specific output."""

639

tool_name = input_data.get("name")

640

641

# Example: Add metadata to tool output

642

return {

643

"systemMessage": f"Tool {tool_name} executed",

644

"hookSpecificOutput": {

645

"metadata": {

646

"tool": tool_name,

647

"timestamp": time.time(),

648

"source": "hook"

649

}

650

}

651

}

652

653

options = ClaudeAgentOptions(

654

hooks={

655

"PostToolUse": [

656

HookMatcher(matcher=None, hooks=[custom_tool_output])

657

]

658

}

659

)

660

661

async for msg in query(prompt="List files", options=options):

662

print(msg)

663

```

664

665

### Multiple Hook Events

666

667

```python

668

from claude_agent_sdk import (

669

ClaudeAgentOptions, HookMatcher, HookContext, HookJSONOutput, query

670

)

671

672

async def log_pre_tool(input_data: dict, tool_use_id: str | None, context: HookContext) -> HookJSONOutput:

673

print(f"[PRE] Tool: {input_data.get('name')}")

674

return {}

675

676

async def log_post_tool(input_data: dict, tool_use_id: str | None, context: HookContext) -> HookJSONOutput:

677

print(f"[POST] Tool: {input_data.get('name')}")

678

return {}

679

680

async def log_prompt(input_data: dict, tool_use_id: str | None, context: HookContext) -> HookJSONOutput:

681

print(f"[PROMPT] {input_data.get('prompt')}")

682

return {}

683

684

async def log_stop(input_data: dict, tool_use_id: str | None, context: HookContext) -> HookJSONOutput:

685

print("[STOP] Conversation ended")

686

return {}

687

688

options = ClaudeAgentOptions(

689

hooks={

690

"PreToolUse": [HookMatcher(matcher=None, hooks=[log_pre_tool])],

691

"PostToolUse": [HookMatcher(matcher=None, hooks=[log_post_tool])],

692

"UserPromptSubmit": [HookMatcher(matcher=None, hooks=[log_prompt])],

693

"Stop": [HookMatcher(matcher=None, hooks=[log_stop])]

694

}

695

)

696

697

async for msg in query(prompt="Hello Claude", options=options):

698

print(msg)

699

```

700