import type { SpendGuard } from '../spend-guard'; import { createFrameworkGuard, recordFrameworkReceipt, redactDataPlane, safeRequestShape, stringValue, type FrameworkAdapterOptions } from './common'; export interface ClaudeCodeHookInput { session_id?: string; transcript_path?: string; cwd?: string; hook_event_name?: string; tool_name?: string; tool_input?: Record; tool_response?: Record; } export interface ClaudeCodeHookOutput { continue?: boolean; suppressOutput?: boolean; systemMessage?: string; hookSpecificOutput?: { hookEventName: 'PreToolUse' | 'PostToolUse'; permissionDecision?: 'allow' | 'deny' | 'ask'; permissionDecisionReason?: string; additionalContext?: string; }; } export interface ClaudeCodeHooks { guard: SpendGuard; handlePreToolUse(input: ClaudeCodeHookInput): Promise; handlePostToolUse(input: ClaudeCodeHookInput): Promise; handleHookInput(input: ClaudeCodeHookInput): Promise; settingsSnippet(command: string): Record; } export function createClaudeCodeHooks(opts: FrameworkAdapterOptions): ClaudeCodeHooks { const guard = createFrameworkGuard({ ...opts, framework: 'claude-code' }); return { guard, handlePreToolUse: (input) => handlePreToolUse(guard, opts, input), handlePostToolUse: (input) => handlePostToolUse(guard, input), handleHookInput: async (input) => { if (input.hook_event_name === 'PostToolUse') return handlePostToolUse(guard, input); return handlePreToolUse(guard, opts, input); }, settingsSnippet: (command) => ({ hooks: { PreToolUse: [{ matcher: '*', hooks: [{ type: 'command', command }] }], PostToolUse: [{ matcher: '*', hooks: [{ type: 'command', command }] }], }, }), }; } export const createAgentGuardClaudeCodeHooks = createClaudeCodeHooks; async function handlePreToolUse( guard: SpendGuard, opts: FrameworkAdapterOptions, input: ClaudeCodeHookInput, ): Promise { const toolName = stringValue(input.tool_name) ?? 'unknown_tool'; const cleanArgs = redactDataPlane(input.tool_input ?? {}) as Record; try { const gate = await guard.guardToolCall({ toolName, toolArgs: safeRequestShape(cleanArgs), workflowId: input.session_id, scope: opts.scope, }); if (!gate.approved) { return deny('AgentGuard blocked tool call: ' + gate.decision.reasons.join('; ')); } if (gate.matchedPattern) { return { hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'ask', permissionDecisionReason: 'AgentGuard matched ' + gate.matchedPattern + '. Confirm before tool execution.', }, }; } return { continue: true, suppressOutput: true }; } catch (error) { return deny(error instanceof Error ? error.message : 'AgentGuard hook failed closed'); } } async function handlePostToolUse(guard: SpendGuard, input: ClaudeCodeHookInput): Promise { await recordFrameworkReceipt(guard, 'claude-code', { event: 'PostToolUse', sessionId: input.session_id ?? null, toolName: input.tool_name ?? 'unknown_tool', cwd: input.cwd ?? null, toolInput: redactDataPlane(input.tool_input ?? {}), toolResponse: redactDataPlane(input.tool_response ?? {}), }); return { continue: true, suppressOutput: true }; } function deny(reason: string): ClaudeCodeHookOutput { return { continue: true, hookSpecificOutput: { hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: reason, }, }; }