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

hook-system.mddocs/

0

# Hook System

1

2

The hook system allows you to intercept and control Claude's operations at specific points in the agent loop. Hooks can modify inputs, add context, block execution, or trigger custom logic during PreToolUse, PostToolUse, UserPromptSubmit, Stop, SubagentStop, and PreCompact events.

3

4

## Capabilities

5

6

### Hook Events

7

8

Six hook event types are supported.

9

10

```python { .api }

11

HookEvent = Literal[

12

"PreToolUse",

13

"PostToolUse",

14

"UserPromptSubmit",

15

"Stop",

16

"SubagentStop",

17

"PreCompact"

18

]

19

```

20

21

**Event Types:**

22

23

- `PreToolUse`: Before a tool is executed (can modify input or block execution)

24

- `PostToolUse`: After a tool is executed (can add context to results)

25

- `UserPromptSubmit`: When user submits a prompt (can add context)

26

- `Stop`: When agent execution stops

27

- `SubagentStop`: When a sub-agent stops

28

- `PreCompact`: Before transcript compaction

29

30

### Hook Matcher

31

32

Configure hooks with pattern matching.

33

34

```python { .api }

35

@dataclass

36

class HookMatcher:

37

"""Hook configuration with pattern matching."""

38

39

matcher: str | None = None

40

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

41

```

42

43

**Fields:**

44

45

- `matcher` (str | None): Tool name pattern for PreToolUse and PostToolUse. Examples:

46

- `"Bash"`: Match only Bash tool

47

- `"Write|Edit"`: Match Write or Edit tools

48

- `"Write|MultiEdit|Edit"`: Match multiple tools

49

- `None`: Match all tools (for PreToolUse/PostToolUse) or not applicable (for other hooks)

50

51

- `hooks` (list[HookCallback]): List of callback functions to execute for this matcher.

52

53

**Usage Example:**

54

55

```python

56

from claude_agent_sdk import HookMatcher, ClaudeAgentOptions

57

58

async def my_hook(input, tool_use_id, context):

59

return {}

60

61

# Match specific tool

62

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

63

64

# Match multiple tools

65

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

66

67

# Match all tools

68

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

69

70

# Use in options

71

options = ClaudeAgentOptions(

72

hooks={

73

"PreToolUse": [matcher]

74

}

75

)

76

```

77

78

### Hook Callback

79

80

Callback function type for hooks.

81

82

```python { .api }

83

HookCallback = Callable[

84

[HookInput, str | None, HookContext],

85

Awaitable[HookJSONOutput]

86

]

87

```

88

89

**Parameters:**

90

91

1. `input` (HookInput): Hook-specific input with discriminated union based on `hook_event_name`

92

2. `tool_use_id` (str | None): Optional tool use identifier

93

3. `context` (HookContext): Hook context with settings directory

94

95

**Returns:**

96

97

`HookJSONOutput`: Either `AsyncHookJSONOutput` or `SyncHookJSONOutput`

98

99

**Signature Example:**

100

101

```python

102

async def my_hook_callback(

103

input: HookInput,

104

tool_use_id: str | None,

105

context: HookContext

106

) -> HookJSONOutput:

107

# Hook logic

108

return {}

109

```

110

111

### Hook Context

112

113

Context information provided to hook callbacks.

114

115

```python { .api }

116

class HookContext(TypedDict):

117

"""Context provided to hooks."""

118

119

signal: Any | None

120

```

121

122

**Fields:**

123

124

- `signal` (Any | None): Reserved for future abort signal support. Currently always None.

125

126

**Usage Example:**

127

128

```python

129

async def my_hook(input, tool_use_id, context):

130

signal = context["signal"]

131

# Future: Use signal for abort operations

132

return {}

133

```

134

135

## Hook Input Types

136

137

All hook inputs extend `BaseHookInput` and use discriminated unions based on `hook_event_name`.

138

139

### Base Hook Input

140

141

Base fields present in all hook inputs.

142

143

```python { .api }

144

class BaseHookInput(TypedDict):

145

"""Base hook input fields present across many hook events."""

146

147

session_id: str

148

transcript_path: str

149

cwd: str

150

permission_mode: NotRequired[str]

151

```

152

153

**Fields:**

154

155

- `session_id` (str): Session identifier

156

- `transcript_path` (str): Path to conversation transcript file

157

- `cwd` (str): Current working directory

158

- `permission_mode` (str, optional): Current permission mode

159

160

### PreToolUse Hook Input

161

162

Input for PreToolUse hooks (before tool execution).

