Skip to content

Bug: before_prompt_build hook's prependContext/appendContext stripped into system prompt #86849

@lileilei-camera

Description

@lileilei-camera

Bug: before_prompt_build hook's prependContext / appendContext stripped into system prompt

Summary

The prependContext and appendContext returned by before_prompt_build hooks are not kept alongside the user message. Instead, resolveRuntimeContextPromptParts strips them out and places them into the system prompt area.

Actual behavior

SYSTEM PROMPT: ... (original system prompt) + prependContext + appendContext  ← stripped here
PROMPT (to model): raw user message only  ← hook-injected context is GONE

Expected behavior

SYSTEM PROMPT: ... (original system prompt only, no hook context)
PROMPT (to model): prependContext + "\n\n" + user message + "\n\n" + appendContext

Hook-injected context should be adjacent to the user message when sent to the model, so the model can associate the context with the user's question.

Requirements

The ideal behavior requires a three-layer separation:

  1. Hook context stays in the prompt areaprependContext / appendContext should be sent to the model alongside the user message in the same prompt area, not stripped into the system prompt.

  2. Not persisted in conversation history — Hook-injected content should NOT be written into the transcript / history. Only the user's raw message should be saved in history. Otherwise the history becomes bloated — injecting hundreds of lines (e.g. AGENTS.md) on every single turn will quickly overflow the context window.

  3. Not echoed to the user-facing UI — The user should only see their own raw message in webchat, Feishu, and other clients. Hook-injected prompts should only be injected during server-side prompt construction and only be visible to the model.

In summary:

Layer Content
Transcript / UI display Raw user message only
Model prompt User message + hook-injected context
System prompt System instructions (no hook prependContext / appendContext)

Root cause

The resolveRuntimeContextPromptParts function in runtime-context-prompt.ts:

function resolveRuntimeContextPromptParts(params) {
  const transcriptPrompt = params.transcriptPrompt;
  if (transcriptPrompt === void 0 || transcriptPrompt === params.effectivePrompt)
    return { prompt: params.effectivePrompt };

  // transcriptPrompt ≠ effectivePrompt → stripping is triggered
  const runtimeContext = removeLastPromptOccurrence(
    params.effectivePrompt, transcriptPrompt
  )?.trim() || params.effectivePrompt.trim();

  return {
    prompt: transcriptPrompt,          // only raw user message remains
    runtimeContext,                     // hook-injected context was stripped
    // runtimeContext ends up in runtimeSystemContext → system prompt area
  };
}

The core issue:

  1. All channels (Feishu, Telegram, Signal, CLI, etc.) set transcriptPrompt = raw user message during agent run setup (source: transcriptCommandBody in get-reply-*.js, queued.transcriptPrompt in agent-runner.runtime-*.js).
  2. After before_prompt_build hooks execute, effectivePrompt = transcriptPrompt + prependContext + appendContext, which is never equal to transcriptPrompt.
  3. Therefore stripping is triggered 100% of the time, and all hook-injected context is moved into the system prompt area.

Scope of impact

In selection-BmjEdnnA.js (lines ~14326-14450), the hook assembles effectivePrompt then calls resolveRuntimeContextPromptParts to split it.

This splitting logic affects all plugins that use before_prompt_build to return prependContext / appendContext. The hook-injected context is completely separated from the user message when sent to the model.

Relevant files

File Function / Lines Role
dist/runtime-context-prompt-*.js:18-34 resolveRuntimeContextPromptParts Splits effectivePrompt
dist/selection-*.js:14349-14355 Hook assembly prependContext/appendContexteffectivePrompt
dist/selection-*.js:14438-14447 Calls resolveRuntimeContextPromptParts effectivePrompt is split
dist/get-reply-*.js transcriptPrompt: transcriptCommandBody Channel-level transcriptPrompt source
dist/agent-runner-execution-*.js transcriptPrompt: params.transcriptCommandBody Queue execution transcriptPrompt source
dist/agent-runner.runtime-*.js transcriptPrompt: queued.transcriptPrompt Runtime transcriptPrompt passthrough

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Normal backlog priority with limited blast radius.clawsweeper:fix-shape-clearClawSweeper found a clear likely implementation shape for this issue.clawsweeper:queueable-fixClawSweeper marked this issue as an existing queue_fix_pr work candidate.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:session-stateSession, memory, transcript, context, or agent state can drift or corrupt.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions