From c44a6aa4c3795d1881ef9a410b0b919c411f5c0a Mon Sep 17 00:00:00 2001 From: Nick Poulos Date: Sun, 24 Aug 2025 01:53:24 -0400 Subject: [PATCH 1/2] Add UserPromptSubmit hook and related tests Introduces the UserPromptSubmit hook for handling user prompt submissions, updates the README with usage examples, registers the new hook in ClaudeHook, and adds corresponding unit tests for both hook creation and prompt access. --- README.md | 32 +++++++++++++++++++++++++++- src/ClaudeHook.php | 2 ++ src/Hooks/UserPromptSubmit.php | 24 +++++++++++++++++++++ tests/ClaudeHookTest.php | 19 +++++++++++++++++ tests/Hooks/UserPromptSubmitTest.php | 22 +++++++++++++++++++ 5 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 src/Hooks/UserPromptSubmit.php create mode 100644 tests/Hooks/UserPromptSubmitTest.php diff --git a/README.md b/README.md index 7e3a29c..1542a79 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ The SDK automatically creates the appropriate hook type based on the input: ```php use BeyondCode\ClaudeHooks\ClaudeHook; -use BeyondCode\ClaudeHooks\Hooks\{PreToolUse, PostToolUse, Notification, Stop, SubagentStop}; +use BeyondCode\ClaudeHooks\Hooks\{PreToolUse, PostToolUse, Notification, UserPromptSubmit, Stop, SubagentStop}; $hook = ClaudeHook::create(); @@ -88,6 +88,10 @@ if ($hook instanceof Notification) { $title = $hook->title(); } +if ($hook instanceof UserPromptSubmit) { + $prompt = $hook->prompt(); +} + if ($hook instanceof Stop || $hook instanceof SubagentStop) { $isActive = $hook->stopHookActive(); } @@ -207,6 +211,32 @@ $notificationData = [ $hook->success(); ``` +#### User Prompt Submit Hook + +Validate and add context to user prompts: + +```php +prompt(), 'password') || str_contains($hook->prompt(), 'secret')) { + // Block the prompt + $hook->response()->block('Security policy violation: Please rephrase without sensitive information.'); +} + +// Add context via stdout +echo "Current time: " . date('Y-m-d H:i:s') . "\n"; +echo "Project status: Active development\n"; +$hook->success(); +``` + ## Testing diff --git a/src/ClaudeHook.php b/src/ClaudeHook.php index 0504301..744d250 100644 --- a/src/ClaudeHook.php +++ b/src/ClaudeHook.php @@ -8,6 +8,7 @@ use BeyondCode\ClaudeHooks\Hooks\PreToolUse; use BeyondCode\ClaudeHooks\Hooks\Stop; use BeyondCode\ClaudeHooks\Hooks\SubagentStop; +use BeyondCode\ClaudeHooks\Hooks\UserPromptSubmit; class ClaudeHook { @@ -15,6 +16,7 @@ class ClaudeHook 'PreToolUse' => PreToolUse::class, 'PostToolUse' => PostToolUse::class, 'Notification' => Notification::class, + 'UserPromptSubmit' => UserPromptSubmit::class, 'Stop' => Stop::class, 'SubagentStop' => SubagentStop::class, ]; diff --git a/src/Hooks/UserPromptSubmit.php b/src/Hooks/UserPromptSubmit.php new file mode 100644 index 0000000..cfe2422 --- /dev/null +++ b/src/Hooks/UserPromptSubmit.php @@ -0,0 +1,24 @@ +prompt = $data['prompt'] ?? ''; + } + + public function eventName(): string + { + return 'UserPromptSubmit'; + } + + public function prompt(): string + { + return $this->prompt; + } +} diff --git a/tests/ClaudeHookTest.php b/tests/ClaudeHookTest.php index 616f34e..9442a21 100644 --- a/tests/ClaudeHookTest.php +++ b/tests/ClaudeHookTest.php @@ -6,6 +6,7 @@ use BeyondCode\ClaudeHooks\Hooks\PreToolUse; use BeyondCode\ClaudeHooks\Hooks\Stop; use BeyondCode\ClaudeHooks\Hooks\SubagentStop; +use BeyondCode\ClaudeHooks\Hooks\UserPromptSubmit; it('creates PreToolUse hook from stdin', function () { $stdin = json_encode([ @@ -94,6 +95,24 @@ expect($hook->stopHookActive())->toBe(false); }); +it('creates UserPromptSubmit hook from stdin', function () { + $stdin = json_encode([ + 'session_id' => 'test-session', + 'transcript_path' => '/path/to/transcript.jsonl', + 'cwd' => '/path/to/project', + 'hook_event_name' => 'UserPromptSubmit', + 'prompt' => 'Write a function to calculate factorial', + ]); + + $hook = ClaudeHook::fromStdin($stdin); + + expect($hook)->toBeInstanceOf(UserPromptSubmit::class); + expect($hook->eventName())->toBe('UserPromptSubmit'); + expect($hook->prompt())->toBe('Write a function to calculate factorial'); + expect($hook->sessionId())->toBe('test-session'); + expect($hook->transcriptPath())->toBe('/path/to/transcript.jsonl'); +}); + it('throws exception for invalid JSON', function () { $stdin = 'invalid json'; diff --git a/tests/Hooks/UserPromptSubmitTest.php b/tests/Hooks/UserPromptSubmitTest.php new file mode 100644 index 0000000..3081b17 --- /dev/null +++ b/tests/Hooks/UserPromptSubmitTest.php @@ -0,0 +1,22 @@ +data = [ + 'session_id' => 'test-session', + 'transcript_path' => '/path/to/transcript.jsonl', + 'prompt' => 'Write a function to calculate factorial', + ]; +}); + +it('accesses prompt', function () { + $hook = new UserPromptSubmit($this->data); + expect($hook->prompt())->toBe('Write a function to calculate factorial'); +}); + +it('handles missing prompt gracefully', function () { + unset($this->data['prompt']); + $hook = new UserPromptSubmit($this->data); + expect($hook->prompt())->toBe(''); +}); From 65163a872c2d73fe987152a1f365b21e6da962ad Mon Sep 17 00:00:00 2001 From: Nick Poulos Date: Sun, 24 Aug 2025 02:46:32 -0400 Subject: [PATCH 2/2] Update hook example with both output examples Enhanced the README hook example with both ways to handle output. --- README.md | 31 +++++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 1542a79..8e783fb 100644 --- a/README.md +++ b/README.md @@ -225,16 +225,31 @@ use BeyondCode\ClaudeHooks\Hooks\UserPromptSubmit; $hook = ClaudeHook::create(); -// Check for sensitive patterns -if (str_contains($hook->prompt(), 'password') || str_contains($hook->prompt(), 'secret')) { - // Block the prompt - $hook->response()->block('Security policy violation: Please rephrase without sensitive information.'); +// Ask Claude Code - what is my secret-code? +// It should respond with ABC123 thanks to our injected prompt + +if ($hook instanceof UserPromptSubmit) { + $prompt = $hook->prompt(); + $lowerPrompt = strtolower($prompt); + + // Add context via your script and output the "fancy" way + if (str_contains($lowerPrompt, 'secret-code')) { + $contextReminder = "\n\nšŸ” Your secret code is 'ABC123'"; + $modifiedPrompt = $prompt . $contextReminder; + + $hook->response()->merge(['prompt' => $modifiedPrompt])->continue(); + return; + } + + // Add context via your script and output directly to stdout + if (str_contains($lowerPrompt, 'laravel')) { + echo PHP_EOL . PHP_EOL . 'Remember, this is a Laravel project, so use laravel-boost mcp server and related tools.'; + return; + } } -// Add context via stdout -echo "Current time: " . date('Y-m-d H:i:s') . "\n"; -echo "Project status: Active development\n"; -$hook->success(); +// For all other hook types, allow them to proceed +$hook->response()->continue(); ```