From 2d1dfd5cfef7bcba45391611578a90392fe1e123 Mon Sep 17 00:00:00 2001 From: Joshua James Date: Sun, 5 Jul 2026 20:06:51 -0400 Subject: [PATCH] fix(cli): capture ExitPlanMode and restore MultiEdit/NotebookEdit in Claude prompt hook MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Telegram channel never showed agent prompts (selection questions, permission approvals) because the PreToolUse hook matcher in settings-hook.json only covered Bash|Edit|Write|AskUserQuestion. Any tool call not matched by this regex never fires the hook, so it's never written to the agent-request store and never reaches the JS dispatch/formatting code (formatPromptMessage in channel-runner.ts, which already handles every tool name generically) — the drop happens entirely upstream, in Claude Code's own hook config. Two concrete gaps in that one matcher: - ExitPlanMode was never included, even in the original design doc — Plan Mode's approval prompt is one of the most common permission-approval prompts in normal usage. - MultiEdit|NotebookEdit were silently removed by an unexplained "chore" commit (61a6633), uncaught because the only existing test exercised a synthetic fixture instead of the real shipped asset. Fix: broaden the matcher to Bash|Edit|Write|MultiEdit|NotebookEdit|ExitPlanMode|AskUserQuestion. Add a regression test that reads the real settings-hook.json asset (not the synthetic fixture) so the matcher can't silently regress again; confirmed it fails before this fix and passes after. Fixes #110 --- packages/cli/assets/claude/settings-hook.json | 2 +- .../services/setup/setup.service.test.ts | 31 +++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/packages/cli/assets/claude/settings-hook.json b/packages/cli/assets/claude/settings-hook.json index e046d122..45512f97 100644 --- a/packages/cli/assets/claude/settings-hook.json +++ b/packages/cli/assets/claude/settings-hook.json @@ -1,5 +1,5 @@ { - "matcher": "Bash|Edit|Write|AskUserQuestion", + "matcher": "Bash|Edit|Write|MultiEdit|NotebookEdit|ExitPlanMode|AskUserQuestion", "hooks": [ { "type": "command", diff --git a/packages/cli/src/__tests__/services/setup/setup.service.test.ts b/packages/cli/src/__tests__/services/setup/setup.service.test.ts index 208cde71..8db00182 100644 --- a/packages/cli/src/__tests__/services/setup/setup.service.test.ts +++ b/packages/cli/src/__tests__/services/setup/setup.service.test.ts @@ -1,6 +1,7 @@ import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'fs'; import { tmpdir } from 'os'; import { join } from 'path'; +import { fileURLToPath } from 'url'; import { createSetupService } from '../../../services/setup/setup.service.js'; describe('setup service', () => { @@ -307,6 +308,36 @@ describe('setup service — claude agent', () => { expect(existsSync(join(homeDir, '.claude', 'hooks', 'claude-prompt-hook.js'))).toBe(true); }); + it('installs the real settings-hook.json asset with a matcher covering every approval-required tool (regression: #110)', async () => { + fsMkdir(join(homeDir, '.claude')); + + // Use the actual shipped asset (not the synthetic fixture below) so this + // test fails whenever the real matcher drifts, instead of only checking + // a copy that can silently go stale. + const realAssetRoot = fileURLToPath(new URL('../../../../assets', import.meta.url)); + const service = createSetupService({ + homeDir, + assetRoot: realAssetRoot, + runCommand: async () => {}, + installBuiltInSkills: async () => {}, + }); + + await service.run({ agents: ['claude'] }); + + const settings = JSON.parse(readFileSync(join(homeDir, '.claude', 'settings.json'), 'utf-8')); + const matcher: string = settings.hooks.PreToolUse[0].matcher; + const matchedTools = matcher.split('|'); + + // Every tool whose invocation surfaces a permission-approval or + // selection-question prompt must be captured here, or that prompt is + // silently dropped and never reaches Telegram (issue #110). ExitPlanMode + // is the plan-mode approval prompt; MultiEdit/NotebookEdit are batch + // file-edit approvals. + for (const tool of ['Bash', 'Edit', 'Write', 'MultiEdit', 'NotebookEdit', 'ExitPlanMode', 'AskUserQuestion']) { + expect(matchedTools).toContain(tool); + } + }); + function createService() { return createSetupService({ homeDir,