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 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-24 04:35:32 +01:00
co-authored by Claude Opus 4.5
parent d1b40ab9e5
commit 2e4320fee7
2 changed files with 22 additions and 8 deletions
+10 -4
View File
@@ -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<string, unknown[]> } {
// 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: {
+12 -4
View File
@@ -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', () => {