From e2b2d58f32e50061af4739d3a1ae0edeac3cfd8d Mon Sep 17 00:00:00 2001 From: danusha2345 Date: Wed, 16 Sep 2026 10:46:31 +0300 Subject: [PATCH] fix(prompt-hook): skip Claude Code task notifications (#1832) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude Code's UserPromptSubmit hook also fires for the `…` blocks the host injects as `user` messages when a background agent finishes. Those blocks are long machine text full of identifier-shaped tokens, so they pass the structural gate and the whole blob is sent to codegraph_explore as the query — a 3.5s median (7.4s max) stall on the blocking path, on text the user never typed. Recognize a prompt that consists solely of one task-notification envelope (leading/trailing whitespace allowed) and exit before any project lookup, gate or explore work. A prompt that merely mentions the marker, or has text around it, is still treated as a user prompt. Fixes #1832 Co-Authored-By: Claude Fable 5.1 --- CHANGELOG.md | 2 ++ __tests__/frontload-hook.test.ts | 15 ++++++++++++++- src/bin/codegraph.ts | 5 ++++- src/directory.ts | 11 +++++++++++ 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cc33b5613..bad96c3f3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -145,6 +145,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ### Fixes +- The Claude Code prompt hook no longer runs on the task-notification messages Claude Code injects when a background agent finishes, removing a multi-second stall on every such turn. (#1832) + - Rust calls on `self` now stay with the enclosing type instead of linking to an unrelated type’s same-named method. Thanks @L4XB. (#1861) - Turning telemetry off now resets its identity and stops running processes from recording, sending, or restoring unsent data. (#1869) diff --git a/__tests__/frontload-hook.test.ts b/__tests__/frontload-hook.test.ts index 95cd83a0b5..5c03557ce7 100644 --- a/__tests__/frontload-hook.test.ts +++ b/__tests__/frontload-hook.test.ts @@ -12,7 +12,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; -import { planFrontload, findIndexedSubprojectRoots, unsafeIndexRootReason, isStructuralPrompt, hasStructuralKeyword, extractCodeTokens, PROMPT_HOOK_INJECTION_MAX, CLAUDE_CODE_INLINE_HOOK_OUTPUT_LIMIT, capPromptHookInjection } from '../src/directory'; +import { planFrontload, isTaskNotification, findIndexedSubprojectRoots, unsafeIndexRootReason, isStructuralPrompt, hasStructuralKeyword, extractCodeTokens, PROMPT_HOOK_INJECTION_MAX, CLAUDE_CODE_INLINE_HOOK_OUTPUT_LIMIT, capPromptHookInjection } from '../src/directory'; // Make the built-in exports configurable so HOME can point at a real temp // fixture without changing the process environment or the user's home files. @@ -376,3 +376,16 @@ describe('prompt-hook injection cap (#1694)', () => { expect(out.length).toBeLessThan(CLAUDE_CODE_INLINE_HOOK_OUTPUT_LIMIT); }); }); + +describe('system task notifications (#1832)', () => { + it('skips the complete system envelope', () => { + expect(isTaskNotification('trace AuthService login flow')).toBe(true); + expect(isTaskNotification(' \nabc\ndone\n\n')).toBe(true); + }); + it('does not suppress a user question that mentions the marker', () => { + expect(isTaskNotification('Why does trace trigger the hook?')).toBe(false); + expect(isTaskNotification('trace Explain this.')).toBe(false); + expect(isTaskNotification('trace AuthService')).toBe(false); + expect(isTaskNotification('trace AuthService login')).toBe(false); + }); +}); diff --git a/src/bin/codegraph.ts b/src/bin/codegraph.ts index a6fe54c98e..83fb2310a6 100644 --- a/src/bin/codegraph.ts +++ b/src/bin/codegraph.ts @@ -41,7 +41,7 @@ try { import { Command } from 'commander'; import * as path from 'path'; import * as fs from 'fs'; -import { getCodeGraphDir, isInitialized, unsafeIndexRootReason, findNearestCodeGraphRoot, planFrontload, hasStructuralKeyword, extractCodeTokens, capPromptHookInjection } from '../directory'; +import { getCodeGraphDir, isInitialized, unsafeIndexRootReason, findNearestCodeGraphRoot, planFrontload, isTaskNotification, hasStructuralKeyword, extractCodeTokens, capPromptHookInjection } from '../directory'; import { extractProseCandidates } from '../search/identifier-segments'; import { detectWorktreeIndexMismatch, worktreeMismatchWarning } from '../sync/worktree'; import { createShimmerProgress } from '../ui/shimmer-progress'; @@ -1413,6 +1413,9 @@ program let input: { prompt?: string; cwd?: string } = {}; try { input = JSON.parse(raw); } catch { return; } const prompt = String(input.prompt || ''); + // System-injected task notifications are not user prompts: exit before + // any project lookup or explore work (#1832). + if (isTaskNotification(prompt)) return; // Gate telemetry: how often each tier fires vs. no-ops — counter names // only, NEVER prompt content (see TELEMETRY.md). This is the data that diff --git a/src/directory.ts b/src/directory.ts index 3ada4c88c3..160ac9c689 100644 --- a/src/directory.ts +++ b/src/directory.ts @@ -899,3 +899,14 @@ export function validateDirectory(projectRoot: string): { errors, }; } + +/** + * Claude Code injects `…` blocks as + * `user` messages when a background agent finishes, and UserPromptSubmit + * hooks receive them exactly like typed prompts (#1832). The whole prompt + * must be that single envelope; a user question that merely mentions the + * marker is still a prompt. + */ +export function isTaskNotification(prompt: string): boolean { + return /^\s*[\s\S]*<\/task-notification>\s*$/.test(prompt); +}