MoonshotAI / MoonshotAI/kimi-code
Hook & permission parity gaps vs Claude Code: split output channels, suppressOutput, ask/defer decisions, updatedInput, missing events, dead ask rules
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 7.5k
- Forks
- 1.2k
- Avg merge
- 11h 53m
- Merged PRs (30d)
- 350
Description
Summary
While porting a Claude Code plugin suite to Kimi Code, we mapped the hook and permission surfaces of both binaries (kimi-code 0.30.0 dist/main.mjs vs claude-code 2.1.220). These are the gaps that make faithful ports impossible today. Each item is verified against the shipped code, with a concrete, minimal proposal.
1. Hook output has a single channel rendered to BOTH model and user (the big one)
In agent-core/src/session/hooks/user-prompt.ts (renderUserPromptHookResult), the same messages array builds both:
text(wrapped in<hook_result>) → appended to the model context, andmessage→ emitted as thehook.resultevent → rendered in the TUI transcript.
userPromptHookMessage uses result.message ?? stdout, so any context a hook injects is dumped into the user's terminal. A rules corpus (~18 KB) injected on every UserPromptSubmit renders as a giant block on every prompt.
Claude Code has two separate channels: hookSpecificOutput.additionalContext (model-only) and systemMessage (user-facing one-liner).
Proposal — parse two more fields in the hook JSON output and split the render:
// hook result parsing (already partial: message is read today)
const { message, hookSpecificOutput } = output.data;
const result = {
message: message ?? hookSpecificOutput?.message,
context: hookSpecificOutput?.additionalContext, // NEW: model-only
structuredOutput: true,
};
// renderUserPromptHookResult: build the two channels from different sources
return {
event: "UserPromptSubmit",
message: notices.join("\n"), // TUI: short notices only
text: contexts.map((c) => renderHookResult(event, c)).join("\n"), // model
};
2. No suppressOutput for hook stdout
Claude Code: suppressOutput: boolean — "Hide stdout from transcript". Kimi has no equivalent, so even benign hook chatter is user-visible. Combined with #1, hook authors have zero control over transcript noise.
3. Hooks can only deny — no allow / ask / defer
hookSpecificOutput parsing in kimi only honors permissionDecision: "deny". Claude Code supports allow, ask, and defer (mapped to ask interactively). Consequence: a hook that wants "ask the user" must hard-deny (dead end — retrying the same call denies again) or pass silently. There is no way for a hook to route a decision to the user's approval panel.
Proposal: map permissionDecision: "ask" to the existing requestToolApproval path in AgentPermissionGate.adjudicate — the infrastructure already exists for rule-based asks.
4. No updatedInput on PreToolUse
Claude Code hooks can rewrite tool input before execution (hookSpecificOutput.updatedInput, 79 references in its binary). Kimi hooks can only allow/block. Rewrite hooks (auto-format, path redirects, argument sanitation) are impossible.
5. No prompt-type hooks
Claude Code supports type: "prompt" hooks (an LLM judges the event, e.g. "verify the workflow was followed before Stop"). Kimi is command-only. A heuristic-only replacement is strictly weaker for workflow verification.
6. Missing lifecycle events
Present in claude-code 2.1.220, absent in kimi 0.30.0:
TeammateIdle— fires when a background teammate goes idle (used to trigger validation of the teammate's diff). Kimi background subagents produce no equivalent;SubagentStopcovers completion but not idle-with-changes.InstructionsLoaded— fires when instruction files load (used for rules-loaded debugging/auditing).
(TaskCompleted has a working equivalent via Notification with matcher task.completed — good.)
7. BUG: user [[permission.rules]] with decision = "ask" never trigger
Repro (0.30.0):
# ~/.kimi-code/config.toml
default_permission_mode = "yolo"
[[permission.rules]]
decision = "ask"
pattern = "Bash(git *)"
- Start a new session. 2. Have the agent run
git status(matches the pattern — verified against the bundled picomatch semantics).
Expected (docs: rules loaded at session start, first match takes effect): approval panel + PermissionRequest hook event.
Actual: immediate execution. Zero permission events in the session wire, no panel.
Source analysis: the policy chain orders UserConfiguredAskPermissionPolicyService before YoloModeApprovePermissionPolicyService with first-non-void-wins, and AgentPermissionGate.adjudicate() routes kind: "ask" to requestToolApproval unconditionally — so the ask should fire even in yolo. It doesn't, which suggests user-configured rules never reach rulesService.rules (config [[permission.rules]] not loaded/registered at session start), or the gate is not consulted on this path.
Side note for the docs: an arg-pattern with / needs a second rule (Bash(git */**)) because * never crosses /. Worth one line in the permission docs.
Environment
kimi-code 0.30.0 (Homebrew), macOS. All findings verified against dist/main.mjs line-level and live sessions (session wire files under ~/.kimi-code/sessions/).
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 by reading agent-core/src/session/hooks/user-prompt.ts, especially renderUserPromptHookResult, then trace AgentPermissionGate.adjudicate and the user-configured permission policy. Reproduce the ask-rule case from the issue using the stated config and session wire files. Done requires agreed, tested behavior for the proposed hook channels, permission decisions, input updates, lifecycle events, and rule loading.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- cli, security
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 30/100