66
77A Laravel-inspired PHP SDK for building Claude Code hook responses with a clean, fluent API. This SDK makes it easy to create structured JSON responses for Claude Code hooks using an expressive, chainable interface.
88
9+ Claude Code hooks are user-defined shell commands that execute at specific points in Claude Code's lifecycle, providing deterministic control over its behavior. For more details, see the [ Claude Code Hooks documentation] ( https://docs.anthropic.com/en/docs/claude-code/hooks ) .
10+
911## Installation
1012
1113You can install the package via composer:
@@ -16,239 +18,195 @@ composer require beyondcode/claude-hooks-sdk
1618
1719## Usage
1820
19- ### Basic Examples
21+ ### Creating a Claude Hook
2022
21- #### PreToolUse Hooks
23+ Here's how to create a PHP script that Claude Code can use as a hook:
2224
23- Block a tool call:
24- ``` php
25- use ClaudeHooks\Hook;
25+ #### Step 1: Create your PHP hook script
2626
27- Hook::preToolUse()
28- ->block('Command uses deprecated grep instead of ripgrep')
29- ->send();
30- ```
27+ Create a new PHP file (e.g., ` validate-code.php ` ) using the SDK:
3128
32- Approve a tool call:
3329``` php
34- Hook::preToolUse()
35- ->approve('Command validated successfully')
36- ->send();
37- ```
30+ <?php
3831
39- #### PostToolUse Hooks
32+ require 'vendor/autoload.php';
4033
41- Provide feedback after tool execution:
42- ``` php
43- Hook::postToolUse()
44- ->block('Code formatting violations detected')
45- ->send();
46- ```
34+ use BeyondCode\ClaudeHooks\ClaudeHook;
4735
48- #### Stop/SubagentStop Hooks
36+ // Read the hook data from stdin.
37+ // This will automatically return the correct Hook instance (for example PreToolUse)
38+ $hook = ClaudeHook::create();
4939
50- Prevent Claude from stopping:
51- ``` php
52- Hook::stop()
53- ->block('Tests are still running, please wait')
54- ->send();
40+ // Example: Validate bash commands
41+ if ($hook->toolName() === 'Bash') {
42+ $command = $hook->toolInput('command', '');
43+
44+ // Check for potentially dangerous commands
45+ if (str_contains($command, 'rm -rf')) {
46+ // Block the tool call with feedback
47+ $hook->response()->block('Dangerous command detected. Use caution with rm -rf commands.');
48+ }
49+ }
50+
51+ // Allow other tool calls to proceed
52+ $hook->success();
5553```
5654
57- ### Advanced Examples
55+ #### Step 2: Register your hook in Claude Code
5856
59- #### Suppress Output
57+ 1 . Run the ` /hooks ` command in Claude Code
58+ 2 . Select the ` PreToolUse ` hook event (runs before tool execution)
59+ 3 . Add a matcher (e.g., ` Bash ` to match shell commands)
60+ 4 . Add your hook command: ` php /path/to/your/validate-code.php `
61+ 5 . Save to user or project settings
6062
61- Hide stdout from transcript mode:
62- ``` php
63- Hook::preToolUse()
64- ->approve('Silently approved')
65- ->suppressOutput()
66- ->send();
67- ```
63+ Your hook is now active and will validate commands before Claude Code executes them!
6864
69- #### Stop Processing
65+ ### Hook Types and Methods
66+
67+ The SDK automatically creates the appropriate hook type based on the input:
7068
71- Stop Claude from continuing with a reason:
7269``` php
73- Hook::make()
74- ->stopProcessing('System maintenance in progress')
75- ->send();
76- ```
70+ use BeyondCode\ClaudeHooks\ClaudeHook;
71+ use BeyondCode\ClaudeHooks\Hooks\{PreToolUse, PostToolUse, Notification, Stop, SubagentStop};
7772
78- #### Custom Fields
73+ $hook = ClaudeHook::create();
7974
80- Add custom fields to the response:
81- ``` php
82- Hook::postToolUse()
83- ->with('customField', 'value')
84- ->merge(['foo' => 'bar', 'baz' => 123])
85- ->send();
86- ```
75+ if ($hook instanceof PreToolUse) {
76+ $toolName = $hook->toolName(); // e.g., "Bash", "Write", "Edit"
77+ $toolInput = $hook->toolInput(); // Full input array
78+ $filePath = $hook->toolInput('file_path'); // Specific input value
79+ }
8780
88- #### Error Responses
81+ if ($hook instanceof PostToolUse) {
82+ $toolResponse = $hook->toolResponse(); // Full response array
83+ $success = $hook->toolResponse('success', true); // With default value
84+ }
8985
90- Send a blocking error (exit code 2):
91- ``` php
92- Hook::blockWithError('Invalid file path detected' );
93- ```
86+ if ($hook instanceof Notification) {
87+ $message = $hook->message();
88+ $title = $hook->title( );
89+ }
9490
95- Send a non-blocking error:
96- ``` php
97- Hook::make()->fail('Warning: deprecated function used', 1);
91+ if ($hook instanceof Stop || $hook instanceof SubagentStop) {
92+ $isActive = $hook->stopHookActive();
93+ }
9894```
9995
100- ### Example Hook Scripts
96+ ### Response Methods
10197
102- #### Code Formatter Hook
98+ All hooks provide a fluent response API:
10399
104100``` php
105- #!/usr/bin/env php
106- <?php
107- require __DIR__ . '/vendor/autoload.php';
101+ // Continue processing (default behavior)
102+ $hook->response()->continue();
108103
109- use ClaudeHooks\Hook;
110-
111- $input = json_decode(file_get_contents('php://stdin'), true);
112- $toolInput = $input['tool_input'] ?? [];
113- $filePath = $toolInput['file_path'] ?? '';
114-
115- if (!$filePath || !file_exists($filePath)) {
116- exit(0);
117- }
104+ // Stop Claude from continuing with a reason
105+ $hook->response()->stop('Reason for stopping');
118106
119- $extension = pathinfo($filePath, PATHINFO_EXTENSION);
107+ // For PreToolUse: approve or block tool calls
108+ $hook->response()->approve('Optional approval message')->continue();
109+ $hook->response()->block('Required reason for blocking')->continue();
120110
121- // Run appropriate formatter
122- $formatters = [
123- 'php' => 'php-cs-fixer fix %s',
124- 'js' => 'prettier --write %s',
125- 'ts' => 'prettier --write %s',
126- 'py' => 'black %s',
127- ];
111+ // Suppress output from transcript mode
112+ $hook->response()->suppressOutput()->continue();
113+ ```
128114
129- if (isset($formatters[$extension])) {
130- $cmd = sprintf($formatters[$extension], escapeshellarg($filePath));
131- exec($cmd, $output, $exitCode);
132-
133- if ($exitCode !== 0) {
134- Hook::postToolUse()
135- ->block("Formatting failed: " . implode("\n", $output))
136- ->send();
137- }
138- }
115+ ### Example Hooks
139116
140- Hook::success();
141- ```
117+ #### Code Formatter Hook
142118
143- #### Command Validator Hook
119+ Automatically format PHP files after edits:
144120
145121``` php
146- #!/usr/bin/env php
147122<?php
148- require __DIR__ . '/vendor/autoload.php';
149123
150- use ClaudeHooks\Hook ;
124+ require 'vendor/autoload.php' ;
151125
152- $input = json_decode(file_get_contents('php://stdin'), true);
126+ use BeyondCode\ClaudeHooks\ClaudeHook;
127+ use BeyondCode\ClaudeHooks\Hooks\PostToolUse;
153128
154- if ($input['tool_name'] !== 'Bash') {
155- exit(0);
156- }
129+ $hook = ClaudeHook::create();
157130
158- $command = $input['tool_input']['command'] ?? '' ;
131+ $filePath = $hook->toolInput('file_path', '') ;
159132
160- // Validate dangerous commands
161- $dangerous = ['rm -rf /', 'dd if=', ':(){:|:& };:'];
162- foreach ($dangerous as $pattern) {
163- if (strpos($command, $pattern) !== false) {
164- Hook::preToolUse()
165- ->block("Dangerous command detected: $pattern")
166- ->send();
133+ if (str_ends_with($filePath, '.php')) {
134+ exec("php-cs-fixer fix $filePath", $output, $exitCode);
135+
136+ if ($exitCode !== 0) {
137+ $hook->response()
138+ ->suppressOutput()
139+ ->merge(['error' => 'Formatting failed'])
140+ ->continue();
167141 }
168142}
169-
170- // Check for deprecated commands
171- if (preg_match('/\bgrep\b(?!.*\|)/', $command)) {
172- Hook::preToolUse()
173- ->block("Use 'rg' (ripgrep) instead of 'grep' for better performance")
174- ->send();
175- }
176-
177- // Approve if all checks pass
178- Hook::preToolUse()
179- ->approve()
180- ->send();
181143```
182144
183- #### Stop Hook with Tests
145+ #### Security Validator Hook
146+
147+ Prevent modifications to sensitive files:
184148
185149``` php
186150#!/usr/bin/env php
187151<?php
188- require __DIR__ . '/vendor/autoload.php';
189152
190- use ClaudeHooks\Hook;
191-
192- // Check if tests are running
193- exec('pgrep -f "phpunit|pest"', $output, $exitCode);
194-
195- if ($exitCode === 0) {
196- Hook::stop()
197- ->block('Tests are still running. Please wait for completion.')
198- ->send();
199- }
200-
201- // Check for uncommitted changes
202- exec('git diff --quiet', $output, $exitCode);
203-
204- if ($exitCode !== 0) {
205- Hook::stop()
206- ->block('You have uncommitted changes. Please commit or stash them first.')
207- ->send();
153+ require 'vendor/autoload.php';
154+
155+ use BeyondCode\ClaudeHooks\ClaudeHook;
156+ use BeyondCode\ClaudeHooks\Hooks\PreToolUse;
157+
158+ $hook = ClaudeHook::fromStdin(file_get_contents('php://stdin'));
159+
160+ if ($hook instanceof PreToolUse) {
161+ // Check file-modifying tools
162+ if (in_array($hook->toolName(), ['Write', 'Edit', 'MultiEdit'])) {
163+ $filePath = $hook->toolInput('file_path', '');
164+
165+ $sensitivePatterns = [
166+ '.env',
167+ 'config/database.php',
168+ 'storage/oauth-private.key',
169+ ];
170+
171+ foreach ($sensitivePatterns as $pattern) {
172+ if (str_contains($filePath, $pattern)) {
173+ $hook->response()->block("Cannot modify sensitive file: $filePath");
174+ }
175+ }
176+ }
208177}
209178
210- // Allow stopping
211- Hook::success ();
179+ // Allow all other operations
180+ $hook->response()->continue ();
212181```
213182
214- ### API Reference
215-
216- #### Static Factory Methods
217-
218- - ` Hook::preToolUse() ` - Create a PreToolUse hook response
219- - ` Hook::postToolUse() ` - Create a PostToolUse hook response
220- - ` Hook::stop() ` - Create a Stop hook response
221- - ` Hook::subagentStop() ` - Create a SubagentStop hook response
222- - ` Hook::make() ` - Create a generic hook response
183+ #### Notification Handler Hook
223184
224- #### Decision Methods
185+ Custom notification handling:
225186
226- - ` approve(string $reason = '') ` - Approve tool execution (PreToolUse only)
227- - ` block(string $reason) ` - Block tool execution or prevent stopping
228-
229- #### Control Flow Methods
230-
231- - ` continueProcessing() ` - Allow Claude to continue (default)
232- - ` stopProcessing(string $stopReason) ` - Stop Claude with a reason
233- - ` suppressOutput(bool $suppress = true) ` - Hide output from transcript
187+ ``` php
188+ <?php
234189
235- #### Data Methods
190+ require 'vendor/autoload.php';
236191
237- - ` with(string $key, $value) ` - Add a custom field
238- - ` merge(array $fields) ` - Merge multiple fields
239- - ` toArray() ` - Get output as array
240- - ` toJson(int $options) ` - Get output as JSON string
192+ use BeyondCode\ClaudeHooks\ClaudeHook;
193+ use BeyondCode\ClaudeHooks\Hooks\Notification;
241194
242- #### Output Methods
195+ $hook = ClaudeHook::create();
243196
244- - ` send(int $exitCode = 0) ` - Send JSON response and exit
245- - ` error(string $message) ` - Send blocking error (exit 2)
246- - ` fail(string $message, int $exitCode) ` - Send non-blocking error
197+ // Send to custom notification system
198+ $notificationData = [
199+ 'title' => $hook->title(),
200+ 'message' => $hook->message(),
201+ 'session' => $hook->sessionId(),
202+ 'timestamp' => time()
203+ ];
204+
205+ // Send notification to Slack, Discord, etc.
247206
248- #### Static Helpers
207+ $hook->success();
208+ ```
249209
250- - ` Hook::blockWithError(string $message) ` - Quick blocking error
251- - ` Hook::success(string $message = '') ` - Quick success response
252210
253211## Testing
254212
0 commit comments