Skip to content

Hook & permission parity gaps vs Claude Code: split output channels, suppressOutput, ask/defer decisions, updatedInput, missing events, dead ask rules #2455

Description

@fusengine

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 *)"
  1. 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/).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions