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-572 — applyUserPromptHook(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-147 — matcherValueText 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?
- Add a
UserPromptSubmit hook 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:
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.
What version of Kimi Code is running?
N/A — this is a documentation-accuracy report about the hook payload contract, verified against the current
mainsource rather than a single CLI build. Reproducible on any version whereUserPromptSubmithooks 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
UserPromptSubmitpayload fieldpromptis the submitted text string, but in the code it is the rawContentPart[]array. A hook that readspayload.promptexpecting 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):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.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.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: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
promptfield a hook reads from stdin is the array, which the doc never states.What steps can reproduce the bug?
UserPromptSubmithook to~/.kimi-code/config.toml:echo-prompt.mjs:hello.Observed:
typeof prompt = objectandprompt = [{"type":"text","text":"hello"}](aContentPart[]), not the string"hello". Any hook doingpayload.prompt.includes(...),payload.prompt.toLowerCase(), regex onpayload.prompt, etc. silently breaks, andtypeof payload.prompt === 'string' ? payload.prompt : ''collapses the real prompt to''.We hit this while building a cross-platform hook/MCP adapter: our
UserPromptSubmithandler assumedpromptwas 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
UserPromptSubmitexplicitly:promptis aContentPart[]array (e.g.[{ "type": "text", "text": "…" }]), not a plain string.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.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_textstring 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
mainbranch 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.