🔗 Your Sub-Agents Follow You To Kimi
The ten harness operators in this repo now run under Kimi Code as well as Claude Code — from one set of files. ui-builder still carries the frontend rules, effect-ts-enforcer still carries the five non-negotiables, verify-done-runner still carries the checklist. Switch CLI and they're all still there, because there is no second copy to keep in sync.
One symlink does it: .kimi-code/agents → .claude/agents. The same trick CLAUDE.md → AGENTS.md has always used for instructions, now applied to the roster.
What you get
- 🔗 One roster, two CLIs —
.kimi-code/agentsis a symlink, committed at git mode120000, so a fresh clone gets the link and not a snapshot. Add an agent under.claude/agents/andkimisees it with no second step. - 🧭 The full harness, verbatim —
brain-navigator,ui-builder,design-critic,recipe-runner,test-author,span-instrumenter,effect-ts-enforcer,verify-done-runner,feature-tracker,feature-verifier. Claude's frontmatter loads as-is: Kimi parsesname,description,tools(comma-string or list),disallowedTools,subagents,override, and skips unknown keys rather than rejecting the file.Read/Write/Edit/Glob/Grep/Bashare named identically in both, so the read-only operators stay read-only. - 🚦
harness-checkcheck D2 — asserts the symlink resolves onto.claude/agents. Not "does the path exist" — identity. A copied directory passes an existence test right up until someone edits one side, and drift is the entire failure being prevented. - 🙈
.gitignoreshares exactly one path —/.kimi-code/*ignored,!/.kimi-code/agentsun-ignored. The roster is shared; per-machine Kimi session state never enters the repo.
Five files, 55 insertions. The capability is a symlink; the rest is making sure it can't quietly stop being one.
Why not just copy the files
Kimi ships its own importer, /import-from-cc-codex, and reading its instructions is what settled the design. It copies skills, and it explicitly refuses to migrate custom agents at all — "Do not migrate … custom agents/subagents."
So the supported path gives you nothing here, and the obvious workaround gives you two rosters that agree only until the next edit. A symlink has no such window. pushExistingRoot resolves every agent root through fs.realpath before use, which is exactly why pointing one at another directory works.
Found the hard way: the first draft of D2 compared $(cd .kimi-code/agents) against $(cd .claude/agents) — and when the symlink dangles, both cd calls fail, both substitutions come back empty, and "" = "" reports PASS while Kimi can load nothing at all. A false green inside the check written to prevent silent drift. Greptile caught it pre-PR; each side now resolves into its own variable and must be non-empty before comparison, and the invariant was driven into all five states — dangling, wrong target, real directory, missing, correct — with only the last one passing.
For the agent-first crowd
- 🤖 The discovery path isn't in Kimi's public docs. It was read out of the binary —
PROJECT_BRAND_DIRS = [".kimi-code/agents"], roots resolved viarealpath, frontmatter parsed byparseAgentFileText— then confirmed empirically withkimi --agent <bogus>, which prints the discovered roster and costs no model call. A useful probe: it's the cheapest way to ask any Kimi workspace what it can actually see. - 📎 Two things deliberately don't carry over, documented in
.claude/agents/README.mdinstead of papered over. Claude'smodel:is ignored — Kimi's field ismodel_preference— sotest-authorruns on Kimi's configured model rather thanopus. AndREADME.mdhas no frontmatter, so Kimi logs oneSkipping invalid agent fileWARN per session. Neither is worth editing ten agent files to satisfy a second consumer. - 🚧 Slash commands, hooks and skills are not shared — those are Claude-specific formats. Root
AGENTS.mdneeded no work at all; Kimi reads it natively, which is the whole reason that file is the source of truth andCLAUDE.mdthe symlink. - ✅ Gates:
harness-check11 passed, 0 failed. The one⊘is the pre-existing--strictgrandfather ratchet — 7 shipped features still awaiting receipts, unchanged by this release. Notypecheck/testrun and none claimed: nothing underapp/moved, and the pre-commit hook reached that conclusion on its own ("no code changes — skipping typecheck/test"). A shell gate gets verified by being driven into every failure state, which is what happened.
Full diff: v1.7.0...v1.8.0