163

164

```python { .api }

165

class PreToolUseHookInput(BaseHookInput):

166

"""Input data for PreToolUse hook events."""

167

168

hook_event_name: Literal["PreToolUse"]

169

tool_name: str

170

tool_input: dict[str, Any]

171

```

172

173

**Fields:**

174

175

- `hook_event_name`: Always `"PreToolUse"`

176

- `tool_name` (str): Name of tool being invoked

177

- `tool_input` (dict[str, Any]): Tool input parameters

178

179

**Usage Example:**

180

181

```python

182

async def pre_tool_use_hook(input, tool_use_id, context):

183

if input["hook_event_name"] == "PreToolUse":

184

tool_name = input["tool_name"]

185

tool_input = input["tool_input"]

186

187

# Validate or modify input

188

if tool_name == "Bash":

189

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

190

if "rm -rf" in command:

191

return {

192

"decision": "block",

193

"reason": "Dangerous command blocked"

194

}

195

196

# Allow with modified input

197

return {

198

"hookSpecificOutput": {

199

"hookEventName": "PreToolUse",

200

"permissionDecision": "allow",

201

"updatedInput": tool_input

202

}

203

}

204

205

return {}

206

```

207

208

### PostToolUse Hook Input

209

210

Input for PostToolUse hooks (after tool execution).

211

212

```python { .api }

213

class PostToolUseHookInput(BaseHookInput):

214

"""Input data for PostToolUse hook events."""

215

216

hook_event_name: Literal["PostToolUse"]

217

tool_name: str

218

tool_input: dict[str, Any]

219

tool_response: Any

220

```

221

222

**Fields:**

223

224

- `hook_event_name`: Always `"PostToolUse"`

225

- `tool_name` (str): Name of tool that was invoked

226

- `tool_input` (dict[str, Any]): Tool input parameters

227

- `tool_response` (Any): Tool execution response

228

229

**Usage Example:**

230

231

```python

232

async def post_tool_use_hook(input, tool_use_id, context):

233

if input["hook_event_name"] == "PostToolUse":

234

tool_name = input["tool_name"]

235

tool_response = input["tool_response"]

236

237

# Add context based on results

238

if tool_name == "Bash":

239

return {

240

"hookSpecificOutput": {

241

"hookEventName": "PostToolUse",

242

"additionalContext": "Command executed successfully"

243

}

244

}

245

246

return {}

247

```

248

249

### UserPromptSubmit Hook Input

250

251

Input for UserPromptSubmit hooks (when user submits prompt).

252

253

```python { .api }

254

class UserPromptSubmitHookInput(BaseHookInput):

255

"""Input data for UserPromptSubmit hook events."""

256

257

hook_event_name: Literal["UserPromptSubmit"]

258

prompt: str

259

```

260

261

**Fields:**

262

263

- `hook_event_name`: Always `"UserPromptSubmit"`

264

- `prompt` (str): User prompt text

265

266

**Usage Example:**

267

268

```python

269

async def user_prompt_hook(input, tool_use_id, context):

270

if input["hook_event_name"] == "UserPromptSubmit":

271

prompt = input["prompt"]

272

273

# Add context to prompt

274

return {

275

"hookSpecificOutput": {

276

"hookEventName": "UserPromptSubmit",

277

"additionalContext": f"Processing prompt: {len(prompt)} characters"

278

}

279

}

280

281

return {}

282

```

283

284

### Stop Hook Input

285

286

Input for Stop hooks (when agent execution stops).

287

288

```python { .api }

289

class StopHookInput(BaseHookInput):

290

"""Input data for Stop hook events."""

291

292

hook_event_name: Literal["Stop"]

293

stop_hook_active: bool

294

```

295

296

**Fields:**

297

298

- `hook_event_name`: Always `"Stop"`

299

- `stop_hook_active` (bool): Whether stop hook is active

300

301

**Usage Example:**

302

303

```python

304

async def stop_hook(input, tool_use_id, context):

305

if input["hook_event_name"] == "Stop":

306

# Cleanup or logging

307

return {}

308

309

return {}

310

```

311

312

### SubagentStop Hook Input

313

314

Input for SubagentStop hooks (when sub-agent stops).

315

316

```python { .api }

317

class SubagentStopHookInput(BaseHookInput):

318

"""Input data for SubagentStop hook events."""

319

320

hook_event_name: Literal["SubagentStop"]

321

stop_hook_active: bool

322

```

323

324

**Fields:**

325

326

- `hook_event_name`: Always `"SubagentStop"`

327

