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, and
message → emitted as the hook.result event → 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; SubagentStop covers 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/).
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.mjsvs 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 samemessagesarray builds both:text(wrapped in<hook_result>) → appended to the model context, andmessage→ emitted as thehook.resultevent → rendered in the TUI transcript.userPromptHookMessageusesresult.message ?? stdout, so any context a hook injects is dumped into the user's terminal. A rules corpus (~18 KB) injected on everyUserPromptSubmitrenders as a giant block on every prompt.Claude Code has two separate channels:
hookSpecificOutput.additionalContext(model-only) andsystemMessage(user-facing one-liner).Proposal — parse two more fields in the hook JSON output and split the render:
2. No
suppressOutputfor hook stdoutClaude 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— noallow/ask/deferhookSpecificOutputparsing in kimi only honorspermissionDecision: "deny". Claude Code supportsallow,ask, anddefer(mapped toaskinteractively). 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 existingrequestToolApprovalpath inAgentPermissionGate.adjudicate— the infrastructure already exists for rule-based asks.4. No
updatedInputon PreToolUseClaude 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).(
TaskCompletedhas a working equivalent viaNotificationwith matchertask.completed— good.)7. BUG: user
[[permission.rules]]withdecision = "ask"never triggerRepro (0.30.0):
git status(matches the pattern — verified against the bundled picomatch semantics).Expected (docs: rules loaded at session start, first match takes effect): approval panel +
PermissionRequesthook event.Actual: immediate execution. Zero permission events in the session wire, no panel.
Source analysis: the policy chain orders
UserConfiguredAskPermissionPolicyServicebeforeYoloModeApprovePermissionPolicyServicewith first-non-void-wins, andAgentPermissionGate.adjudicate()routeskind: "ask"torequestToolApprovalunconditionally — so the ask should fire even in yolo. It doesn't, which suggests user-configured rules never reachrulesService.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.mjsline-level and live sessions (session wire files under~/.kimi-code/sessions/).