MoonshotAI / MoonshotAI/kimi-code

docs(hooks): UserPromptSubmit hook payload `prompt` is a ContentPart[] array, not the submitted text string

Open Beginner friendly
#917 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
7.5k
Forks
1.2k
Avg merge
11h 53m
Merged PRs (30d)
350

Description

What version of Kimi Code is running?

N/A — this is a documentation-accuracy report about the hook payload contract, verified against the current main source rather than a single CLI build. Reproducible on any version where UserPromptSubmit hooks exist.

Which open platform/subscription were you using?

N/A (documentation / hook-protocol issue, not tied to a login platform).

What issue are you seeing?

The Hooks docs lead a hook author to believe the UserPromptSubmit payload field prompt is the submitted text string, but in the code it is the raw ContentPart[] array. A hook that reads payload.prompt expecting a string gets an array of content-part objects (e.g. [{ "type": "text", "text": "…" }]), so naive string handling silently produces wrong/empty results.

Where the docs mislead (docs/en/customization/hooks.md):

  • The Event Reference table (line 101) describes UserPromptSubmit only as: "Matcher matches: The text submitted by the user … Triggered when the user sends a message; returned text is appended to context …". This describes the matcher subject, not the JSON payload field — and there is no column or note about the payload shape.
  • The Event Data Format section (lines 57–69) shows the base payload (hook_event_name, session_id, cwd) and states "Specific events will also include additional fields … see the event reference below. All field names use snake_case." But the Event Reference table has no additional-fields column, so prompt is never documented at all — its existence, name, or shape.
  • The only worked payload example (lines 130–146, the block-dangerous-bash.mjs snippet) reads payload.tool_input?.command, reinforcing a "fields are scalars/plain objects" mental model. Nothing signals that prompt is an array.

What the code actually sends (current main):

  • packages/agent-core/src/agent/turn/index.ts:561,568-572applyUserPromptHook(turnId, input: readonly ContentPart[], …) triggers the hook with:
    this.agent.hooks?.trigger('UserPromptSubmit', {
      matcherValue: input,
      signal,
      inputData: { prompt: input },   // input is readonly ContentPart[]
    });
    
    So payload.prompt is the raw ContentPart[].
  • packages/agent-core/src/session/hooks/engine.ts:72-78 — the engine flattens matcherValue to text via matcherValueText(...), but spreads ...args.inputData into the payload verbatim. The flattening applies only to the matcher, not to the payload field.
  • packages/agent-core/src/session/hooks/engine.ts:140-147matcherValueText is what joins the text parts with a space (this is what the matcher regex sees). The payload prompt does not go through this.

Net effect: the matcher matches against the joined text (consistent with the doc's "The text submitted by the user"), but the payload prompt field a hook reads from stdin is the array, which the doc never states.

What steps can reproduce the bug?
  1. Add a UserPromptSubmit hook to ~/.kimi-code/config.toml:
    [[hooks]]
    event = "UserPromptSubmit"
    command = "node ~/.kimi-code/hooks/echo-prompt.mjs"
    
  2. echo-prompt.mjs:
    let input = '';
    process.stdin.on('data', (c) => (input += c));
    process.stdin.on('end', () => {
      const payload = JSON.parse(input);
      // Doc-implied expectation: prompt is the submitted text string
      console.error('typeof prompt =', typeof payload.prompt);
      console.error('prompt =', JSON.stringify(payload.prompt));
    });
    
  3. Start a session and submit a message such as hello.

Observed: typeof prompt = object and prompt = [{"type":"text","text":"hello"}] (a ContentPart[]), not the string "hello". Any hook doing payload.prompt.includes(...), payload.prompt.toLowerCase(), regex on payload.prompt, etc. silently breaks, and typeof payload.prompt === 'string' ? payload.prompt : '' collapses the real prompt to ''.

We hit this while building a cross-platform hook/MCP adapter: our UserPromptSubmit handler assumed prompt was a string (matching the doc's "text submitted by the user"), and the payload silently degraded to empty against the real array. The matcher behaved as documented, which made the payload-vs-matcher distinction easy to miss.

What is the expected behavior?

The docs should state the payload contract for UserPromptSubmit explicitly:

  • prompt is a ContentPart[] array (e.g. [{ "type": "text", "text": "…" }]), not a plain string.
  • The matcher is applied to the joined text of those parts (per matcherValueText), which is why the Event Reference says "The text submitted by the user" — that describes the matcher, not the payload field.
  • A short snippet showing how to recover the text, e.g. payload.prompt.filter(p => p.type === 'text').map(p => p.text).join(' ').

Optionally (separate, smaller ergonomics enhancement — happy to open a dedicated feature-request issue first per CONTRIBUTING): consider also passing a flattened prompt_text string in the payload so hook authors who only need the text don't each reimplement the join. The doc clarification stands on its own regardless.

I'm happy to send a documentation PR for the clarification (docs-only, no changeset, Conventional Commit title) — see the proposed diff in the linked PR if/when a maintainer is open to it. Related but distinct existing reports: #897 (hiding UserPromptSubmit hook output in the TUI) and #345 (PreToolUse updatedInput).

Additional information

Line numbers cited against the repository main branch at the time of filing (docs/en/customization/hooks.md, packages/agent-core/src/agent/turn/index.ts, packages/agent-core/src/session/hooks/engine.ts). The Hooks feature is documented as Beta, so pinning the payload contract now should be low-risk and high-value for early hook authors.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with docs/en/customization/hooks.md, especially the Event Data Format and Event Reference sections, then compare the stated contract with the cited hook implementation in packages/agent-core/src/agent/turn/index.ts and packages/agent-core/src/session/hooks/engine.ts. Document that UserPromptSubmit.payload.prompt is a ContentPart[] while matcher text is joined, include a text-extraction example, and verify the documentation clearly distinguishes both behaviors.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
documentation
Issue type
Documentation
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
76/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.