- `stop_hook_active` (bool): Whether stop hook is active

328

329

**Usage Example:**

330

331

```python

332

async def subagent_stop_hook(input, tool_use_id, context):

333

if input["hook_event_name"] == "SubagentStop":

334

# Handle sub-agent completion

335

return {}

336

337

return {}

338

```

339

340

### PreCompact Hook Input

341

342

Input for PreCompact hooks (before transcript compaction).

343

344

```python { .api }

345

class PreCompactHookInput(BaseHookInput):

346

"""Input data for PreCompact hook events."""

347

348

hook_event_name: Literal["PreCompact"]

349

trigger: Literal["manual", "auto"]

350

custom_instructions: str | None

351

```

352

353

**Fields:**

354

355

- `hook_event_name`: Always `"PreCompact"`

356

- `trigger`: Compaction trigger type (`"manual"` or `"auto"`)

357

- `custom_instructions` (str | None): Custom compaction instructions

358

359

**Usage Example:**

360

361

```python

362

async def pre_compact_hook(input, tool_use_id, context):

363

if input["hook_event_name"] == "PreCompact":

364

trigger = input["trigger"]

365

# Perform pre-compaction actions

366

return {}

367

368

return {}

369

```

370

371

### Hook Input Union

372

373

Union of all hook input types.

374

375

```python { .api }

376

HookInput = (

377

PreToolUseHookInput

378

| PostToolUseHookInput

379

| UserPromptSubmitHookInput

380

| StopHookInput

381

| SubagentStopHookInput

382

| PreCompactHookInput

383

)

384

```

385

386

## Hook Output Types

387

388

Hooks return either synchronous or asynchronous outputs.

389

390

### Synchronous Hook Output

391

392

Standard hook response with control and decision fields.

393

394

```python { .api }

395

class SyncHookJSONOutput(TypedDict):

396

"""Synchronous hook output with control and decision fields."""

397

398

# Common control fields

399

continue_: NotRequired[bool]

400

suppressOutput: NotRequired[bool]

401

stopReason: NotRequired[str]

402

403

# Decision fields

404

decision: NotRequired[Literal["block"]]

405

systemMessage: NotRequired[str]

406

reason: NotRequired[str]

407

408

# Hook-specific outputs

409

hookSpecificOutput: NotRequired[HookSpecificOutput]

410

```

411

412

**Fields:**

413

414

- `continue_` (bool, optional): Whether Claude should proceed after hook execution. Default: True. **Note**: Use `continue_` in Python code; it's automatically converted to `"continue"` for CLI.

415

416

- `suppressOutput` (bool, optional): Hide stdout from transcript mode. Default: False.

417

418

- `stopReason` (str, optional): Message shown when `continue_` is False.

419

420

- `decision` (Literal["block"], optional): Set to `"block"` to indicate blocking behavior.

421

422

- `systemMessage` (str, optional): Warning message displayed to the user.

423

424

- `reason` (str, optional): Feedback message for Claude about the decision.

425

426

- `hookSpecificOutput` (HookSpecificOutput, optional): Event-specific controls (see below).

427

428

**Usage Example:**

429

430

```python

431

# Allow execution

432

async def allow_hook(input, tool_use_id, context):

433

return {} # Empty dict means continue normally

434

435

# Block execution

436

async def block_hook(input, tool_use_id, context):

437

return {

438

"decision": "block",

439

"systemMessage": "This operation is not allowed",

440

"reason": "Security policy violation"

441

}

442

443

# Stop execution

444

async def stop_hook(input, tool_use_id, context):

445

return {

446

"continue_": False, # Note the underscore

447

"stopReason": "User intervention required"

448

}

449

450

# Suppress output

451

async def quiet_hook(input, tool_use_id, context):

452

return {

453

"suppressOutput": True

454

}

455

```

456

457

### Asynchronous Hook Output

458

459

Deferred hook execution response.

460

461

```python { .api }

462

class AsyncHookJSONOutput(TypedDict):

463

"""Async hook output that defers hook execution."""

464

465

async_: Literal[True]

466

asyncTimeout: NotRequired[int]

467

```

468

469

**Fields:**

470

471

- `async_` (Literal[True]): Set to True to defer hook execution. **Note**: Use `async_` in Python code; it's automatically converted to `"async"` for CLI.

472

473

- `asyncTimeout` (int, optional): Timeout in milliseconds for the async operation.

474

475

**Usage Example:**

476

477

