diff --git a/packages/providers/src/claude/provider.test.ts b/packages/providers/src/claude/provider.test.ts index 77880128da..a088b91bb5 100644 --- a/packages/providers/src/claude/provider.test.ts +++ b/packages/providers/src/claude/provider.test.ts @@ -866,6 +866,34 @@ describe('ClaudeProvider', () => { expect(callArgs.options.sandbox).toEqual(sandbox); }); + test('initializes options.hooks when nodeConfig provides hooks and options.hooks is undefined', async () => { + // Regression: applyNodeConfig previously assumed options.hooks was + // already an object when per-node hooks were declared, causing + // "undefined is not an object" at runtime for any workflow using + // per-node hooks (e.g. archon-architect, archon-refactor-safely). + mockQuery.mockImplementation(async function* () { + yield { type: 'result', session_id: 'sid' }; + }); + + for await (const _ of client.sendQuery('test', '/tmp', undefined, { + nodeConfig: { + hooks: { + PostToolUse: [{ matcher: 'Write', response: { continue: true } }], + }, + }, + })) { + // consume + } + + expect(mockQuery).toHaveBeenCalledTimes(1); + const callArgs = mockQuery.mock.calls[0][0] as { + options: { hooks?: Record> }; + }; + expect(callArgs.options.hooks).toBeDefined(); + expect(callArgs.options.hooks?.PostToolUse).toBeDefined(); + expect(callArgs.options.hooks?.PostToolUse?.[0]?.matcher).toBe('Write'); + }); + test('ignores empty text blocks', async () => { mockQuery.mockImplementation(async function* () { yield { diff --git a/packages/providers/src/claude/provider.ts b/packages/providers/src/claude/provider.ts index d6d9e39b97..a54c86f8f7 100644 --- a/packages/providers/src/claude/provider.ts +++ b/packages/providers/src/claude/provider.ts @@ -382,6 +382,7 @@ async function applyNodeConfig( if (Object.keys(builtHooks).length > 0) { // Merge with existing hooks (PostToolUse capture hook) const existingHooks = options.hooks as SDKHooksMap | undefined; + options.hooks ??= {}; for (const [event, matchers] of Object.entries(builtHooks)) { if (!matchers) continue; const existing = existingHooks?.[event] as HookCallbackMatcher[] | undefined;