fix(prompt-hook): skip Claude Code task notifications (#1832) (#1884)

Claude Code's UserPromptSubmit hook also fires for the
`<task-notification>…</task-notification>` 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: danusha2345 <danusha2345@users.noreply.github.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Daniil
2026-09-27 15:22:37 +03:00
committed by GitHub
parent 9e4b8e75f6
commit cba44c9cad
4 changed files with 31 additions and 2 deletions
+2
View File
@@ -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)
+14 -1
View File
@@ -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('<task-notification>trace AuthService login flow</task-notification>')).toBe(true);
expect(isTaskNotification(' <task-notification>\n<task-id>abc</task-id>\n<summary>done</summary>\n</task-notification>\n')).toBe(true);
});
it('does not suppress a user question that mentions the marker', () => {
expect(isTaskNotification('Why does <task-notification>trace</task-notification> trigger the hook?')).toBe(false);
expect(isTaskNotification('<task-notification>trace</task-notification> Explain this.')).toBe(false);
expect(isTaskNotification('<other-tag>trace AuthService</other-tag>')).toBe(false);
expect(isTaskNotification('trace AuthService login')).toBe(false);
});
});
+4 -1
View File
@@ -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
+11
View File
@@ -899,3 +899,14 @@ export function validateDirectory(projectRoot: string): {
errors,
};
}
/**
* Claude Code injects `<task-notification>…</task-notification>` 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*<task-notification>[\s\S]*<\/task-notification>\s*$/.test(prompt);
}