diff --git a/CHANGELOG.md b/CHANGELOG.md
index a85a4d0b8c..6210576619 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -92,6 +92,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- A Swift reference to a type now links to the type's own declaration, not to a file that extends it. An `extension View { … }` or `extension Text { … }` used to stand in for SwiftUI's type, so every view, every `Text("…")` and every `Color.red` in an app linked to whichever file happened to extend it. Those files topped the most-depended-on lists and their impact reached the whole app. A bare name like `@State`, `@Test` or `Result<…>` no longer links to some other type's nested `State` or `Result`, and a qualified name like `Build.Id` links to the `Id` it names. Methods declared in an extension of an SDK type still resolve on the types that conform to it, and a protocol's methods declared in any of its extensions now connect to each conforming type's own implementation. Re-index Swift projects after upgrading.
- A Vapor route now links to the handler it names. `use: SearchController.show` used to link to whichever controller's `show` came first, so routes with a common handler name, like `show`, `index` or `get`, pointed at another endpoint's code in callers, impact and `codegraph_explore` answers. Nested types like `API.PackageController.get` and handlers declared in an extension of the controller now resolve too, and `use: self.index` resolves to the collection's own `index`. Re-index Vapor projects after upgrading.
- A PHP call written without a receiver, such as `redirect($url)`, `view('books.show')`, `auth()` or `basename($path)`, is a function call, and no longer links to a same-named method, field or class elsewhere in the project. These wrong links showed up in callers, impact and `codegraph_explore` answers wherever a Laravel helper or PHP built-in shared its name with a project member. Re-index PHP projects after upgrading.
+- The Claude Code prompt hook no longer runs on the messages Claude Code uses to hand a subagent's report back to the main session. Before, such a long report could keep the hook busy past Claude Code's 30-second hook timeout and inject context unrelated to what you asked. (#2184)
## [1.6.1] - 2026-09-29
diff --git a/__tests__/frontload-hook.test.ts b/__tests__/frontload-hook.test.ts
index 3e81e6090e..c85700b9c0 100644
--- a/__tests__/frontload-hook.test.ts
+++ b/__tests__/frontload-hook.test.ts
@@ -13,7 +13,7 @@ import * as os from 'os';
import * as path from 'path';
import { spawnSync } from 'node:child_process';
import { CodeGraph } from '../src';
-import { planFrontload, isTaskNotification, findIndexedSubprojectRoots, unsafeIndexRootReason, isStructuralPrompt, hasStructuralKeyword, extractCodeTokens, PROMPT_HOOK_INJECTION_MAX, CLAUDE_CODE_INLINE_HOOK_OUTPUT_LIMIT, capPromptHookInjection } from '../src/directory';
+import { planFrontload, isTaskNotification, isAgentMessage, 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.
@@ -374,6 +374,12 @@ export class OrderStateMachine {
}
});
+ it('stays silent on a subagent hand-back envelope, even one that names indexed symbols (#2184)', () => {
+ const report = 'how does OrderStateMachine work? submitOrder() calls into the state machine.';
+ expect(hook(report)).toContain('Structural context from CodeGraph');
+ expect(hook(`\n[Subagent hand-back] ${report}\n`)).toBe('');
+ });
+
it('uses MEDIUM for indexed prose segments without a strong keyword or verified token', () => {
for (const prompt of ['como state machine?', 'wie state machine?']) {
const output = hook(prompt);
@@ -471,3 +477,20 @@ describe('system task notifications (#1832)', () => {
expect(isTaskNotification('trace AuthService login')).toBe(false);
});
});
+
+describe('subagent hand-backs (#2184)', () => {
+ const handBack = '\n[Subagent hand-back] Traced how AuthService.login calls TokenStore.save and which callers are affected.\n';
+
+ it('skips the complete hand-back envelope', () => {
+ expect(isAgentMessage(handBack)).toBe(true);
+ expect(isAgentMessage(` \n${handBack}\n`)).toBe(true);
+ expect(isAgentMessage('trace AuthService login flow')).toBe(true);
+ });
+ it('does not suppress a user question that mentions the marker', () => {
+ expect(isAgentMessage(`Why does ${handBack} trigger the hook?`)).toBe(false);
+ expect(isAgentMessage(`${handBack} Explain this.`)).toBe(false);
+ expect(isAgentMessage('trace AuthService')).toBe(false);
+ expect(isAgentMessage('trace AuthService')).toBe(false);
+ expect(isAgentMessage('trace AuthService login')).toBe(false);
+ });
+});
diff --git a/src/bin/codegraph.ts b/src/bin/codegraph.ts
index 38b4e55b56..64078dfab6 100644
--- a/src/bin/codegraph.ts
+++ b/src/bin/codegraph.ts
@@ -53,7 +53,7 @@ try {
import { Command } from 'commander';
import * as path from 'path';
import * as fs from 'fs';
-import { getCodeGraphDir, isInitialized, hasSchemalessDb, hasForeignDbFile, unsafeIndexRootReason, findNearestCodeGraphRoot, planFrontload, isTaskNotification, hasStructuralKeyword, extractCodeTokens, capPromptHookInjection, codeGraphDirName, DEFAULT_CODEGRAPH_DIR } from '../directory';
+import { getCodeGraphDir, isInitialized, hasSchemalessDb, hasForeignDbFile, unsafeIndexRootReason, findNearestCodeGraphRoot, planFrontload, isTaskNotification, isAgentMessage, hasStructuralKeyword, extractCodeTokens, capPromptHookInjection, codeGraphDirName, DEFAULT_CODEGRAPH_DIR } from '../directory';
import { extractProseCandidates } from '../search/identifier-segments';
import { detectWorktreeIndexMismatch, worktreeMismatchWarning } from '../sync/worktree';
import { createShimmerProgress } from '../ui/shimmer-progress';
@@ -1473,9 +1473,10 @@ 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;
+ // System-injected task notifications and subagent hand-backs are not
+ // user prompts: exit before any project lookup or explore work (#1832,
+ // #2184).
+ if (isTaskNotification(prompt) || isAgentMessage(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 0a08df9521..d99860f04e 100644
--- a/src/directory.ts
+++ b/src/directory.ts
@@ -1079,3 +1079,13 @@ export function validateDirectory(projectRoot: string): {
export function isTaskNotification(prompt: string): boolean {
return /^\s*[\s\S]*<\/task-notification>\s*$/.test(prompt);
}
+
+/**
+ * Claude Code hands a subagent's report back to the parent session as a
+ * `…` prompt, which UserPromptSubmit
+ * hooks also receive (#2184). Same rule as {@link isTaskNotification}: only a
+ * prompt that is entirely that envelope is skipped.
+ */
+export function isAgentMessage(prompt: string): boolean {
+ return /^\s*]*)?>[\s\S]*<\/agent-message>\s*$/.test(prompt);
+}