MoonshotAI / MoonshotAI/kimi-code

PreToolUse hook is never invoked for the plan-file Write on agent-core-v2 (`kimi -p`, `default_plan_mode = true`)

Open
#3,431 3 comments 0 reactions 0 assignees View on GitHub

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?

0.39.1 (kimi --version; npm @moonshot-ai/kimi-code@0.39.1, tag 5efca0c3), default engine (agent-core-v2, KIMI_CODE_LEGACY_FLAG unset). Registration order and the single .allow() call in packages/agent-core-v2/src are unchanged on main at 7bc5b202 (src/index.ts:330,338; planService.ts:112).

Which open platform/subscription were you using?

Kimi subscription (kimi.com login)

Which model were you using?

k3

What platform is your computer?

macOS, Apple Silicon (arm64); Darwin 27.0.0

What issue are you seeing?

With default_plan_mode = true and one [[hooks]] PreToolUse entry (no matcher, exit 2), a kimi -p session on agent-core-v2 writes the active plan file without invoking the hook. The hook is live in the same session — it runs for, and blocks, a Glob call — but the plan-file Write executes with no hook invocation. The legacy engine (KIMI_CODE_LEGACY_FLAG=1) invokes the hook for the same Write and honors the block.

v2 run (session session_6fd44243-081f-43b5-90ae-270a401a2d37, exit 0, 36 s), stream-json stdout, hook = deny-all script that appends every stdin payload to a log:

{"role":"meta","type":"system.version","version":"0.39.1"}
assistant tool_call  Glob  {"pattern":"**/*"}
tool result          repro-hook: denied                                   <- hook invoked, block honored
assistant tool_call  Write {"path":".../sessions/wd_…/session_6fd44243-…/agents/main/plans/taskmaster-atom-smasher-sandman.md", …}
tool result          Wrote 796 bytes to .../agents/main/plans/taskmaster-atom-smasher-sandman.md   <- hook NOT invoked
{"role":"meta","type":"session.resume_hint","session_id":"session_6fd44243-…"}

Hook log after the run — exactly one payload, the Glob:

{"hook_event_name":"PreToolUse","session_id":"session_6fd44243-081f-43b5-90ae-270a401a2d37","cwd":"…/ws-v2","client_type":"kimi_code_cli","tool_name":"Glob","tool_input":{"pattern":"**/*"},"tool_call_id":"tool_YTLHwPh64OLiaUSaMNHz3Gzg"}

No Write payload; the plan file exists on disk (796 B).

Legacy control, separate home, same config + KIMI_CODE_LEGACY_FLAG=1 (session session_513f199b-0d21-4d6b-81cc-10a038525b53, 26 s): the hook log contains the Write to …/agents/main/plans/rocket-black-canary-siryn.md, the tool result is the block, and no plan file exists.

What steps can reproduce the bug?

Prerequisite: kimi-code 0.39.1, logged in (~/.kimi-code/config.toml carries your default_model; credentials/ your token). Each block creates its own isolated KIMI_CODE_HOME and empty workspace. Do not set KIMI_CODE_LEGACY_FLAG in block 1.

Block 1 — agent-core-v2 (bug):

export KIMI_CODE_HOME=$(mktemp -d) KIMI_CODE_NO_AUTO_UPDATE=1; unset KIMI_CODE_LEGACY_FLAG
cp -R ~/.kimi-code/credentials ~/.kimi-code/oauth ~/.kimi-code/device_id "$KIMI_CODE_HOME"/ 2>/dev/null || true   # auth
cat > "$KIMI_CODE_HOME/deny-all-hook.sh" <<H
#!/bin/sh
cat >> "$KIMI_CODE_HOME/hook-log.ndjson"; printf '\n' >> "$KIMI_CODE_HOME/hook-log.ndjson"
echo "repro-hook: denied" >&2
exit 2
H
chmod 755 "$KIMI_CODE_HOME/deny-all-hook.sh"
# keep your provider/default_model tables; prepend the plan default, append the hook
# (if your config.toml already sets default_plan_mode or has [[hooks]] entries, remove those first)
{ printf 'default_plan_mode = true\n'; cat ~/.kimi-code/config.toml; printf '\n[[hooks]]\nevent = "PreToolUse"\ncommand = "%s/deny-all-hook.sh"\ntimeout = 30\n' "$KIMI_CODE_HOME"; } > "$KIMI_CODE_HOME/config.toml"
WS=$(mktemp -d); cd "$WS"
kimi -p 'You are in plan mode. FIRST call the Glob tool with pattern "**/*" on the current directory (if it is denied, just note that and continue). THEN write a concise three-step plan for adding a README.md to this repository into the plan file (use the Write tool on the plan file path given in your instructions), then stop. Do not exit plan mode and do not touch any other file.' --output-format stream-json
echo "--- hook log:"; cat "$KIMI_CODE_HOME/hook-log.ndjson" 2>/dev/null
echo "--- plan files:"; find "$KIMI_CODE_HOME/sessions" -path '*/plans/*.md' 2>/dev/null

