diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5cc33b561..bad96c3f3 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 95cd83a0b..5c03557ce 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 a6fe54c98..83fb2310a 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 3ada4c88c..160ac9c68 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);
+}