fix: restrict auto-accept to plan mode only, block AskUserQuestion prompts

The auto-accept feature was too aggressive - it would press Enter for any
silence without a completion message, including AskUserQuestion prompts.
Now uses the elicitation_dialog notification hook to detect when Claude is
asking a question, and blocks auto-accept in that case. Only plan mode
approvals (silence with no completion message AND no elicitation signal)
trigger auto-accept.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-24 03:10:02 +01:00
co-authored by Claude Opus 4.5
parent b4aea57d9c
commit 10222075cb
9 changed files with 136 additions and 31 deletions
+10 -2
View File
@@ -20,7 +20,7 @@ describe('generateHooksConfig', () => {
it('should have Notification hooks array', () => {
const config = generateHooksConfig();
expect(config.hooks.Notification).toBeInstanceOf(Array);
expect(config.hooks.Notification).toHaveLength(2);
expect(config.hooks.Notification).toHaveLength(3);
});
it('should have Stop hooks array', () => {
@@ -43,6 +43,13 @@ describe('generateHooksConfig', () => {
expect(permHook).toBeDefined();
});
it('should configure elicitation_dialog matcher', () => {
const config = generateHooksConfig();
const notifHooks = config.hooks.Notification as Array<{ matcher?: string }>;
const elicitHook = notifHooks.find(h => h.matcher === 'elicitation_dialog');
expect(elicitHook).toBeDefined();
});
it('should use env vars in curl commands (not hardcoded URLs)', () => {
const config = generateHooksConfig();
const notifHooks = config.hooks.Notification as Array<{ hooks: Array<{ command: string }> }>;
@@ -69,6 +76,7 @@ describe('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"');
const stopHooks = config.hooks.Stop as Array<{ hooks: Array<{ command: string }> }>;
expect(stopHooks[0].hooks[0].command).toContain('"stop"');
});
@@ -118,7 +126,7 @@ describe('writeHooksConfig', () => {
const settingsPath = join(testDir, '.claude', 'settings.local.json');
const parsed = JSON.parse(readFileSync(settingsPath, 'utf-8'));
expect(parsed.hooks).toBeDefined();
expect(parsed.hooks.Notification).toHaveLength(2);
expect(parsed.hooks.Notification).toHaveLength(3);
expect(parsed.hooks.Stop).toHaveLength(1);
});
+59
View File
@@ -1055,5 +1055,64 @@ describe('RespawnController Edge Cases', () => {
expect(autoAcceptFired).toBe(false);
autoAcceptController.stop();
});
it('should NOT auto-accept when elicitation dialog is signaled', async () => {
const autoAcceptController = new RespawnController(session as unknown as Session, {
autoAcceptPrompts: true,
autoAcceptDelayMs: 100,
completionConfirmMs: 50,
noOutputTimeoutMs: 5000,
});
let autoAcceptFired = false;
autoAcceptController.on('autoAcceptSent', () => {
autoAcceptFired = true;
});
autoAcceptController.start();
session.simulateTerminalOutput('Which option do you prefer?');
// Signal that an elicitation dialog (AskUserQuestion) was detected
autoAcceptController.signalElicitation();
// Wait for autoAcceptDelayMs to expire
await new Promise(resolve => setTimeout(resolve, 200));
// Auto-accept should NOT fire because elicitation was signaled
expect(autoAcceptFired).toBe(false);
autoAcceptController.stop();
});
it('should clear elicitation flag when working patterns detected', async () => {
const autoAcceptController = new RespawnController(session as unknown as Session, {
autoAcceptPrompts: true,
autoAcceptDelayMs: 100,
completionConfirmMs: 50,
noOutputTimeoutMs: 5000,
});
let autoAcceptFired = false;
autoAcceptController.on('autoAcceptSent', () => {
autoAcceptFired = true;
});
autoAcceptController.start();
session.simulateTerminalOutput('Question output');
// Signal elicitation
autoAcceptController.signalElicitation();
// Working pattern clears the elicitation flag (new turn started)
session.simulateTerminalOutput('Thinking');
// New silence after work - plan mode approval
session.simulateTerminalOutput('Plan: Here is the plan...');
await new Promise(resolve => setTimeout(resolve, 200));
// Auto-accept should fire now (elicitation cleared by working pattern)
expect(autoAcceptFired).toBe(true);
autoAcceptController.stop();
});
});
});