kimi¶
Kimi Code reads the project AGENTS.md from the filesystem natively, and also reads a
user-scope AGENTS.md at ~/.kimi-code/AGENTS.md. So agedum leaves the project
instructions in place and binds only the global ones. Skills, both scopes, are injected as
binds like Claude's.
Wrapper resolution¶
Project instructions — Kimi Code merges every AGENTS.md from the project root (the
nearest .git) down to the working directory into its system prompt (KIMI_AGENTS_MD). The
agent-neutral source's AGENTS.md already sits at the project root, which is exactly where
Kimi looks, so agedum injects nothing for it — and never tries to, since that root
AGENTS.md is typically git-tracked.
Global instructions — Kimi Code also reads a user-scope AGENTS.md at
~/.kimi-code/AGENTS.md, so the global AGENTS.md (base merged with an optional
AGENTS.kimi.md
overlay) is bound there:
| Source | Injected at |
|---|---|
global ~/.config/agents/AGENTS.md |
~/.kimi-code/AGENTS.md |
Both scopes merge natively into KIMI_AGENTS_MD — the project AGENTS.md by tree discovery,
the global one from the user-scope path — so agedum appends no flag for instructions. A
project with no global scope needs no injection at all: its AGENTS.md is read natively. This
mirrors the Claude harness — each scope kept distinct, never merged.
Skills — bound into the directories Kimi Code reads automatically:
| Source | Injected at |
|---|---|
global ~/.config/agents/skills/ |
~/.kimi-code/skills/ |
project .agents/skills/ |
<root>/.kimi-code/skills/ |
- Skills use the
SKILL.kimi.mdoverlay where present; assets are copied verbatim. - The AGENTS.md and skills binds land at paths Kimi Code already reads, so there is no
config rewrite and
extra_argsstays empty. ~/.kimi-codeabove is whateverKIMI_CODE_HOMEnames — a provider config that generates aconfig.tomlrelocates it, and these targets follow.
Provider config¶
Kimi Code reads its API token from the environment, so the key goes in requiredEnv (or
secretEnv, which is appended automatically) and is exported into the child unchanged. The
config knobs become appended CLI flags on the launched command:
{
"harness": "kimi",
"slug": "kimi",
"secretEnv": "MOONSHOT_API_KEY",
"requiredEnv": ["MOONSHOT_API_KEY"],
"config": { "model": "kimi-k2", "yolo": true }
}
config key |
Appended |
|---|---|
model |
--model <model> |
plan |
--plan (true) |
yolo |
--yolo (true) |
binary |
overrides the launched CLI name (default kimi) |
The config above launches kimi --model kimi-k2 --yolo, with MOONSHOT_API_KEY in the
environment. --run seeds a one-shot task with --prompt "<text>" (Kimi Code's --prompt
runs once and exits) — it drops --yolo/--plan, which Kimi Code refuses to combine with
--prompt. --prompt (seed-then-stay interactive) is unsupported and fails loudly — see the
prompt-seeding table.
Kimi Code dropped the
--thinking/--no-thinkingflags; thinking is now a config setting, so it is applied only through the generatedconfig.tomlbelow (which needsbaseUrl).
Custom endpoint — baseUrl¶
Kimi Code has no base-URL flag, its config does not interpolate $ENV (so a key can't
be referenced by name the way pi's models.json does), and there is no --config-file
flag. To run Kimi Code against an arbitrary OpenAI-/Anthropic-compatible endpoint, set
baseUrl: agedum then generates a config.toml with one provider (named agedum) and one
model, bakes the resolved key into it (masked in --dry-run, like opencode's
OPENCODE_CONFIG_CONTENT), and binds it over ~/.kimi-code/config.toml — the file Kimi reads
from its data dir. Because the bind replaces that file inside the namespace, the generated doc
is self-sufficient; Kimi fills every other setting from its own defaults.
{
"harness": "kimi",
"secretEnv": "OPENCODE_GO_API_KEY",
"config": {
"baseUrl": "https://opencode.ai/zen/go/v1",
"providerType": "openai",
"model": "kimi-k2.7-code",
"contextWindow": 262144,
"thinking": true
}
}
config key |
Generated config.toml field |
Default |
|---|---|---|
baseUrl |
providers.agedum.base_url (turns this mode on) |
— |
providerType |
providers.agedum.type |
openai |
model |
models.<model>.model + default_model (required) |
— |
contextWindow |
models.<model>.max_context_size |
262144 |
capabilities |
models.<model>.capabilities |
["thinking"] |
thinking |
[thinking].enabled (only when set) |
— |
effortLevel |
[thinking].effort (only when set) |
— |
supportEfforts |
models.<model>.support_efforts |
— |
defaultEffort |
models.<model>.default_effort |
— |
models |
one [models.<id>] table per entry (see Several models) |
— |
subagentModel |
[secondary_model].model |
— |
subagentEffort |
[secondary_model].default_effort |
— |
(secretEnv value) |
providers.agedum.api_key (resolved key, baked in) |
— |
The above launches kimi --model kimi-k2.7-code, reading the generated config.toml.
baseUrl requires model + secretEnv. providerType must name a Kimi Code provider type
(openai for an OpenAI Chat Completions surface, anthropic, kimi, google-genai,
openai_responses, vertexai).
A generated config moves the Kimi home. Kimi Code refreshes its provider-model catalogue
at startup and persists it by writing a temp file and renaming it over config.toml — and
a rename cannot replace a bind mount, so a read-only bind there fails EBUSY and the harness
reports Skipped refreshing <provider> on every launch. So a baseUrl launcher instead gets
its own Kimi home: agedum sets KIMI_CODE_HOME to ~/.cache/agedum/kimi/<endpoint-model
slug> and seeds config.toml (and mcp.json) there as real files. The rewrite lands, the
user's own ~/.kimi-code is untouched, and the launcher stays authoritative because agedum
re-seeds every launch — whatever Kimi discovered in-session is replaced by the declared
config. kimi_config_dir() reads the same variable, so the instruction and skill targets
follow into that home. The dir is derived from endpoint + model, so repeat launches of one
launcher reuse it (skills, sessions, logs); two launchers do not share session history, and
neither sees ~/.kimi-code's. Without baseUrl nothing is generated: Kimi runs on its own
account config in ~/.kimi-code, and an injected mcp.json is read-only bound there as before.
Several models — tiers and subagents¶
model alone declares one model. A models map declares several on the same provider,
and model picks which one is default_model:
{
"config": {
"baseUrl": "https://api.kimi.com/coding/v1",
"providerType": "kimi",
"model": "k3",
"subagentModel": "kimi-for-coding",
"thinking": true,
"effortLevel": "high",
"models": {
"k3": {
"contextWindow": 1048576,
"capabilities": ["thinking", "always_thinking"],
"supportEfforts": ["low", "high", "max"],
"defaultEffort": "high"
},
"kimi-for-coding": { "contextWindow": 262144 }
}
}
}
Each entry takes the same per-model keys as the single-model form (contextWindow,
capabilities, supportEfforts, defaultEffort); every model rides the one generated
agedum provider, so a models map is a tier list on one endpoint, not a second
endpoint. Setting models and a top-level per-model key is an error: once the map exists
the top-level value would apply to no model, so agedum rejects it instead of dropping it.
model must name one of the declared entries.
subagentModel is the second tier. Kimi Code spawns subagents on [secondary_model], so
this is how a launcher pairs a wide primary with a cheaper subagent model (the harness
analogue of a per-agent model list). It must name a declared entry — Kimi Code fails subagent
spawning when [secondary_model].model resolves to nothing, and kimi doctor does not
catch a dangling pointer (verified: a [secondary_model] naming an undeclared model still
reports "All checked config files are valid"), so agedum rejects it at launch instead.
subagentEffort sets that entry's default_effort and must appear in that model's
supportEfforts. Both are overridable at runtime by Kimi Code's own KIMI_SECONDARY_MODEL /
KIMI_SECONDARY_EFFORT.
Subagent tiering is an experimental Kimi Code flag (secondary-model, default off) —
without it [secondary_model] parses cleanly and is simply never consulted. So a
subagentModel also makes agedum emit [experimental] secondary-model = true, the config
seam the flag resolver reads (keyed by flag id); the KIMI_CODE_EXPERIMENTAL_SECONDARY_MODEL
env var still overrides it at runtime.
Thinking effort¶
effortLevel sets [thinking] effort, but on the kimi wire protocol (providerType:
"kimi") Kimi Code resolves that value against the model's support_efforts list, and the
failure modes are both silent-ish:
- no
supportEfforts→ Kimi normalises the effort away to plainon, so the configured effort is discarded while the config still reads as set; - an effort outside
supportEfforts→ Kimi raisesMODEL_CONFIG_INVALIDat launch.
So agedum requires supportEfforts whenever effortLevel is set on providerType: "kimi",
and rejects an effortLevel the list doesn't contain — a config that would no-op is an error,
not a surprise at runtime. A model's own roster entry reports these under think_efforts
(valid_efforts / default_effort) in GET /models; mirror them:
{
"config": {
"providerType": "kimi",
"model": "k3",
"contextWindow": 1048576,
"thinking": true,
"effortLevel": "max",
"supportEfforts": ["max"],
"defaultEffort": "max"
}
}
Widening supportEfforts is the seam for later: when a model accepts more efforts, list them
and effortLevel can move off max. On a non-kimi providerType the guard does not apply —
a compatible backend receives the effort string unchanged and makes its own decision.
With a models map the check runs against the default model's entry —
[thinking] effort applies to the session's model, and a model reached by switching later is
Kimi Code's own check at switch time. The subagent tier is checked separately, through
subagentEffort against [secondary_model].model's own list.
MCP servers¶
Kimi Code reads MCP servers from mcp.json, never config.toml, so mcpServers becomes a
second generated doc bound at ~/.kimi-code/mcp.json. It is bound read-only rather than merged,
so the launcher declares its own server set instead of inheriting the host's:
{
"config": {
"mcpServers": {
"context7": { "command": "npx", "args": ["-y", "@upstash/context7-mcp@latest"] },
"playwright": { "command": "npx", "args": ["-y", "@playwright/mcp@latest"] }
}
}
}
Entries use Kimi's MCP shape: stdio takes command (+ args, env, cwd); HTTP takes url
(+ bearerTokenEnvVar for a static token from the environment). mcpServers is independent of
baseUrl — a launcher can inject MCP without generating a config.toml. Kimi also reads a
project-root .mcp.json (Claude-compatible) on its own; agedum does not touch that file.
kimi is not part of the canonical translation. claude and opencode
have their mcpServers block rewritten into their own dialects, including respelling
${VAR} placeholders; kimi keeps the verbatim passthrough above. Kimi Code is not known to
expand ${VAR} in mcp.json, so rather than hand a server the literal string ${TOKEN},
a placeholder in a kimi entry is a fail-loud error — use bearerTokenEnvVar for a
remote token, or write the value literally. This is what stops a shared MCP base, extended
onto a kimi launcher, from breaking silently.