# Safety This guide describes the current checkout runtime. Read-only tasks deny file writes and shell commands. Rescue and pursue can edit your workspace. Write-swarm returns a patch from a temporary Git worktree. Native agent-core-v2 is certified at exact CLI versions `0.42.0`, `0.43.0`, `0.43.1`, `2.0.0`, `2.0.1`, `2.0.2`, `2.1.0`, `2.1.1`, `2.2.0`, `2.3.0`, `2.3.1`, `2.3.2`, `2.3.3`, `2.4.0`, and `2.4.1`, with plan mode disabled. The [runtime certification table](../runtime/kimi-engine.ts) controls routing. The hook is not an operating-system sandbox. ## Why the safety story is hook-based kimi-code's `kimi -p` mode hard-codes `permission: 'auto'` and registers an auto-approve handler for every tool call. There is no argv flag to disable this — `-p` is a non-interactive batch mode and assumes the caller wants tools to run unattended. That's a sensible default for a coding agent invoked directly from a terminal. It's the wrong default when a plugin is forwarding a read-only review request and the user expects no shell or file mutations to happen. The plugin installs a `PreToolUse` hook in Kimi's `config.toml`. Each host has its own hook entry. The plugin sets `KIMI_PLUGIN_CC_CMD` for each child process so the hook can select the operation's policy. On certified native-v2 versions, every tool call reaches the hook because plugin-managed sessions never activate plan mode. The preflight, tool policy, and resume checks enforce that condition. See the no-plan construction below. An explicitly pinned legacy-v1 binary uses the earlier permission-policy chain. ``` ┌──────────────────────┐ KIMI_PLUGIN_CC_CMD=review ┌──────────────────────┐ │ /kimi:review │ ───────────────────────────────► │ kimi -p │ │ (companion.sh) │ │ (permission:auto) │ └──────────────────────┘ └─────────┬────────────┘ │ │ PreToolUse: │ Bash ▼ ┌──────────────────────┐ │ approval-hook.js │ │ reads env var, │ │ applies policy, │ │ exits 2 + stderr │ │ (deny) │ └──────────────────────┘ ``` If the hook is missing or invalid, **every model-spawning command fails closed before Kimi starts**: rescue/pursue/swarm already refused, and v1.8 extends the same rule to ask/review/challenge. The review gate cannot safely block a host Stop event without enforcement, so it skips visibly instead of spawning an un-enforced Kimi turn. `/kimi:setup` also runs a two-layer probe before reporting success so missing-Node-on-PATH and broken-quoting failures surface up front instead of at first tool call. `KIMI_PLUGIN_CC_SKIP_HOOK_CHECK=1` remains an explicit tests/diagnostics escape hatch for hook verification; it accepts kimi-code's unsafe `permission: auto` posture but **does not** bypass `CLI_V2_HOOK_ORDER_UNSAFE` and is never a repair path. ## The managed block `/kimi:setup` writes a marker-delimited block to `~/.kimi-code/config.toml`: ```toml # === BEGIN kimi-plugin-cc-managed:claude-code (vX.Y.Z) === # DO NOT EDIT — managed by /kimi:setup. Run /kimi:setup --uninstall to remove. # Host: claude-code — Claude Code and Codex each own a separate block in this # shared ~/.kimi-code/config.toml; setup in one host never touches the other's. # Purpose: # kimi-code's `kimi -p` mode hard-codes permission='auto' and # auto-approves every tool call. This hook enforces /kimi:review, # /kimi:challenge, /kimi:review_gate, and /kimi:ask as read-only, # and applies the workspace-bound rescue allowlist for /kimi:rescue. # Without this block the plugin's safety contract collapses. # Matcher field is intentionally OMITTED — kimi-code compiles the # matcher with `new RegExp(...)`. An empty/missing matcher means # "fire for every tool". The string "*" would throw and silently # disable the hook. Do not "fix" this. # The Node binary path is absolute so kimi-code's `/bin/sh -c` # hook spawn doesn't need `node` on its PATH (GUI launches, # LaunchAgents, etc.). [[hooks]] event = "PreToolUse" command = "'/abs/path/to/node' '/abs/path/to/dist/hooks/approval-hook.js'" timeout = 15 # === END kimi-plugin-cc-managed:claude-code === ``` The `vX.Y.Z` marker is replaced with the live plugin version by `runtime/commands/setup.ts`. Both the node binary path and the hook script path are shell-quoted via `runtime/hooks/install-paths.ts` before they reach the config (so a path containing spaces or quotes can't bypass the safety contract through argv splitting). Key constraints (verified by [`runtime/hooks/managed-block.ts`](../runtime/hooks/managed-block.ts) on both install and verify): 1. **No `matcher` line.** kimi-code compiles the matcher field via `new RegExp(matcher)`. An empty matcher means "fire for every tool." `new RegExp("*")` throws, which kimi-code catches and treats as "no matcher" → silently disables the hook. The installer omits the field intentionally; the verifier rejects any block that contains a `matcher = ...` line. 2. **Absolute Node path.** kimi-code spawns hook commands via `/bin/sh -c ""`. If we wrote bare `node`, the shell would resolve it against kimi-code's PATH at execution time. On a system where kimi-code launches from a GUI/LaunchAgent with a sanitized PATH (nvm/asdf/mise users), bare `node` would exit 127 → hook protocol treats non-{0,2} as **allow** → safety contract collapses with no user-visible signal. 3. **TOML basic-string escaping.** The command field is a TOML basic string. Quotes, backslashes, newlines, and control characters in the resolved hook path are escaped during install; paths containing characters that cannot be safely escaped (literal `"`, raw control chars) are rejected up front with `SETUP_HOOK_PATH_UNSAFE`. 4. **Exact marker grammar.** The verifier rejects duplicate blocks and orphan or mismatched host markers. Install removes lexically proven unpaired marker comments across hosts, preserving surrounding content and all foreign hook tables, and warns. Text inside TOML strings is not a marker. The verifier remains fail-closed until repair and full validation succeed. 5. **Exact command equality, per host.** The verifier reconstructs the canonical command byte string from the current env and equality-checks the installed `command` (never a substring — a crafted `true # /path/to/approval-hook.js` runs only `true` and would auto-allow). This holds *per host id* (below): each host verifies its own block. 6. **Full-config parse and whole-hook-array validation.** The verifier parses the complete file with vendored `smol-toml@1.9.1` with upstream fixes included and no local JavaScript modifications (see `runtime/vendor/smol-toml/README.md`), before inspecting the managed block. It then applies upstream's strict config schema to every configured hook (`event`, optional string `matcher`, non-empty `command`, optional integer `timeout` 1–600, no extra fields). A malformed unrelated TOML value or one invalid foreign hook can make kimi-code discard enforcement; both therefore fail verification. 7. **Serialized, identity-bound setup mutations.** Install and uninstall hold a private `0o600` sibling lock across the complete read-modify-write transaction. The candidate lock is published atomically; lock reads are no-follow, nonblocking, regular-file-only, and capped at 4 KiB. Acquisition is bounded, live owners are never stolen, stale recovery is guarded by a nonrecursive recovery lease, and inode/token ownership is rechecked before config mutation or lock removal. Simultaneous Claude Code/Codex setup runs therefore cannot lose either host's block, and a replaced lock is never treated as ours. A newly created Kimi home is `0o700`; atomic config writes remain `0o600`. 8. **Inline hook arrays are normalized before insertion.** kimi-code accepts both `hooks = [...]` and `[[hooks]]`, but appending a managed array-of-tables block after an inline assignment can produce a conflicting document. Setup converts a valid inline hook array to canonical `[[hooks]]` tables before inserting the host block and emits an explicit warning that formatting and comments inside that assignment are normalized. Surrounding config bytes are preserved. ### Host scoping (all hosts share one config) Claude Code and Codex install this plugin to **different, version-stamped paths** (`~/.claude/plugins/cache/…` vs `~/.codex/plugins/cache/…`), while DSH uses its own profile package path. All read the **same** `~/.kimi-code/config.toml`. So the managed block is **host-scoped**: its marker carries a `:` suffix, and each host owns, verifies, and uninstalls only its **own** block. - **Host id** (`runtime/hooks/install-paths.ts::resolveHostId`) is derived version-independently from the hook path: `~/.claude/…` → `claude-code`, `~/.codex/…` → `codex`, else a stable `host-` for dev checkouts. DSH explicitly pins `dsh`. A plugin upgrade *refreshes* the same host's block rather than adding a new one. - **No clobbering.** `/kimi:setup` (Claude), `$kimi-setup` (Codex), and `/kimi-setup` (DSH) each write their own block and leave the others byte-identical. Before v1.7.0 there was a single block exact-matched to the running host, so the two hosts overwrote each other and the loser's write-commands refused with "hook path drift." - **Enforcement is redundant-safe.** kimi-code fires *every* `[[hooks]]` entry and aggregates **any-block-wins** (first `block` wins). Both hosts' hooks run the same allowlist logic, so the current host's live hook always enforces — even if another host's path is stale (a stale path fails open, but the live one still denies). A drifted *own* block is still caught at the per-command verify gate, which prompts a re-setup for that host only. - **Migration + cleanup.** A pre-v1.7.0 un-suffixed block is adopted and converted in place by the current host on its next `/kimi:setup`. Orphaned, marker-less `[[hooks]]` entries that are unambiguously our approval hook (canonical command, `approval-hook.js` under a kimi-marketplace/kimi-plugin-cc/ dsh-kimi-plugin-cc tree) are pruned **host-scoped** (v1.8.2): install and scoped uninstall touch only tables whose command path derives to the *current* host. `/kimi:setup --uninstall` removes only the current host's block (plus legacy + this host's orphans); `/kimi:setup --uninstall --all` removes every host's block and every plugin-owned table. Hand-rolled user hooks are never touched. - **Markers are not durable — verification does not depend on them (v1.8.2).** kimi-code persists its config via smol-toml `stringify(parse(...))` on every login/settings write, which deletes ALL comments — the managed-block BEGIN/END markers included — while the `[[hooks]]` table (data) survives and keeps enforcing. When this host's marked block is absent, the verifier falls back to a marker-less table whose decoded `command` is **byte-identical** to the canonical expected command, under the same strict grammar (PreToolUse event, no matcher, no keys beyond event/command/timeout). Anything weaker — drifted path, matcher-bearing table, foreign keys — still refuses. The fallback never applies over duplicate/orphan/invalid marked states. The next `/kimi:setup` re-adorns the markers; the other host's marker-less table is that host's LIVE hook and is left byte-untouched (pre-v1.8.2, the host-blind prune here caused each host's setup to disarm the other after any kimi login — the "seesaw"). ## DeepSeek Harness providers DSH's host and any profile-local harness packages must use patched `zod >=4.6.6` and `undici >=8.12.2`. The plugin treats DSH services as optional host-provided peers; it does not install the harness for other hosts or rewrite an existing DSH installation. Audit the actual host installation as well as the profile. See [patched DSH setup](./registry-releases.md#patched-dsh-host-and-profiles). The DSH bundle exposes the same seven operations as the other hosts: `kimi_ask`, `kimi_review`, `kimi_challenge`, `kimi_rescue`, `kimi_pursue`, `kimi_swarm`, and `kimi_swarm_write`. Each activation starts the existing companion through DSH's managed subprocess service and uses the same `kimi -p` transport, hook verification, exact-version certification, job store, and response or safety budgets. ACP is not used. Run `/kimi-setup` explicitly before delegating. Installing or enabling the bundle does not modify Kimi configuration. DSH uses host id `dsh` and `$DSH_HOME/plugin-data/kimi-plugin-cc` unless an absolute data-root override selects another store. Its provider-owned environment clears other-host roots/data, hook/script overrides, trusted roots, and verification bypasses instead of inheriting them. The parent receives only complete successful stdout. stderr remains on the host, and nonzero, empty, or truncated output becomes a bounded diagnostic rather than partial prose. Cancellation terminates the managed process range and waits for quiescence. DSH activation ids are distinct from companion job ids; use `/kimi-status`, `/kimi-result`, or `/kimi-cancel` for the persisted job. Write boundaries are identical to the companion commands: rescue and pursue edit the real selected workspace, ask/review/challenge and read-only swarm deny writes and shell commands, and swarm-write works from born HEAD in a temporary worktree and returns a patch without applying or committing it. `/kimi-setup --uninstall` removes only the DSH managed block; the all-host uninstall flag is not exposed. ## The hook's per-command policy [`runtime/hooks/approval-policy.ts`](../runtime/hooks/approval-policy.ts) is the single source of truth: | `KIMI_PLUGIN_CC_CMD` | Allowed tools | Denied tools | |---|---|---| | unset / empty | everything | nothing (kimi was invoked outside the plugin) | | `ask` | `Read`, `Grep`, `Glob`, `ReadMediaFile`, `TaskList`, `TaskOutput` | everything else, including `Bash`, `Write`, `Edit` | | `review`, `challenge`, `review_gate` | same as `ask` | same as `ask` | | `rescue` | governed by [`evaluateRescueHookRequest`](../runtime/rescue-approval.ts) against the plugin-owned `KIMI_PLUGIN_CC_WORKSPACE_ROOT` — workspace-bound shell allowlist, symlink-aware path containment, mutating-flag detection on `git`/`find`/`sed`, etc. | missing-root writes and every shell command, file edit, or write that the allowlist rejects | | `swarm` | same read-only set as `ask`, plus exact `AgentSwarm` | everything else, including singular `Agent`, shell, and writes | | `swarm-write` | read-only set + `AgentSwarm`; `Write`/`Edit`/`Bash` only through the rescue evaluator against the trusted ephemeral-worktree root | singular `Agent`, missing-root writes, and everything the rescue evaluator rejects | | unknown label | `Read`, `Grep`, `Glob`, `ReadMediaFile`, `TaskList`, `TaskOutput` | everything else (conservative-deny for stale/misconfigured callers) | `EnterPlanMode` and `ExitPlanMode` are explicitly denied before any per-label write evaluator for every managed label. Denied tool calls exit the hook with code 2 and write a reason to stderr. kimi-code surfaces the reason to the model, which can adapt and try a different approach (e.g., use `Read` instead of `Bash cat`). ### Native agent-core-v2: the no-plan construction (v1.10.0) agent-core-v2 awaits external PreToolUse hooks on ordinary tool calls, but it registers `AgentPlanService` before `AgentExternalHooksService`, and the plan guard final-allows a `Write`/`Edit` whose only write access is the exact current plan file — ending listener iteration before the managed hook runs. Upstream documents no ordering contract; kimi-code 0.42.0 deleted the legacy-v1 engine, so the pin the plugin used since v1.9.6 is inert there. The plugin therefore certifies native v2 on a different, explicitly amended basis (see [`native-v2-certification-provenance.md` §2](native-v2-certification-provenance.md#2-native-v2-entry-gate)): the plan guard is the engine's **only** chain-breaking final allow, it fires **only while plan mode is active**, and plugin-managed sessions never arm plan mode: - The default-plan-mode setting — under EVERY spelling upstream's config loader normalizes to the `defaultPlanMode` domain (`default_plan_mode`, `defaultPlanMode`, mixed; the preflight mirrors upstream's `snakeToCamel` verbatim) — is refused before spawn by `runtime/native-v2-preflight.ts` (`CLI_V2_PLAN_MODE_CONFIGURED`, remedy: set it to `false` or remove it — never `/kimi:setup`). The key lives only in `/config.toml` and is read once inside session creation; the plugin parses that same file just before spawn and again at the spawn boundary. - `EnterPlanMode` and `ExitPlanMode` are denied by the managed hook for every label. Nothing final-allows them, and the auto-approve gate uses a non-terminal `pass()`, so the deny wins. Verified live on 0.42.0. - A session is resumed only when it is proven native-v2 plugin lineage AND a raw scan of every agent `wire.jsonl` finds no `plan_mode.*`/`plan.*` record (`KIMI_SESSION_PLAN_TAINTED`); unknown-provenance rows refuse (`KIMI_SESSION_LINEAGE_UNKNOWN`). Nothing saved is modified. - v2-only experimental features the plugin does not certify (`tower`, `subagent_fork`, whether from `[experimental]` config or per-flag env) and the master `KIMI_CODE_EXPERIMENTAL_FLAG` are refused before spawn. What the headline claim is: **every executed tool call in a plugin-managed session passes the managed hook, and the engine's sole final allow is never armed.** What it is not: a claim that the hook precedes every final allow. Because plan mode is never armed, the property does not depend on listener order at all. It is re-proven per certified tag by a mechanized source scan (`tests/audit/v2-tag-scan.test.ts`) and a live plan-mode-ON control that must still show the bypass (proving the refusal is load-bearing rather than vacuous). Provenance: a native-v2 plan requires the `system.version` marker as the first stream-json line and requires it to equal the probed version; a legacy plan still refuses that marker. Either mismatch tears down the owned process tree before any record reaches the caller (`CLI_ENGINE_PROVENANCE_MISMATCH`). Certification is per exact version and per operation. See the current version list at the start of this guide. Residuals, unchanged from v1 and stated plainly: `config.toml` and the session journals are trusted operator state (an actor who can write them could already delete the `[[hooks]]` entry); the upstream hook runner fails open on its own internal errors; the plan-path comparison does not realpath (a symlink there would need a prior hook-allowed write). Native plan mode, tower, subagent fork, Remote Control and `--add-dir` are out of scope. In CLI 2.3.0 and later, stale-session retry can reread those same journals. Do not concurrently operate on or edit a plugin-managed session from another client. The upstream session lock is not a security boundary; no parent-side rescan can make that reload atomic. CLI 2.3.1 adds `KIMI_CODE_SESSION_LOCK=0` and explicit TUI/SDK/REST lock-takeover paths, which can disable or break mutual exclusion. Plugin children inherit an ambient lock-disable setting; print mode itself reports occupancy and does not take over. The prohibition on concurrent clients still applies. During POSIX lock contention, upstream can also execute bare `ps` with the child's inherited PATH and cwd outside PreToolUse; a PATH containing the workspace is therefore part of the internal-helper trust boundary. Legacy workspace-trust migration recognizes more old path spellings and can enable project MCP for those already trusted roots. Trust-store read failure can suppress the initial MCP load, an availability/diagnostics change rather than a trust fail-open. ## The rescue allowlist For `/kimi:rescue`, the hook delegates to [`evaluateRescueHookRequest`](../runtime/rescue-approval.ts). The allowlist has been extended since the original Wire-transport implementation. Highlights: - **File edits** (`Write`, `Edit`): the file path must resolve inside the workspace (`realpath`-based check), must not be inside `.git/`, and must not be a symlink that escapes the workspace. - **Shell commands** (`Bash`): the command is parsed with `shell-quote`. Pipelines are split and each stage validated independently. Mutating flags on `git` (`commit`, `push`, `reset --hard`, ...), `find` (`-delete`, `-exec`), and `sed` (`-i`) are rejected. - **Package-manager scripts**: `npm/pnpm/yarn/bun/uv run