Expected on 0.39.1: ~20–40 s; the hook log contains only the Glob payload; a plan file is listed. A run is conclusive only if stdout contains a Write tool call to …/agents/main/plans/*.md — if the model skips it, retry the same prompt. If the model skips the Glob, any first tool call serves as the liveness control; the bug is Write absent from the log while the plan file exists.

Block 2 — legacy engine (control): repeat block 1 in a fresh shell, replacing unset KIMI_CODE_LEGACY_FLAG in the first line with export KIMI_CODE_LEGACY_FLAG=1. The hook log contains the Write payload, the tool result is the block, and no plan file is listed.

What is the expected behavior?

docs/en/customization/hooks.md (PreToolUse row): "Triggered before a tool call (before permission checks); the tool will not execute if blocked." A configured PreToolUse hook with no matcher should run before this tool call executes, and exit 2 should prevent it — as it does for the Glob in the same v2 session, and as the legacy engine does for this same Write. On agent-core-v2 the Write executes with no hook invocation at all.

This is not asking to change the plan-file allow() exemption from user deny/ask rules (test/features/plan/plan.test.ts:613-693) — that can stay. The bug is that the hook never runs for a tool call that then executes.

Additional information

Root cause (@0.39.1, verified by instrumenting the shipped bundle in a temp copy): FIRE-BEFORE-EXECUTE tool=Write listeners=11 → listener#0 → ALLOW (final) at AgentPlanService.guardToolExecution — listener #1, the external hook, never runs. AgentPlanService.guardToolExecution calls event.allow() for the exact active plan file (packages/agent-core-v2/src/features/plan/planService.ts:117, the only .allow() call in packages/agent-core-v2/src; Edit takes the same branch); fireBeforeExecute returns as soon as finalAllowed is set (agent/toolExecutor/beforeToolExecuteEvent.ts:108). Both services subscribe from their constructors and activate in the same DI cascade pass, so feature import order (src/index.ts:343 planFeature before :351 externalHooksFeature) makes plan listener #0 and external hooks #1 — an insertion-order tie-break, while docs/en/customization/hooks.md documents the hook as triggered before the tool call and able to block it. Same order and result on the resumed path. Note: test/wire/ builds the agent scope from hand-authored seeds (test/harness/agent.ts:1366-1369), so its listener order is incidental and a wire-level result proves nothing either way — reproduce against the shipped CLI as above.

Scope today: writesOnlyPlanFile is exact path equality, so the un-hooked write is one file under <KIMI_CODE_HOME>/sessions/…/agents/main/plans/, not the user worktree. Reached headlessly whenever plan mode is active: default_plan_mode = true at session creation (sessionLifecycleService.ts:191-213) or restored plan state on resume.

Context: reproduced on kimi -p (auto mode, no approval prompt). We hit this embedding kimi -p (kimi-plugin-cc) and currently pin our invocations to KIMI_CODE_LEGACY_FLAG=1 because of it (CONTRIBUTING.md notes agent-core v1 is being phased out).

Fix: any mechanism that makes the external-hook veto run before a final allow() on this channel fixes it, with the plan guard's tested allow() semantics untouched — this is the concrete instance of the same-phase ordering question your Permission.md already lists as open. We have a reference fix with tests, green at current main, and will open the PR on /approve — or implement whatever mechanism the team prefers.

Raw artifacts: v2 stream-json stdout, v2 hook log, legacy-control stdout and hook log

v2 stdout (session_6fd44243…):

{"role":"meta","type":"system.version","version":"0.39.1"}
{"role":"assistant","content":"I'll start by globbing the current directory as requested.","tool_calls":[{"type":"function","id":"tool_YTLHwPh64OLiaUSaMNHz3Gzg","function":{"name":"Glob","arguments":"{\"pattern\":\"**/*\"}"}}]}
{"role":"tool","tool_call_id":"tool_YTLHwPh64OLiaUSaMNHz3Gzg","content":"repro-hook: denied"}
{"role":"assistant","tool_calls":[{"type":"function","id":"tool_oecKZluG4m0Wt734OWrOUeCP","function":{"name":"Write","arguments":"{\"content\":\"# Plan: Add README.md to this repository\\n\\nContext: the working directory `/private/tmp/kimi-v2-repro/run-1788253154794/ws-v2` appears empty; a Glob of `**/*` was denied by policy, so no existing files could be enumerated. The README will therefore be a generic project placeholder.\\n\\n1. **Draft README.md content** — Write a minimal `README.md` at the repo root containing: project title (from directory name), a short description placeholder, and standard sections (Getting Started, Usage, License) to be filled in later.\\n2. **Create the file** — Use the Write tool to create `/private/tmp/kimi-v2-repro/run-1788253154794/ws-v2/README.md` with that content (single new file, no other changes).\\n3. **Verify** — Read the file back to confirm it was written correctly, then report the result.\\n\",\"path\":\"/tmp/kimi-v2-repro/run-1788253154794/home-v2/sessions/wd_ws-v2_2f17d905f35f/session_6fd44243-081f-43b5-90ae-270a401a2d37/agents/main/plans/taskmaster-atom-smasher-sandman.md\"}"}}]}
{"role":"tool","tool_call_id":"tool_oecKZluG4m0Wt734OWrOUeCP","content":"Wrote 796 bytes to /tmp/kimi-v2-repro/run-1788253154794/home-v2/sessions/wd_ws-v2_2f17d905f35f/session_6fd44243-081f-43b5-90ae-270a401a2d37/agents/main/plans/taskmaster-atom-smasher-sandman.md"}
{"role":"assistant","content":"Done. Summary of what happened:\n\n- `Glob` with `**/*` on the working directory was **denied** by the permission policy, so I could not enumerate files (the directory appeared empty in the initial listing anyway).\n- Wrote a concise three-step plan to the plan file: draft a minimal `README.md`, create it with Write, then verify by reading it back.\n\nPer your instruction, I've stopped here without calling ExitPlanMode and haven't touched any other file."}
{"role":"meta","type":"session.resume_hint","session_id":"session_6fd44243-081f-43b5-90ae-270a401a2d37","command":"kimi -r session_6fd44243-081f-43b5-90ae-270a401a2d37","content":"To resume this session: kimi -r session_6fd44243-081f-43b5-90ae-270a401a2d37"}

