diff --git a/cli/src/agent/runners/runAgentSession.test.ts b/cli/src/agent/runners/runAgentSession.test.ts index ef9d4ca1..c3462da8 100644 --- a/cli/src/agent/runners/runAgentSession.test.ts +++ b/cli/src/agent/runners/runAgentSession.test.ts @@ -180,8 +180,10 @@ describe('runAgentSession', () => { const firstPrompt = JSON.stringify(harness.prompts[0]) const secondPrompt = JSON.stringify(harness.prompts[1]) - expect(firstPrompt).toContain('$name') - expect(firstPrompt).toContain('skill_lookup') + expect(firstPrompt).toContain('first') + expect(firstPrompt).not.toContain('skill_lookup') + expect(firstPrompt).not.toContain('$name') + expect(secondPrompt).toContain('second') expect(secondPrompt).not.toContain('skill_lookup') }) }) diff --git a/cli/src/agent/runners/runAgentSession.ts b/cli/src/agent/runners/runAgentSession.ts index 9c9d6b68..a4e10046 100644 --- a/cli/src/agent/runners/runAgentSession.ts +++ b/cli/src/agent/runners/runAgentSession.ts @@ -16,8 +16,6 @@ import { PermissionModeSchema } from '@hapi/protocol/schemas'; import { isPermissionModeAllowedForFlavor } from '@hapi/protocol'; import { RPC_METHODS } from '@hapi/protocol/rpcMethods'; import type { SessionEndReason } from '@hapi/protocol'; -import { SKILL_LOOKUP_INSTRUCTION } from '@/modules/common/skillLookupInstruction'; - function emitReadyIfIdle(props: { queueSize: () => number; shouldExit: boolean; @@ -101,8 +99,6 @@ export async function runAgentSession(opts: { let thinking = false; let shouldExit = false; let waitAbortController: AbortController | null = null; - let skillLookupInstructionSent = false; - const syncKeepAlive = () => { session.keepAlive(thinking, 'remote', { permissionMode: currentPermissionMode @@ -180,15 +176,11 @@ export async function runAgentSession(opts: { continue; } - let messageText = batch.message; - if (!skillLookupInstructionSent && !messageText.trimStart().startsWith('/')) { - messageText = `${SKILL_LOOKUP_INSTRUCTION}\n\n${messageText}`; - skillLookupInstructionSent = true; - } - + // skill_lookup discovery lives on the MCP tool description — do not + // prepend instructions onto user turns (prompt-injection false positive). const promptContent: PromptContent[] = [{ type: 'text', - text: messageText + text: batch.message }]; thinking = true; diff --git a/cli/src/cursor/cursorAcpRemoteLauncher.test.ts b/cli/src/cursor/cursorAcpRemoteLauncher.test.ts index 56a387ea..e75c4ceb 100644 --- a/cli/src/cursor/cursorAcpRemoteLauncher.test.ts +++ b/cli/src/cursor/cursorAcpRemoteLauncher.test.ts @@ -768,8 +768,10 @@ describe('cursorAcpRemoteLauncher', () => { await cursorAcpRemoteLauncher(session); expect(harness.promptCalls).toBe(2); - expect(JSON.stringify(harness.prompts[0])).toContain('$name'); - expect(JSON.stringify(harness.prompts[0])).toContain('skill_lookup'); + expect(JSON.stringify(harness.prompts[0])).toContain('first'); + expect(JSON.stringify(harness.prompts[0])).not.toContain('skill_lookup'); + expect(JSON.stringify(harness.prompts[0])).not.toContain('$name'); + expect(JSON.stringify(harness.prompts[1])).toContain('second'); expect(JSON.stringify(harness.prompts[1])).not.toContain('skill_lookup'); }); }); diff --git a/cli/src/cursor/cursorAcpRemoteLauncher.ts b/cli/src/cursor/cursorAcpRemoteLauncher.ts index 3b20c6ca..fd362bb5 100644 --- a/cli/src/cursor/cursorAcpRemoteLauncher.ts +++ b/cli/src/cursor/cursorAcpRemoteLauncher.ts @@ -30,8 +30,6 @@ import { cursorPassThroughStatusMessage, parseCursorSpecialCommand } from './cur import { buildCursorModelsSeedPayload, seedCursorModelsCache } from '@/modules/common/cursorModels'; import { readSharedCursorModelsCache } from '@/modules/common/cursorModelsSharedCache'; import type { AcpSdkBackend } from '@/agent/backends/acp'; -import { SKILL_LOOKUP_INSTRUCTION } from '@/modules/common/skillLookupInstruction'; - class CursorAcpRemoteLauncher extends RemoteLauncherBase { private readonly session: CursorSession; private backend: ReturnType | null = null; @@ -48,8 +46,6 @@ class CursorAcpRemoteLauncher extends RemoteLauncherBase { private spawnedWithAutoReview = false; /** Avoid re-queueing `/auto-review` on every mid-session mode sync. */ private autoReviewSlashQueued = false; - private skillLookupInstructionSent = false; - constructor(session: CursorSession) { super(process.env.DEBUG ? session.logPath : undefined); this.session = session; @@ -243,15 +239,11 @@ class CursorAcpRemoteLauncher extends RemoteLauncherBase { } messageBuffer.addMessage(batch.message, 'user'); - let messageText = batch.message; - if (!this.skillLookupInstructionSent && !messageText.trimStart().startsWith('/')) { - messageText = `${SKILL_LOOKUP_INSTRUCTION}\n\n${messageText}`; - this.skillLookupInstructionSent = true; - } - + // skill_lookup discovery lives on the MCP tool description — do not + // prepend instructions onto user turns (prompt-injection false positive). const promptContent: PromptContent[] = [{ type: 'text', - text: messageText + text: batch.message }]; session.onThinkingChange(true); diff --git a/cli/src/kimi/kimiRemoteLauncher.test.ts b/cli/src/kimi/kimiRemoteLauncher.test.ts index 0fb721c0..0e0c75cf 100644 --- a/cli/src/kimi/kimiRemoteLauncher.test.ts +++ b/cli/src/kimi/kimiRemoteLauncher.test.ts @@ -73,12 +73,13 @@ describe('kimiRemoteLauncher skill lookup instruction', () => { harness.prompts = [] }) - it('injects the instruction only on the first prompt', async () => { + it('does not prepend skill_lookup instructions onto user turns', async () => { await kimiRemoteLauncher(createSession() as never, { model: 'kimi-k2' }) expect(harness.prompts).toHaveLength(2) - expect(JSON.stringify(harness.prompts[0])).toContain('$name') - expect(JSON.stringify(harness.prompts[0])).toContain('skill_lookup') + expect(JSON.stringify(harness.prompts[0])).toContain('first') + expect(JSON.stringify(harness.prompts[0])).not.toContain('skill_lookup') + expect(JSON.stringify(harness.prompts[0])).not.toContain('$name') expect(JSON.stringify(harness.prompts[1])).not.toContain('skill_lookup') }) }) diff --git a/cli/src/kimi/kimiRemoteLauncher.ts b/cli/src/kimi/kimiRemoteLauncher.ts index 12a39a42..33e9ae4b 100644 --- a/cli/src/kimi/kimiRemoteLauncher.ts +++ b/cli/src/kimi/kimiRemoteLauncher.ts @@ -10,8 +10,6 @@ import type { PermissionMode } from './types'; import { createKimiBackend } from './utils/kimiBackend'; import { KimiPermissionHandler } from './utils/permissionHandler'; import { resolveKimiRuntimeConfig } from './utils/config'; -import { SKILL_LOOKUP_INSTRUCTION } from '@/modules/common/skillLookupInstruction'; - class KimiRemoteLauncher extends RemoteLauncherBase { private readonly session: KimiSession; private readonly model?: string; @@ -24,8 +22,6 @@ class KimiRemoteLauncher extends RemoteLauncherBase { private currentBackendModel: string | null = null; private setModelSupported: boolean | undefined = undefined; private lastDisplayedToolCall = new Map(); - private skillLookupInstructionSent = false; - constructor(session: KimiSession, opts: { model?: string }) { super(process.env.DEBUG ? session.logPath : undefined); this.session = session; @@ -171,15 +167,11 @@ class KimiRemoteLauncher extends RemoteLauncherBase { this.applyDisplayMode(batch.mode.permissionMode, batch.mode.model); messageBuffer.addMessage(batch.message, 'user'); - let messageText = batch.message; - if (!this.skillLookupInstructionSent && !messageText.trimStart().startsWith('/')) { - messageText = `${SKILL_LOOKUP_INSTRUCTION}\n\n${messageText}`; - this.skillLookupInstructionSent = true; - } - + // skill_lookup discovery lives on the MCP tool description — do not + // prepend instructions onto user turns (prompt-injection false positive). const promptContent: PromptContent[] = [{ type: 'text', - text: messageText + text: batch.message }]; session.onThinkingChange(true); diff --git a/cli/src/modules/common/skillLookupInstruction.ts b/cli/src/modules/common/skillLookupInstruction.ts index 86736a1a..aa04d33f 100644 --- a/cli/src/modules/common/skillLookupInstruction.ts +++ b/cli/src/modules/common/skillLookupInstruction.ts @@ -1,2 +1,11 @@ +/** + * Discovery copy for agents that can host a durable system / instructions block + * (OpenCode, Grok). Do **not** prepend this to user turns — that path looks like + * prompt injection on Cursor ACP and similar remotes (tiann/hapi#1095). + * + * Cursor / Kimi / generic ACP rely on the `skill_lookup` MCP tool description + * (and Cursor's native `.cursor/mcp.json` overlay where session/new mcpServers + * are ignored) instead of a user-message prepend. + */ export const SKILL_LOOKUP_INSTRUCTION = 'When a user message starts with "$name", call HAPI\'s skill_lookup tool with "name" (without "$") before acting.'