```python

478

async def async_hook(input, tool_use_id, context):

479

# Defer execution

480

return {

481

"async_": True, # Note the underscore

482

"asyncTimeout": 5000 # 5 second timeout

483

}

484

```

485

486

### Hook Output Union

487

488

Union of hook output types.

489

490

```python { .api }

491

HookJSONOutput = AsyncHookJSONOutput | SyncHookJSONOutput

492

```

493

494

## Hook-Specific Output Types

495

496

Event-specific controls in `hookSpecificOutput`.

497

498

### PreToolUse Specific Output

499

500

```python { .api }

501

class PreToolUseHookSpecificOutput(TypedDict):

502

"""Output specific to PreToolUse hooks."""

503

504

hookEventName: Literal["PreToolUse"]

505

permissionDecision: NotRequired[Literal["allow", "deny", "ask"]]

506

permissionDecisionReason: NotRequired[str]

507

updatedInput: NotRequired[dict[str, Any]]

508

```

509

510

**Fields:**

511

512

- `hookEventName`: Must be `"PreToolUse"`

513

- `permissionDecision`: Permission decision (`"allow"`, `"deny"`, or `"ask"`)

514

- `permissionDecisionReason`: Reason for the decision

515

- `updatedInput`: Modified tool input parameters

516

517

**Usage Example:**

518

519

```python

520

async def pre_tool_hook(input, tool_use_id, context):

521

if input["hook_event_name"] == "PreToolUse":

522

# Modify tool input

523

modified_input = input["tool_input"].copy()

524

modified_input["safe_mode"] = True

525

526

return {

527

"hookSpecificOutput": {

528

"hookEventName": "PreToolUse",

529

"permissionDecision": "allow",

530

"permissionDecisionReason": "Added safety flag",

531

"updatedInput": modified_input

532

}

533

}

534

return {}

535

```

536

537

### PostToolUse Specific Output

538

539

```python { .api }

540

class PostToolUseHookSpecificOutput(TypedDict):

541

"""Output specific to PostToolUse hooks."""

542

543

hookEventName: Literal["PostToolUse"]

544

additionalContext: NotRequired[str]

545

```

546

547

**Fields:**

548

549

- `hookEventName`: Must be `"PostToolUse"`

550

- `additionalContext`: Additional context to provide to Claude

551

552

**Usage Example:**

553

554

```python

555

async def post_tool_hook(input, tool_use_id, context):

556

if input["hook_event_name"] == "PostToolUse":

557

return {

558

"hookSpecificOutput": {

559

"hookEventName": "PostToolUse",

560

"additionalContext": "Tool executed successfully with no errors"

561

}

562

}

563

return {}

564

```

565

566

### UserPromptSubmit Specific Output

567

568

```python { .api }

569

class UserPromptSubmitHookSpecificOutput(TypedDict):

570

"""Output specific to UserPromptSubmit hooks."""

571

572

hookEventName: Literal["UserPromptSubmit"]

573

additionalContext: NotRequired[str]

574

```

575

576

**Fields:**

577

578

- `hookEventName`: Must be `"UserPromptSubmit"`

579

- `additionalContext`: Additional context to provide to Claude

580

581

**Usage Example:**

582

583

```python

584

async def prompt_submit_hook(input, tool_use_id, context):

585

if input["hook_event_name"] == "UserPromptSubmit":

586

return {

587

"hookSpecificOutput": {

588

"hookEventName": "UserPromptSubmit",

589

"additionalContext": "Context: User is working on a Python project"

590

}

591

}

592

return {}

593

```

594

595

## Complete Examples

596

597

### Security Hook

598

599

Block dangerous bash commands:

600

601

```python

602

from claude_agent_sdk import ClaudeAgentOptions, HookMatcher

603

604

DANGEROUS_PATTERNS = ["rm -rf", "sudo rm", "> /dev/sda", ":(){ :|:& };:"]

605

606

async def security_hook(input, tool_use_id, context):

607

if input["hook_event_name"] == "PreToolUse":

608

if input["tool_name"] == "Bash":

609

command = input["tool_input"].get("command", "")

610

611

for pattern in DANGEROUS_PATTERNS:

612

if pattern in command:

613

return {

614

"decision": "block",

615

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

616

"reason": "Security policy violation",

617

"hookSpecificOutput": {

618

"hookEventName": "PreToolUse",

619

"permissionDecision": "deny",

620

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

621

}

622

}

623

return {}

624

625

options = ClaudeAgentOptions(

626

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

627

hooks={

628

"PreToolUse": [

629

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

630

]

631

}

632

)

633

```

634

635

### Logging Hook

636

637

