From 2e4320fee712bffc6a42294ac85ffc99aac4d6e5 Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 24 Jan 2026 04:35:32 +0100 Subject: [PATCH] feat: hooks forward stdin data from Claude Code to notifications The hook commands now read stdin JSON from Claude Code (contains tool_name, tool_input, etc.) and forward it as the data field to the API. This enables richer notifications showing actual context (e.g., "Bash: docker push prod"). Previously the curl commands only sent event type and session ID, losing all hook context data. Co-Authored-By: Claude Opus 4.5 --- src/hooks-config.ts | 14 ++++++++++---- test/hooks-config.test.ts | 16 ++++++++++++---- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/hooks-config.ts b/src/hooks-config.ts index c862c712..e19210b2 100644 --- a/src/hooks-config.ts +++ b/src/hooks-config.ts @@ -16,14 +16,20 @@ import type { HookEventType } from './types.js'; /** * Generates the hooks section for .claude/settings.local.json * - * The curl commands reference env vars that are resolved at runtime by - * the shell, so the same config works for any session in the case dir. + * The hook commands read stdin JSON from Claude Code (contains tool_name, + * tool_input, etc.) and forward it as the `data` field to Claudeman's API. + * Env vars are resolved at runtime by the shell, so the config is static + * per case directory. */ export function generateHooksConfig(): { hooks: Record } { + // Read Claude Code's stdin JSON and forward it as the data field. + // Falls back to empty object if stdin is unavailable or malformed. const curlCmd = (event: HookEventType) => - `curl -s -X POST $CLAUDEMAN_API_URL/api/hook-event ` + + `HOOK_DATA=$(cat 2>/dev/null || echo '{}'); ` + + `curl -s -X POST "$CLAUDEMAN_API_URL/api/hook-event" ` + `-H 'Content-Type: application/json' ` + - `-d '{"event":"${event}","sessionId":"'$CLAUDEMAN_SESSION_ID'"}' 2>/dev/null || true`; + `-d "{\\"event\\":\\"${event}\\",\\"sessionId\\":\\"$CLAUDEMAN_SESSION_ID\\",\\"data\\":$HOOK_DATA}" ` + + `2>/dev/null || true`; return { hooks: { diff --git a/test/hooks-config.test.ts b/test/hooks-config.test.ts index 2ae07695..2d5ce295 100644 --- a/test/hooks-config.test.ts +++ b/test/hooks-config.test.ts @@ -74,11 +74,19 @@ describe('generateHooksConfig', () => { it('should include correct event names in curl payloads', () => { const config = generateHooksConfig(); const notifHooks = config.hooks.Notification as Array<{ hooks: Array<{ command: string }> }>; - expect(notifHooks[0].hooks[0].command).toContain('"idle_prompt"'); - expect(notifHooks[1].hooks[0].command).toContain('"permission_prompt"'); - expect(notifHooks[2].hooks[0].command).toContain('"elicitation_dialog"'); + expect(notifHooks[0].hooks[0].command).toContain('idle_prompt'); + expect(notifHooks[1].hooks[0].command).toContain('permission_prompt'); + expect(notifHooks[2].hooks[0].command).toContain('elicitation_dialog'); const stopHooks = config.hooks.Stop as Array<{ hooks: Array<{ command: string }> }>; - expect(stopHooks[0].hooks[0].command).toContain('"stop"'); + expect(stopHooks[0].hooks[0].command).toContain('stop'); + }); + + it('should read stdin and forward as data field', () => { + const config = generateHooksConfig(); + const notifHooks = config.hooks.Notification as Array<{ hooks: Array<{ command: string }> }>; + // Should capture stdin via cat and include as $HOOK_DATA + expect(notifHooks[0].hooks[0].command).toContain('HOOK_DATA=$(cat'); + expect(notifHooks[0].hooks[0].command).toContain('$HOOK_DATA'); }); it('should set hook type to command', () => {