v2 hook log:

{"hook_event_name":"PreToolUse","session_id":"session_6fd44243-081f-43b5-90ae-270a401a2d37","cwd":"/private/tmp/kimi-v2-repro/run-1788253154794/ws-v2","client_type":"kimi_code_cli","tool_name":"Glob","tool_input":{"pattern":"**/*"},"tool_call_id":"tool_YTLHwPh64OLiaUSaMNHz3Gzg"}

legacy control stdout (session_513f199b…):

{"role":"assistant","tool_calls":[{"type":"function","id":"tool_U6IFEXiQhAOE5aKRpOPJLqWz","function":{"name":"Write","arguments":"{\"content\":\"# Plan: Add README.md\\n\\n1. **Survey the repo** — inspect the directory contents (currently empty) and any files added before execution, to determine the project name, purpose, and structure the README should describe.\\n2. **Draft `README.md`** — create `README.md` in the repo root with the standard sections that fit the project: title, short description, installation/setup, usage, and license (placeholder if unknown).\\n3. **Verify** — re-read the created `README.md` to confirm it is valid Markdown, accurately reflects the repo contents, and contains no placeholder-only sections that need user input.\\n\",\"path\":\"/tmp/kimi-v2-repro/run-1788253093050/home-v1/sessions/wd_ws-v1_2897a19190cc/session_513f199b-0d21-4d6b-81cc-10a038525b53/agents/main/plans/rocket-black-canary-siryn.md\"}"}}]}
{"role":"tool","tool_call_id":"tool_U6IFEXiQhAOE5aKRpOPJLqWz","content":"repro-hook: denied"}
{"role":"assistant","content":"The Write to the plan file was denied (`repro-hook: denied`), so no plan file was created and nothing else was touched. Per your instructions, I'm not exiting plan mode and am stopping here.\n\nIf you want to proceed, either approve writes to the plan file path so I can retry, or let me know how you'd like to handle it."}
{"role":"meta","type":"session.resume_hint","session_id":"session_513f199b-0d21-4d6b-81cc-10a038525b53","command":"kimi -r session_513f199b-0d21-4d6b-81cc-10a038525b53","content":"To resume this session: kimi -r session_513f199b-0d21-4d6b-81cc-10a038525b53"}

legacy control hook log:

{"hook_event_name":"PreToolUse","session_id":"session_513f199b-0d21-4d6b-81cc-10a038525b53","cwd":"/private/tmp/kimi-v2-repro/run-1788253093050/ws-v1","tool_name":"Write","tool_input":{"content":"# Plan: Add README.md\n\n1. **Survey the repo** — inspect the directory contents (currently empty) and any files added before execution, to determine the project name, purpose, and structure the README should describe.\n2. **Draft `README.md`** — create `README.md` in the repo root with the standard sections that fit the project: title, short description, installation/setup, usage, and license (placeholder if unknown).\n3. **Verify** — re-read the created `README.md` to confirm it is valid Markdown, accurately reflects the repo contents, and contains no placeholder-only sections that need user input.\n","path":"/tmp/kimi-v2-repro/run-1788253093050/home-v1/sessions/wd_ws-v1_2897a19190cc/session_513f199b-0d21-4d6b-81cc-10a038525b53/agents/main/plans/rocket-black-canary-siryn.md"},"tool_call_id":"tool_U6IFEXiQhAOE5aKRpOPJLqWz"}
Contribution
  • I am willing to submit a PR for this bug fix myself (please wait for maintainer approval in this issue first)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with packages/agent-core-v2/src/features/plan/planService.ts and agent/toolExecutor/beforeToolExecuteEvent.ts to trace the plan-file allow path and listener ordering. Compare the external hook subscription in src/index.ts with the documented behavior in docs/en/customization/hooks.md, then review test/features/plan/plan.test.ts and add coverage for the shipped CLI path. Done means PreToolUse runs and can block the plan-file Write without changing the existing plan allow semantics.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
cli, testing, tooling
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
58/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.