Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
15 changes: 14 additions & 1 deletion __tests__/frontload-hook.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);
});
});
5 changes: 4 additions & 1 deletion src/bin/codegraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions src/directory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}