AGENTS.md · git:20260909.254d20a · 2026-09-09 · sha256 188b5609f9ed0a8a
AGENTS.md git:20260909.254d20aA
Immutable. This exact content is served forever at /api/v1/blob/188b5609f9ed0a8a.
# AGENTS.md
Project context for coding agents working in this repository.
## Quick reference
- **Version**: 1.10.1 (kimi-code, subprocess transport, **native agent-core-v2**). **The native-v2 safety contract — the current headline, do not regress:** every executed tool call in a plugin-managed session passes the managed PreToolUse hook, and the engine's sole chain-breaking final allow (the plan-file guard, `features/plan/planService.ts`, active plan mode only) is NEVER armed. That is a construction, not an upstream ordering promise, and it rests on four plugin-owned closures that must all stay in place: (1) `runtime/native-v2-preflight.ts` refuses the default-plan-mode setting under EVERY spelling upstream's loader normalizes to the `defaultPlanMode` domain (`default_plan_mode`, `defaultPlanMode`, mixed — it mirrors upstream's `snakeToCamel` verbatim, pinned by the tag scan; the `[experimental]` table keeps raw keys and is NOT normalized) unless absent or literal `false` — parsed from the SAME `<KIMI_CODE_HOME>/config.toml` the child reads once inside `sessions.create()`, before spawn and again at the spawn boundary, with `KIMI_CODE_HOME` resolved against the child's cwd on every entry path incl. the Stop hook (`CLI_V2_PLAN_MODE_CONFIGURED`, remedy is the config key, NEVER `/kimi:setup`); (2) the hook denies `EnterPlanMode`/`ExitPlanMode` for every label (`runtime/hooks/approval-policy.ts`); (3) resume accepts only proven `native-v2` plugin lineage (`KIMI_SESSION_LINEAGE_UNKNOWN` for unknown rows, `KIMI_SESSION_ENGINE_MISMATCH` for v1) after a raw scan of every agent `wire.jsonl` for `plan_mode.*`/`plan.*` records (`KIMI_SESSION_PLAN_TAINTED`) — `restore()` folds only that journal, nothing else can restore plan state; (4) v2-only experimental features (`tower`, `subagent_fork`, from `[experimental]` config or per-flag env) and the master `KIMI_CODE_EXPERIMENTAL_FLAG` refuse before spawn. Certification is per EXACT version and per operation (`NATIVE_V2_CERTIFIED` in `runtime/kimi-engine.ts`; `KIMI_TESTED_MINORS` is the legacy-v1 table and never gains 0.42); a new kimi-code version is appended only after `tests/audit/v2-tag-scan.test.ts` passes against its source tree, the plan-mode-ON live control still shows the bypass (proving the refusal is load-bearing), and the per-operation smoke is green. 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 it (`CLI_ENGINE_PROVENANCE_MISMATCH`, teardown before any record is delivered). `Bash.cwd` is validated against the trusted root before the command string (`runtime/rescue-approval.ts`) — the hook payload's top-level `cwd` is the process cwd, not the Bash cwd. Basis and residuals: `docs/native-v2-certification-provenance.md` §2. **Hook-pin durability contract (unchanged):** the canonical hook command must never embed a version-stamped Node token. `preferStableNodePath` prefers `process.argv0` over `process.execPath` ONLY when it is absolute AND `realpath`-identical; `verifyHookInstalled` runs `access(nodeBin, X_OK)` fail-closed; `nodeBin` is parsed back from the built command (`parseHookShellCommand`), never re-resolved. That check covers only "config and expected command still byte-equal, yet the embedded node token fails X_OK" (`refusal_kind:"node-bin-not-executable"`, the one refusal whose remedy is NOT /kimi:setup). Re-pin-recoverable refusals carry `drift`/`hookRefusalDetails`; agents re-pin through `${CLAUDE_PLUGIN_ROOT}/scripts/companion.sh setup` and retry ONCE only when `retryable_after_setup` AND setup exits 0 (`hookRefusalRetryProtocol`, single source in `hooks/install.ts`). Startup-failure wraps thread `availability`/`cause_code`/`cause_message` into `details`; `auth.login_required` classifies to `*_KIMI_AUTH_UNAVAILABLE` with a `kimi login` remedy. **In-verifier auto-repin is REJECTED — do not re-propose.** **Background-worker test discipline:** detached-worker hook-enforcement tests MUST run the companion CLI as a real node subprocess through a symlink with NO `KIMI_PLUGIN_CC_NODE_BIN` override and NO `KIMI_PLUGIN_CC_SKIP_HOOK_CHECK` (`tests/runtime/background-hook-enforcement.test.ts`). **Smoke-home representativeness:** a real-binary smoke case MUST assert its own precondition when the behaviour under test depends on host state; do NOT write a "plan-file write is denied under plan mode" smoke — under this construction that path is never armed and a green result would be exactly the vacuous-precondition failure. **Full per-version release + compat history → [CHANGELOG.md](./CHANGELOG.md) and [ROADMAP-TO-GA.md § Post-GA audit log](./ROADMAP-TO-GA.md#post-ga-audit-log).**
- **Upstream compat**: **native agent-core-v2 certified at exact `@moonshot-ai/kimi-code@0.42.0`** (commit `6954d2c8`) for all eight operations; legacy-v1 remains certified from 0.2.0 through **0.41.x** for an explicitly pinned binary only (0.42.0 deleted the v1 engine and `KIMI_CODE_LEGACY_FLAG`, #3542, unmentioned in its changelog). Engine selection is plugin-owned (`selectIntendedEngine`): exact version in `NATIVE_V2_CERTIFIED` → v2; minor in `KIMI_TESTED_MINORS` → v1 with the child-only legacy pin; else refuse. Truthy `KIMI_CODE_EXPERIMENTAL_FLAG` (`1|true|yes|on`) still refuses before spawn (`CLI_V2_HOOK_ORDER_UNSAFE`), now joined by `[experimental] tower`/`subagent_fork` from config or per-flag env (`CLI_V2_EXPERIMENTAL_UNSAFE`). The upstream ordering defect (plan before external hooks; sole final allow at `features/plan/planService.ts:110`) is UNCHANGED at 0.42.0 and confirmed in the shipped bundle; it stays filed as MoonshotAI/kimi-code#3431 (open, unanswered) as the preferred end state that would permit native plan mode. Current evidence: [docs/native-v2-status.md](docs/native-v2-status.md); basis and residuals: [docs/native-v2-certification-provenance.md](docs/native-v2-certification-provenance.md) §2; audit routine: [docs/upstream-compat-audit.md](docs/upstream-compat-audit.md) (+ `tests/audit/v2-tag-scan.test.ts`); per-release narrative: [ROADMAP-TO-GA.md § Post-GA audit log](./ROADMAP-TO-GA.md#post-ga-audit-log) + [CHANGELOG.md](./CHANGELOG.md). **The load-bearing v2 contract — re-verified each audit:**
- **Engine provenance:** every model job persists operation kind, intended/observed engine, exact command tuple, probed version, certification, `safety_profile` (`native-v2-no-plan/1` for v2 plans, null for v1) and resume lineage. A v2 plan requires the first stream-json line to be `{"role":"meta","type":"system.version","version":<probed>}`; a v1 plan refuses that marker. Either mismatch tears down the process tree before any record is delivered. Historical rows stay unknown unless saved-log evidence proves an engine. Rescue/pursue/swarm-write receive their trusted root from plugin-owned `KIMI_PLUGIN_CC_WORKSPACE_ROOT`, never the payload `cwd`.
- **Before-execute channel** (`packages/agent-core-v2/src/agent/toolExecutor/beforeToolExecuteEvent.ts`): listeners run sequentially in registration order; `finalAllowed` (`event.allow()`) ends iteration; `veto` wins; `pass` is non-terminal. Subscribers at 0.42.0: permissionGate, toolDedupe, btw, externalHooks, goal, plan, swarm, tower — only plan calls `allow()`, gated on active plan mode. The auto-approve policy (`permissionGate` → `AutoModeApprovePermissionPolicyService`, `pass()`) can never override a hook veto. Invariant: the `.allow()` count stays 1 and the subscriber set stays this list (tag scan).
- **Hook engine** lives in `packages/agent-core-v2/src/features/externalHooks/` (config `configSection.ts` — strict `{event, matcher?, command, timeout?}`; runner `internal/runHook.ts` — exit 2 = deny with stderr reason, exit 0 + JSON `permissionDecision:"deny"` = deny, anything else allow, fail-OPEN on spawn error/timeout/abort/bad JSON, 30 s default; matcher `internal/matchHooks.ts` — empty = all tools, throwing regex matches nothing, `Promise.all` then first block in match order, allow never overrides block). Payload keys `hook_event_name, session_id, cwd (process cwd), client_type, session_title, tool_name, tool_input, tool_call_id`. **`tool_input` field names:** `Write`/`Edit` use **`path`** (Write also `mode: overwrite|append`); `Bash` uses `command` plus **`cwd`**, `timeout`, `run_in_background`, … — `Bash.cwd` executes without upstream membership assertion (`bashTool.ts:191`), so `rescue-approval.ts` confines it to the trusted root before the command string. Plugin-contributed hooks are appended and deduped on `cwd\0command`; any-block-wins keeps them compat-benign. Re-verify all of this per tag.
- **`-p` mode** (`apps/kimi-code/src/cli/v2/run-v2-print.ts`): `auto` forced fresh and resumed, `nonInteractive:true` (dangerous-command policy omitted). argv consumed: `-p` (prompt as VALUE), `-r` (alias of `-S/--session`), `--output-format stream-json`, `-m`, `--skills-dir`; `--auto/--yolo/--plan` rejected with `-p`; `--agent`/`--agent-file`/`--add-dir` exist and are NEVER passed (profiles replace the system prompt and don't reach children; `--add-dir` widens roots). `-r` refuses when the session's recorded cwd ≠ current cwd. Records: assistant/tool, meta `system.version`/`session.resume_hint`/`turn.step.retrying`, role-less `goal.summary`; hook denials also surface as assistant text `"PreToolUse hook blocked…"`. Every spawn exports `KIMI_CODE_NO_AUTO_UPDATE=1` (the update preflight runs on `-p`) and an absolute `KIMI_CODE_HOME`.
- **Plan mode arming vectors** (exhaustive at 0.42.0, all closed — see the Version headline): `default_plan_mode` (single user file, no env/argv/project overlay, read once in `sessions.create()`); `EnterPlanMode` tool; journal replay on resume (`restore()` folds only `agents/<id>/wire.jsonl`). NOT reachable from `-p`: kap-server routes, TUI `/plan`, `--plan`, agent profiles (no plan field), subagent/fork/tower inheritance (fresh scope; `snapshotExcluded`), undo.
- **Plugin slash commands** are RPC/host-initiated and absent from `-p`; `-p` intercepts exactly one prefix, `/goal`. Read-only commands hard-prefix an instruction line so their prompt never starts with `/goal`. Re-verify this stays host/UI-only.
- **Custom-agent trust boundary (0.31+):** print mode discovers user/project/plugin agent profiles and plugin system-prompt contributions (`<git-root>/.kimi-code/agents`, `<git-root>/.agents/agents`); they can steer intended work but cannot set hooks, permission mode, cwd, plan state, or widen the allowlist. Inspect them before rescue/pursue/swarm-write in an untrusted repository.
- **Daily upstream monitor reports** live under `.claude/kimi-code-research/daily-monitor/` (gitignored). Read `LATEST.md` plus recent dated reports before any audit; they never certify a version — certification needs the tag scan + live control + smoke.
- **Goal mode** triggers on `/^\/goal(\s|$)/` alone (`KIMI_CODE_EXPERIMENTAL_GOAL_COMMAND` no longer exists); exit 0/3/6 = complete/blocked/paused; `/kimi:pursue` is hook-gated on every continuation turn.
- **AgentSwarm / subagents** (`features/swarm/tools/agent-swarm/agentSwarmTool.ts`, `'AgentSwarm'`, strict schema; `subagent_type` free string, default `coder`): children run in-process in a child DI scope sharing the session workspace, with their OWN eager external-hooks service — every child tool call fires the hook; no deny-all path on the swarm route. The singular `Agent` tool exists (non-strict) and is denied by every label. `KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY` is a hard cap but **unset = no cap** — `swarm.ts` always exports it (4 read / 1 write). `fork` is gated by `subagent_fork` (refused).
- **Background auto-upgrade** drifts the operator's binary out-of-band; each spawn probes the exact tuple and refuses an uncertified version. Recovery is a plugin release or `KIMI_PLUGIN_CC_KIMI_BIN`; `KIMI_PLUGIN_CC_SKIP_VERSION_PROBE` is tests/smoke only.
- **Print-mode defaults:** upstream sets subagent/swarm timeouts unbounded in `-p`; the plugin keeps mandatory finite `--budget` AbortController ceilings, hard concurrency caps, and the identity-safe cancellation barrier.
- **Toolchain**: Node >= 22.5, TypeScript, **bun** (not npm/yarn)
- **Workflow**: edit `runtime/**/*.ts` → `bun run check` (build + typecheck + test + drift gate)
- **Installed-state verification**: For questions about what a host is actually running, resolve that host's active version-stamped install cache and run its bundled `scripts/companion.sh setup --check`. The checkout, package metadata, and another host's cache are not proof of active runtime state.
- **Credential-bearing config**: Treat `~/.kimi-code/config.toml` as credential-bearing (it can hold a BYO-provider `apiKey`) **and `~/.kimi-code/credentials/` (e.g. `kimi-code.json`, mode 0600) as the real OAuth-token store** — the managed login token has lived under `credentials/` since the earliest kimi-code (source-verified), never in config.toml, so it is NOT a per-version migration. For routine status checks, do not read, print, copy, or retain the raw config **or anything under `credentials/`**; use the sanitized setup check. If an explicitly authorized diagnosis requires direct inspection of config.toml, restrict it to the plugin-managed hook block and redact all credential material from output; the `credentials/` store is never read or copied.
## Directory layout
```
.claude-plugin/ Claude Code plugin manifest (plugin.json, marketplace.json) — id stays as kimi/kimi-marketplace (no rename for v1); v0.4 users opt-in to v1 by updating the same install
.claude/ Gitignored local audit workspace. Upstream clones/reports live here, including daily monitor reports under kimi-code-research/daily-monitor/
.agents/ Codex repo marketplace sidecar (plugins/marketplace.json) — generated; source.path points at plugins/kimi-codex
commands/ Slash command markdown — thin wrappers over companion.sh (Claude Code surface)
agents/ Claude Code subagent definitions (kimi-rescue, kimi-review, kimi-challenge, kimi-ask, kimi-swarm, kimi-pursue, kimi-swarm-write)
hooks/ Stop hook for the review gate (kimi-code-side PreToolUse hook lives at runtime/hooks/)
scripts/ Shell entry points (companion.sh, review-gate-hook.sh) + dev-only surface generator (surface-registry.ts, generate-surfaces.ts)
plugins/kimi-codex/ SELF-CONTAINED Codex plugin root — GENERATED, do not hand-edit. Codex copies a plugin root to its install cache and forbids ../ escapes, so this dir bundles everything it needs:
├── .codex-plugin/plugin.json Codex manifest (skills: "./skills/")
├── skills/ 12 Codex skills that shell out to companion.sh (no MCP). Lives HERE, not at repo root, so Claude Code does NOT auto-discover them
├── scripts/ byte-mirror of companion.sh + review-gate-hook.sh (0755)
└── dist/ byte-mirror of the compiled runtime
runtime/ TypeScript source — the real runtime
├── background-spawn.ts Shared detached-worker spawn helper (rescue + ask)
├── cli-client.ts Subprocess wrapper around `kimi -p --output-format stream-json`
├── stream-json.ts Pure parser for kimi-code's NDJSON output
├── cli-cancellation.ts AbortController-based cancellation for long-running commands
├── rescue-approval.ts Workspace-bound allowlist (called by the PreToolUse hook)
├── commands/ One file per companion subcommand
├── hooks/ PreToolUse approval hook (entry script + policy + install verifier)
└── schemas/ Structured output contract for review_gate (review/challenge dropped theirs in v0.2.3)
dist/ Compiled JS — committed for zero-build install
tests/ bun test suite
```
## Commands
- `bun run check` — rebuild `dist/`, verify generated surfaces (`check:surfaces`: Claude hash gate + Codex sidecars + the `plugins/kimi-codex` runtime mirror/orphan checks), typecheck, run full test suite, then drift gate (`git diff --exit-code -- dist plugins/kimi-codex`). If `dist/` or the bundled Codex copy has unstaged changes, check fails — stage them and retry.
- `bun test <path>` — run a single test file
- `bun run build` — compile `runtime/**/*.ts` → `dist/**/*.js`
The companion runs via `scripts/companion.sh <subcommand>`, which resolves `node` and runs `dist/companion.js`. Subcommands: `setup`, `review`, `task` (`task rescue`, `task challenge`, `task pursue`, `task swarm`), `ask`, `status`, `result`, `cancel`, `replay`.
## Architecture
**Thin plugin, rich runtime** — mirrors [codex-plugin-cc](https://github.com/openai/codex-plugin-cc):
- Plugin layer (commands/, agents/, hooks/) handles routing only
- Runtime layer (runtime/, scripts/) owns subprocess lifecycle, SQLite job store, hook-side approval policy, and rendering
- Flow: `slash command → companion.sh → companion.js → kimi -p --output-format stream-json → job store → artifact`
**Key invariants:**
- Subprocess-first. One `kimi -p` process per job. Session id capture is **dual-source for kimi-code compatibility**: kimi-code 0.2.0+ emits a `role:"meta", type:"session.resume_hint"` record on stdout in stream-json mode once per prompt run at session END (after `runPromptTurn` settles), carrying a `session_<uuid>` token; kimi-code 0.1.x emitted a plain `To resume this session: kimi -r <uuid>` line on stderr. The cli-client consumes both channels — first-announce-wins, pinned, idempotent — and filters the meta record out of the consumer-facing records[] surface so commands iterating assistant/tool prose don't see it. The stderr regex anchors to line bounds and requires a full UUID payload (both bare and `session_<uuid>` alternations) so a malformed line can't pin a garbage id. 0.1.x users on text-output mode (or any future kimi version that re-introduces stderr emission with the new shape) keep working. Captured id round-trips verbatim via `kimi -r <token>` — we treat it as an opaque identifier whose shape may evolve again.
- Every model-spawning command is enforced by the PreToolUse hook in `~/.kimi-code/config.toml` because `kimi -p`'s built-in auto-approve cannot be overridden via argv. On native v2 the hook is load-bearing under the no-plan construction (Version headline): the preflight refuses `default_plan_mode`, tainted/unknown resume, and unsafe experimental selectors before process creation, and the hook denies plan-mode tools and every non-allowlisted tool. Ask/review/challenge/rescue/pursue/swarm refuse before spawning Kimi when hook verification fails; the review gate skips visibly. Claude Code `/kimi:setup` and Codex `$kimi-setup` install their host-scoped managed blocks. `KIMI_PLUGIN_CC_SKIP_HOOK_CHECK=1` is the explicit tests/diagnostics bypass for hook-verification refusals only; it does not bypass the v2 preflight or `CLI_V2_HOOK_ORDER_UNSAFE`, and is never a repair path.
- Hook verification is **strict-by-default and exact**: `verifyHookInstalled` reconstructs the canonical shell command from env via `runtime/hooks/install-paths.ts::tryBuildExpectedHookCommand` and equality-checks it against the current host's installed command. Setup/check first parse the **complete** TOML with vendored upstream `smol-toml@1.6.1`, then strictly validate every configured hook against kimi-code's config schema before applying the plugin's managed-block grammar; malformed unrelated config therefore cannot be hidden by a locally valid managed block. (The strict validation is hooks-scoped by design — unknown top-level tables such as 0.39's `[swarm]`/`[experimental]` parse and are tolerated, so new upstream config sections never lock out setup/check.) Substring and partial-parse paths are forbidden because crafted commands or malformed foreign tables can otherwise bypass setup-time safety checks. The canonical command bytes still have one source in `install-paths.ts`, consumed by install, check, the shell probe, and the per-command verifier. **Markers are NOT durable:** kimi-code persists its config via smol-toml `stringify(parse(...))` on every login/settings write, which deletes ALL comments — including the managed-block BEGIN/END markers — while the `[[hooks]]` table (data) survives and keeps enforcing. `evaluateInstalled` therefore falls back, ONLY from the absent state, to a marker-less table that is our clean enforcing hook. **That decision is made with the REAL `smol-toml` parser** (`hasCleanEnforcingHookEntry`), not a line scanner: it strips managed-block lines (host isolation), parses the remainder, and accepts only a `hooks[]` entry with `event="PreToolUse"`, `command===` the byte-exact canonical command, **NO `matcher` in any TOML spelling** (a matcher can disable/narrow the hook — `new RegExp("*")` throws → hook off → auto-approve), and no keys beyond `{event,command,timeout}`. Never regress this to a line scanner: one has demonstrated blind spots (a matcher hidden after a blank line, behind a quoted `"matcher"` key, or past a multiline array value); the parser is immune. `findBareApprovalHookTables` remains ONLY the conservative line-based PRUNE scanner. Orphan pruning (`findUnmanagedApprovalHookBlocks`) is **host-scoped** via `hostIdFromHookCommand` plus an `alsoMatchCommand` byte-exact clause — one host's install/scoped-uninstall/`--uninstall` never removes another host's marker-less live hook yet always reclaims its own by exact command even under a host-id override (`--uninstall --all` is the explicit every-host sweep; hand-rolled hooks are never touched). Invariants to re-verify each audit: the installed check stays **parser-based, matcher-rejecting, byte-exact, absent-state-only**; the prune stays host-scoped.
- **Serialized config mutation.** Install and uninstall hold a private adjacent `config.toml.kimi-plugin-cc.lock` across the complete read-modify-write transaction. Candidate publication is atomic; lock reads are no-follow/nonblocking/regular-file-only and capped; acquisition is bounded; live owners are never stolen; stale recovery uses an ABA-safe recovery lease; and inode/token ownership is rechecked before mutation or removal. A newly-created kimi-code home is mode `0700`, and both config and lock files are mode `0600`. Valid inline `hooks = [...]` assignments are normalized to canonical `[[hooks]]` tables with an explicit formatting/comment-loss warning before insertion. `/kimi:setup --check` exits nonzero when any whole-file, hook-schema, managed-block, path, or shell-probe check fails.
- **Host-scoped managed blocks.** Claude Code and Codex install the plugin to **different, version-stamped paths** (`~/.claude/plugins/cache/…` vs `~/.codex/plugins/cache/…`) but share one `~/.kimi-code/config.toml`. The managed block is therefore keyed by a **host id** — the marker carries a `:<host-id>` suffix (`# === BEGIN kimi-plugin-cc-managed:claude-code (vX) ===`). **Never collapse back to one shared block:** a single un-suffixed block was exact-matched to the running host, so `/kimi:setup` (Claude) and `$kimi-setup` (Codex) overwrote each other and the loser's write-commands refused with "hook path drift." `resolveHostId` (`install-paths.ts`) derives the id *version-independently* from the hook path (`.claude`→`claude-code`, `.codex`→`codex`, else `host-<sha1[:16]>`; `KIMI_PLUGIN_CC_HOST_ID` overrides), so a plugin upgrade **refreshes** the same host's block instead of accumulating one per version. `parseManagedBlock(contents, hostId)` resolves to the current host's own suffixed block (a lone legacy un-suffixed block is adopted as a fallback); `evaluateInstalled` **requires** `hostId` and keeps the per-host exact-equality contract. `runInstall` upserts only this host's block, converts a matching legacy block in place, and prunes orphaned marker-less approval-hook `[[hooks]]` (via `findUnmanagedApprovalHookBlocks`/`isOurApprovalHookCommand` — strict: canonical two-single-quoted-token command whose script is `approval-hook.js` under a kimi-marketplace/kimi-plugin-cc tree, so a hand-rolled hook is never removed). `runUninstall` is host-scoped by default; `/kimi:setup --uninstall --all` removes every host's block. Enforcement stays correct under coexistence because kimi-code fires every `[[hooks]]` and aggregates **any-block-wins** — the current host's live hook always denies; a stale *other*-host path fails open harmlessly; a drifted *own* block is caught at the per-command verify gate (prompts a re-setup for that host only). Invariant to re-verify: the exact-equality check is per-host, and no install/uninstall path ever mutates another host's block.
- Rescue defines the direct-workspace write allowlist used by both rescue and pursue. Workspace allowlist (shell-quote parser, mutating-flag detector, symlink reject, path-realpath check) lives in `runtime/rescue-approval.ts` and is called by the hook via `evaluateRescueHookRequest`. Mutating-flag list includes `--fix`/`--write`/`--apply`/`--in-place`/`-i`/`-w` **and** the output-to-file class (`--output`/`--output-file`/`--output-directory`/`--output-dir`, exact + `=` prefix) so tools can't write outside the workspace via their own report-output mechanism (e.g. `git diff --output=`, `curl --output`, `eslint --output-file=`). The `-o` short form is rejected per-tool where its semantics are write-shape (`eslint -o`, `go -o`, `ruff check -o`, `sort -o`; `uniq IN OUT` in pipelines). A global `hasExecDelegatingFlag` rejects, for every binary, flags whose *value* runs as a command/tool — `--open-files-in-pager` (a CRITICAL RCE: `git grep --open-files-in-pager=<cmd>` makes git exec `<cmd>`, the same pager-smuggle class as the pre-subcommand `git -c core.pager=` defense but via a subcommand flag; the `-O` short form is rejected git-locally to avoid colliding with `find -O<level>`), `-vettool`/`-toolexec`/`-exec`. `mypy --junit-xml`/`--*-report` and `pytest --junitxml`/`--result-log`/`--report-log` are rejected (report-writers escape the workspace). Test runners (`go test`/`cargo test`/`pytest`) still execute repo code by design — documented as an explicit trust boundary in `docs/safety.md`.
- Rescue cannot mutate git state. The main Claude thread owns branch/commit.
- **`/kimi:pursue` (experimental) = autonomous goal mode.** Exposes kimi-code 0.8.0+ headless goal mode (`kimi -p "/goal ..."`) so Kimi pursues an objective across continuation turns. Goal mode is reached by the `/goal` prompt prefix; on 0.8–0.11 it is *also* gated by the `goal-command` experimental flag, which `pursue.ts` sets per-spawn via `KIMI_CODE_EXPERIMENTAL_GOAL_COMMAND=1` — redundant since 0.12.0 removed that gate, but keep it for 0.8–0.11 compat. It is **write-capable and reuses the rescue trust boundary verbatim**: `runtime/commands/pursue.ts` spawns with `commandLabel: "rescue"` (so the PreToolUse hook applies the same workspace allowlist on *every* continuation turn — every executed tool call passes the managed hook under the no-plan construction, verified end-to-end by the goal-mode real-binary smoke), refuses without the hook, reuses the `command_type:"rescue"` job lineage (so status/result/cancel work unchanged), and cannot mutate git state. The only new risk is unboundedness, bounded by a **mandatory finite `--budget` wall-clock AbortController ceiling** (default 45m) — the sole hard bound (`--turns` is a soft prompt-injected hint). Runtime-foreground-only (no detached-worker flag), but the *caller* detaches its own shell call — same contract as write-swarm (see that bullet), except pursue's edits land in the real tree and cancel does not roll them back. No `--resume` (goal mode's `goalId` differs from the resume-hint `sessionId`; the goalId is captured/surfaced but resume is deferred until upstream reconciles the split). Exit codes 0/3/6 (complete/blocked/paused) are terminal, not failures. See `docs/safety.md` § "Autonomous goal mode".
- **`/kimi:swarm` = read-only parallel fan-out.** Exposes kimi-code 0.12.0+'s `AgentSwarm` tool so Kimi fans a **read-only** review out across subagents (one per target) and consolidates the findings. `runtime/commands/swarm.ts` spawns with `commandLabel: "swarm"` — a hook label (`runtime/hooks/approval-policy.ts`) that allows the read-only tool set PLUS the exact tool name `AgentSwarm` (verified `readonly name = 'AgentSwarm' as const`), and nothing else (the singular `Agent` tool is denied). The coordinator MUST be allowed to call `AgentSwarm` or the swarm never launches; every spawned subagent inherits the `swarm` label and fires the SAME hook through its own eager external-hooks service (swarm subagents use the standard before-execute channel — no `deny-all`, see the compat note above), so a subagent's write/edit/shell is denied exactly like a single-turn review's → **read-only swarm opens ZERO new write surface.** It reuses the `command_type:"review"` job lineage (status/result/cancel work unchanged; `agent_profile` is `<swarm>`). Like every model-spawning command, swarm **REFUSES without the hook**; its fan-out makes the reason especially acute (`KIMI_PLUGIN_CC_SKIP_HOOK_CHECK=1` still explicitly opts out). The only new risk is COST/runaway from N parallel runs (+ possible nested `AgentSwarm`), bounded by a **mandatory finite `--budget` wall-clock AbortController ceiling** (default 30m). Two **distinct** subagent bounds: `--cap N` is a SOFT prompt-injected *total*-count hint (the hook is stateless — it can't count subagents), and `--max-concurrency N` is a HARD ceiling on *concurrent* subagents on **kimi-code 0.18.0+** (`KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY`) — `swarm.ts` passes the latter via `CliClientOptions.swarmMaxConcurrency`, which `cli-client.ts::buildEnv` exports per-spawn (older binaries ignore the unknown env var). **Never re-merge the two flags into one** (the "cap-as-total-count illusion"): a user reading `--cap N` expects a total-cost bound, but the env var caps only *peak* parallelism — a coordinator can still launch many subagents sequentially. `--max-concurrency` defaults to **4** (`resolveSwarmMaxConcurrency` in `swarm.ts`) when the user passes none, so the env var is ALWAYS exported and an auto-dispatched fan-out is never unbounded on 0.18.0+. Foreground-only; `--write` mode lives in the next bullet. **A model-invocable `kimi-swarm` agent wraps this command** so the main Claude thread can dispatch a read-only fan-out autonomously (distinct from the human-only `/kimi:swarm` slash command; agents are the model-invocable path, as for review/challenge/ask/rescue/pursue). Auto-dispatch widens no write surface (swarm is read-only) and removes no cost bound: `--budget` plus the default `--max-concurrency` (4, enforced on 0.18.0+ for every run) hold whoever launches it. See `docs/safety.md` § "Read-only swarm".
- **`/kimi:swarm --write` = write-capable parallel fan-out.** `runtime/commands/swarm.ts::runWriteSwarm` creates an **ephemeral detached git worktree off HEAD** under the plugin's own `worktreesDir` (never inside the user repo), spawns the coordinator `kimi -p` with **cwd = that worktree** (the load-bearing `spawnCwd` seam in `executeSwarmJob`; `job.cwd` stays the user's real cwd for lineage), and prompts `subagent_type:"coder"` (write-capable) to edit **disjoint** targets with no git and no nested `AgentSwarm`. Spawns with `commandLabel: "swarm-write"` — a hook label that allows `AgentSwarm` + the read-only set and routes write/edit/shell through `evaluateRescueHookRequest` **verbatim**, BUT scoped to a **forge-proof trusted worktree root**: `cli-client.buildEnv` exports the worktree path as `KIMI_PLUGIN_CC_WORKSPACE_ROOT`, the entry script passes it as `ctx.trustedWorkspaceRoot`, and the `swarm-write` case confines writes to THAT (not the hook payload `cwd`) — the model inside kimi cannot forge an env var on its own process, so confinement does not depend on upstream payload-cwd derivation. **Fail-closed**: a missing trusted root denies all writes. Confinement works only because kimi-code runs `AgentSwarm` subagents in-process in a child DI scope sharing the session workspace, each with its OWN eager external-hooks service (no deny-all on the swarm route; the `btw` side-question path is separate — re-verified at 0.42.0), so every `coder` subagent's write fires the managed hook and is confined to the one shared worktree → **the user's real tree is never touched**; re-verify that permission-stack claim each audit. The plugin captures the change set as an **untruncated, applyable `.patch`** (`git add -N` intent-to-add + `git diff --binary`, writing NO loose objects into the shared object DB) on **every** terminal path (success/cancel/budget/failure) BEFORE removing the worktree; a startup sweep reaps orphans from a hard kill. **The plugin never applies or commits — the main thread owns the merge** (`docs/safety.md`); the result report prepends the patch path. Reuses the `command_type:"rescue"` lineage (`agent_profile "<swarm-write>"`). **Refuses** without the hook (like read swarm). Gated to kimi-code **≥ 0.18.0** (the hard `KIMI_CODE_AGENT_SWARM_MAX_CONCURRENCY` cap — below it a write fan-out has no peak bound), a git repo with a **born HEAD** (`WRITE_SWARM_NOT_A_REPO`/`WRITE_SWARM_NO_HEAD`), with a **loud dirty-tree warning** (worktree is off HEAD, so uncommitted work is not included). Default `--max-concurrency` is **1** (`SWARM_WRITE_DEFAULT_MAX_CONCURRENCY`) — writes serialize since disjoint-target partitioning is prompt-only. **Runtime-foreground-only ≠ caller-foreground-only:** the parser rejects `--background`/`--wait`/`--fresh`/`--resume`, but the *caller's* shell call is separate and detaching it is the documented default — a fan-out outlives a foreground timeout (Claude Code caps foreground Bash at 10m vs the 30m default budget), and the bounds (`--budget`, `--max-concurrency`, hook-required, patch-only) are runtime-enforced, not watcher-enforced. **Cancellation is no-id:** job ids are `randomUUID()` and a detached run emits nothing until its final report, so the stop path is `companion.sh cancel` with NO id (`findLatestJob({runningOnly:true})` for the repo); never document "the agent keeps the job id from the launch output" — that flow cannot execute. With several runs in flight the no-id form takes the most recent, so a multi-run session confirms first. **Prefer that command over Esc/TaskStop:** a harness interrupt is SIGTERM→SIGKILL with ~1.35s of grace, delivered process-group-wide (so no Kimi descendant survives), which is less than `cli-client.ts`'s 1500ms child escalation + quiescence check + `git diff --binary` capture — interrupting can therefore kill the companion mid-teardown and lose the patch, while the cancel command signals from a separate process not racing that deadline. The margin is thin; re-measure on harness changes. **Pursue is NOT swarm-write:** its edits land in the user's real tree and cancel does not roll them back; swarm-write's stay in a throwaway worktree and return as a discardable patch. Keep that asymmetry visible in the agent docs. **A model-invocable `kimi-swarm-write` agent** wraps this command: auto-dispatch widens no write surface and removes no bound — it runs the identical `runSwarm --write` (patch-only; the plugin never applies/commits; worktree confinement + managed hook + `--budget` + default `--max-concurrency` 1 + hook-required all hold), making it a *less* dangerous agent than the write-capable `kimi-pursue` (which writes directly to the workspace). Strict triggering: BOTH many disjoint write targets AND explicit fan-out intent, else steer to `kimi-rescue`/`kimi-swarm`/`kimi-pursue`. See `docs/safety.md` § "Write-capable swarm".
- The `goal.summary` stream-json record (kimi-code 0.8.0+, role-less) is recognized by `runtime/stream-json.ts` on a dedicated `StreamJsonOutcome.goalSummary` channel (the `StreamJsonRecord` union stays role-keyed; no `.role`-consumer churn) and captured out-of-band on `CliClientResult.goalSummary`, mirroring the resume-hint meta record. kimi-code 0.23.5's `role:"meta", type:"turn.step.retrying"` record is modeled exactly and normalized, then filtered out of consumer `records[]`/`onRecord` with the other wrapper metadata; malformed retry shapes still enter diagnostics.
- Jobs in SQLite are the source of truth. Terminal states are permanent.
- Review/challenge/ask/rescue are all prose pass-through (review and challenge emit no JSON schema). Review gate is the only command that still parses Kimi output (JSON allow/block).
- Review gate is a Stop hook, disabled by default, fail-open on malformed output. It skips visibly without spawning Kimi when hook enforcement is missing or invalid; other skip cases (missing assistant message, unreadable transcript) also surface their reason through `systemMessage`.
- LLM-caller discipline: stderr is for humans only; anything load-bearing for an agent caller goes in stdout, exit codes, or persisted SQLite state. Parsers hard-fail on unknown flag-shaped tokens (INVALID_ARGS) instead of warn-and-swallowing — wrappers never see stderr warnings. `RuntimeError` carries an optional `details: Record<string, unknown>` for structured failure context.
- Default command stdout: review/challenge/ask/rescue emit raw prose; status emits raw job-row JSON; result emits raw artifact markdown. `result <jobId> --json` opts into a structured envelope (`{job_id, kind, status, summary, error, artifact_path, body, created_at, completed_at}`) for downstream automation.
- Cancellation uses AbortController + SIGTERM → SIGKILL (1500ms default escalation) and treats teardown as a settlement barrier: neither a normal result nor a budget error reaches its caller while the owned teardown is still running. POSIX spawns detached, captures descendant ancestry plus PID/PGID/start-time identity in one bounded process-table snapshot (no PID-only handoff), revalidates identities in batched bounded passes before positive-pid or group signals, starts the grace interval only after SIGTERM dispatch, and waits through a bounded 500ms post-SIGKILL quiescence check. A changed identity is never signaled; unexpected teardown failure rejects explicitly and emergency-kills the controlled root rather than stranding settlement. This closes the normal close-before-escalation race that could let a detached Bash grandchild keep writing while write-swarm captured and removed its worktree. A post-kill probe timeout is logged and bounded rather than hanging the host forever. win32 retains direct-child-only cancellation, so descendant reaping there remains a known gap. The pre-listener abort race is still covered by re-checking `signal.aborted` after listener attachment.
- Config writes preserve mode 0o600. `writeConfigAtomic` chmods the temp file before rename so the final inode never exists at a wider mode, matching the user's existing kimi-code config permissions (the file holds API keys + tokens).
## Post-GA roadmap
See [ROADMAP-TO-GA.md](./ROADMAP-TO-GA.md) for the full pre-GA history and v1.1 backlog. **v1.0.0 GA shipped 2026-05-26**, closing the kimi-code 0.2.0 stream-json session-meta gap (caught by alpha.4's loud-warning surface in production smoke), H6 (kimi-version probe at setup time), H2 (closed by upstream + plugin consumption), and H3 partial (forward-compat for unknown meta types). **2026-05-27 audit**: 4-reviewer pass verified compat through kimi-code 0.4.0 (released same day); no runtime changes required. Remaining: H1 (hook fail-open runtime drift) and H5 (per-spawn thinking control — upstream-blocked). **H3, H4, H8, H9 closed in v1.2.x** (see the Version line above), and **H7 is effectively complete** — the local real-binary smoke (`tests/runtime/real-binary-smoke.test.ts`, `bun run smoke:real`, delivered 2026-05-28) ships and runs on manual CI dispatch (`smoke.yml`); wiring it to *every* push is gated only on adding a `KIMI_MODEL_API_KEY` secret and is a deliberate cost choice, not a blocker.
## When editing
- Read the code before changing it — the runtime has specific invariants that aren't obvious from file names
- Run `bun run check` before considering any change done
- After any `runtime/**` (or `scripts/*.sh`) change, regenerate the Codex package: `bun run build && bun run generate:surfaces`, then stage `dist/` AND `plugins/kimi-codex/`. The `plugins/kimi-codex/` tree is GENERATED — never hand-edit it; edit `runtime/`/`scripts/`/`scripts/surface-registry.ts` and regenerate.
- Codex sidecars are generated by `bun run generate:surfaces` from `scripts/surface-registry.ts`. `bun run check:surfaces` (run after `build` inside `check`) verifies: the Claude plugin bytes match the locked hash gate, the generated Codex text surfaces are current, the bundled runtime mirror (`plugins/kimi-codex/{scripts,dist}`) byte-matches root, and no orphaned generated files remain.
- Editing a Claude command/agent or any `.claude-plugin/*` file changes its bytes, so you MUST re-pin its sha256 in `CLAUDE_SURFACE_HASHES` (`scripts/surface-registry.ts`) — `shasum -a 256 <file>` — or `check:surfaces` fails closed.
- `dist/` and `plugins/kimi-codex/` are committed intentionally (zero-build install for both hosts; Codex copies the subfolder to its cache). The drift gate catches forgotten rebuilds/regenerations.
- Agent files register at session start. Adding or editing `agents/*.md` mid-session doesn't activate them until Claude Code reloads — reach for slash commands or direct `companion.sh` in the same session.
- `.claude/` is gitignored — notes, worktrees, internal docs under it stay local. Don't try to commit them.
## Releasing
`runtime/version.ts` is the SINGLE version source — `scripts/surface-registry.ts` (`PLUGIN_VERSION`) imports it, so the Codex manifest/marketplace version propagates automatically. A version bump touches these and then regenerates:
- `runtime/version.ts` (`KIMI_PLUGIN_CC_VERSION` — written into the managed-block marker comment by /kimi:setup; imported by surface-registry)
- `package.json`
- `.claude-plugin/plugin.json` — **also re-pin its sha256 in `CLAUDE_SURFACE_HASHES`** (its bytes change)
- `.claude-plugin/marketplace.json` — **also re-pin its sha256 in `CLAUDE_SURFACE_HASHES`**
- `AGENTS.md` — bump the version number, and **REPLACE** the Version-line headline if the release changes a standing contract. The headline states the **contract an agent must not regress**, in present tense — NOT what the release did. "v1.9.1 closes the test gap that let X reach review" is a changelog entry; "detached-worker tests MUST run a real node subprocess with no override" is a headline. Keep ONE, never append. Everything a release *did* — dates, PR numbers, what broke, how it was reviewed — goes in `CHANGELOG.md` / ROADMAP's Post-GA audit log, never here. Same rule for the invariant bullets below: state today's contract plus the rationale that stops it being re-broken, and let the archaeology live in the log files.
- `CHANGELOG.md` (add a new version section — see its top banner. Docs-only compat checkups that DON'T bump the version are logged in ROADMAP's Post-GA audit log instead, not here.)
- then run `bun run build && bun run generate:surfaces` to regenerate `plugins/kimi-codex/` (manifest version + bundled runtime), and stage it.
Then `bun run check`, commit, `git tag -a vX.Y.Z -m "..."`, `git push` + `git push origin vX.Y.Z`, then `gh release create`.