Log all tool usage:

638

639

```python

640

import logging

641

from claude_agent_sdk import ClaudeAgentOptions, HookMatcher

642

643

logging.basicConfig(level=logging.INFO)

644

logger = logging.getLogger(__name__)

645

646

async def pre_tool_logger(input, tool_use_id, context):

647

if input["hook_event_name"] == "PreToolUse":

648

logger.info(f"Tool {input['tool_name']} called with: {input['tool_input']}")

649

return {}

650

651

async def post_tool_logger(input, tool_use_id, context):

652

if input["hook_event_name"] == "PostToolUse":

653

logger.info(f"Tool {input['tool_name']} completed: {input['tool_response']}")

654

return {}

655

656

options = ClaudeAgentOptions(

657

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

658

hooks={

659

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

660

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

661

}

662

)

663

```

664

665

### Context Enhancement Hook

666

667

Add context based on tool results:

668

669

```python

670

async def context_enhancer(input, tool_use_id, context):

671

if input["hook_event_name"] == "PostToolUse":

672

tool_name = input["tool_name"]

673

response = input["tool_response"]

674

675

if tool_name == "Read":

676

# Add context about file type

677

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

678

if "import" in content and "def" in content:

679

return {

680

"hookSpecificOutput": {

681

"hookEventName": "PostToolUse",

682

"additionalContext": "This appears to be Python code with imports and functions"

683

}

684

}

685

686

elif tool_name == "Bash":

687

# Add context about command success

688

if response.get("exit_code") == 0:

689

return {

690

"hookSpecificOutput": {

691

"hookEventName": "PostToolUse",

692

"additionalContext": "Command executed successfully"

693

}

694

}

695

696

return {}

697

698

options = ClaudeAgentOptions(

699

hooks={

700

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

701

}

702

)

703

```

704

705

### File Protection Hook

706

707

Protect critical files from modification:

708

709

```python

710

PROTECTED_FILES = ["/etc/passwd", "/etc/shadow", "~/.ssh/id_rsa"]

711

712

async def file_protection_hook(input, tool_use_id, context):

713

if input["hook_event_name"] == "PreToolUse":

714

tool_name = input["tool_name"]

715

716

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

717

file_path = input["tool_input"].get("file_path", "")

718

719

for protected in PROTECTED_FILES:

720

if protected in file_path:

721

return {

722

"decision": "block",

723

"systemMessage": f"Cannot modify protected file: {file_path}",

724

"hookSpecificOutput": {

725

"hookEventName": "PreToolUse",

726

"permissionDecision": "deny",

727

"permissionDecisionReason": "File is protected by policy"

728

}

729

}

730

731

return {}

732

733

options = ClaudeAgentOptions(

734

hooks={

735

"PreToolUse": [

736

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

737

]

738

}

739

)

740

```

741

742

### Input Sanitization Hook

743

744

Sanitize tool inputs:

745

746

```python

747

async def input_sanitizer(input, tool_use_id, context):

748

if input["hook_event_name"] == "PreToolUse":

749

if input["tool_name"] == "Bash":

750

# Remove potentially dangerous env vars

751

tool_input = input["tool_input"].copy()

752

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

753

754

# Strip environment variable exports

755

if "export" in command.lower():

756

safe_command = command.replace("export", "# export")

757

tool_input["command"] = safe_command

758

759

return {

760

"hookSpecificOutput": {

761

"hookEventName": "PreToolUse",

762

"permissionDecision": "allow",

763

"permissionDecisionReason": "Sanitized environment exports",

764

"updatedInput": tool_input

765

}

766

}

767

768

return {}

769

770

options = ClaudeAgentOptions(

771

hooks={

772

"PreToolUse": [HookMatcher(matcher="Bash", hooks=[input_sanitizer])]

773

}

774

)

775

```

776

777

## Important Notes

778

779

### Python Keyword Conflicts

780

781

Use underscored versions in Python code:

782

- `async_` instead of `async`

783

- `continue_` instead of `continue`

784

785

These are automatically converted to the correct field names when sent to the CLI.

786

787

### Hook Execution Order

788

789

When multiple hooks match, they execute in order defined in the hooks list.

790

791

### Error Handling

792

793

Exceptions in hooks are caught and logged but don't break execution. Always handle errors gracefully.

794

795

### Performance

796

797

Hooks are called synchronously in the agent loop. Keep hook logic fast to avoid delays.

798

799

### Hook Limitations

800

801

The Python SDK does not support SessionStart, SessionEnd, and Notification hooks due to setup limitations.

802