MoonshotAI / MoonshotAI/kimi-code
docs(hooks): UserPromptSubmit hook payload `prompt` is a ContentPart[] array, not the submitted text string
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
UserPromptSubmitonly 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, sopromptis never documented at all — its existence, name, or shape. - The only worked payload example (lines 130–146, the
block-dangerous-bash.mjssnippet) readspayload.tool_input?.command, reinforcing a "fields are scalars/plain objects" mental model. Nothing signals thatpromptis an array.
What the code actually sends (current main):
packages/agent-core/src/agent/turn/index.ts:561,568-572—applyUserPromptHook(turnId, input: readonly ContentPart[], …)triggers the hook with:
Sothis.agent.hooks?.trigger('UserPromptSubmit', { matcherValue: input, signal, inputData: { prompt: input }, // input is readonly ContentPart[] });payload.promptis the rawContentPart[].packages/agent-core/src/session/hooks/engine.ts:72-78— the engine flattensmatcherValueto text viamatcherValueText(...), but spreads...args.inputDatainto the payload verbatim. The flattening applies only to the matcher, not to the payload field.packages/agent-core/src/session/hooks/engine.ts:140-147—matcherValueTextis what joins the text parts with a space (this is what thematcherregex sees). The payloadpromptdoes 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?
- Add a
UserPromptSubmithook to~/.kimi-code/config.toml:[[hooks]] event = "UserPromptSubmit" command = "node ~/.kimi-code/hooks/echo-prompt.mjs" 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)); });- 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:
promptis aContentPart[]array (e.g.[{ "type": "text", "text": "…" }]), not a plain string.- The
matcheris applied to the joined text of those parts (permatcherValueText), 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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