Kimi Code CLI + tman
The hook can allow, ask, or deny — but not rewrite. tman still decides whether the command runs; getting it supervised takes a shim or a denial the model can act on.
- vendor
- Moonshot AI
- integration
- gates
- hook event
- PreToolUse
- config
- ~/.kimi-code/config.toml
Kimi Code CLI supports lifecycle hooks that run local commands to gate risky tool calls, declared as [[hooks]] entries in config.toml. What is not documented is whether a PreToolUse hook can rewrite the tool's arguments — so this guide treats it as a gate, and puts the weight on the shims, which do not depend on the answer.
1. Install tman and adopt the project
Everything below assumes the binary is on your PATH and the repository has a .tman.kdl. That file is what scopes caps to this project, and it is also the signal several of the integrations test for before they do anything.
npm install -g @standardbeagle/tman # or the install.sh one-liner
cd your-project
tman init --shims --gitignoretman init writes a defaults block and an alias for each test or build command it can detect. Commands it cannot detect are left commented out rather than stubbed, so an alias you never filled in fails loudly instead of exiting 0 having run nothing.
2. Register the hook
Config lives under ~/.kimi-code/ by default, relocatable with KIMI_CODE_HOME; a project-local .kimi-code/local.toml carries workspace settings. The hook timeout here is in seconds.
[[hooks]]
event = "PreToolUse"
matcher = "Bash"
command = "~/.config/tman-gate.sh"
timeout = 10Turn the rewrite into an actionable denial
Because this hook cannot replace the command, the useful move is to refuse the unsupervised one and hand the model the supervised string to re-issue. tman already knows which commands deserve that and what the replacement should be, so the adapter is a few lines around tman hook pretooluse rather than a second copy of the classifier.
#!/usr/bin/env bash
# tman-gate.sh — deny an unsupervised test/build command, naming its supervised form.
set -euo pipefail
payload=$(cat)
# .tool_input.command // .toolArgs.command // empty
command=$(jq -r '.tool_input.command // .toolArgs.command // empty' <<<"$payload")
[ -n "$command" ] && [ "$command" != "null" ] || exit 0
verdict=$(jq -nc --arg c "$command" --arg d "$PWD" \
'{tool_name:"Bash", cwd:$d, tool_input:{command:$c}}' | tman hook pretooluse)
# tman stays silent on anything it will not supervise. So does this.
[ -n "$verdict" ] || exit 0
supervised=$(jq -r '.hookSpecificOutput.updatedInput.command // empty' <<<"$verdict")
[ -n "$supervised" ] || exit 0
jq -nc --arg s "$supervised" '{
permissionDecision: "deny",
permissionDecisionReason: ("Run it supervised instead: " + $s)
}'Confirm the payload field before you trust it. Hook payload shapes move between versions, and a
jqpath that no longer matches yields an empty command and a script that silently allows everything — a false green. Replace the script withcat > /tmp/hook-payload.jsonfor one run, read the file, then wire the real adapter against the field name you actually saw.
Find out whether your build rewrites
It is worth five minutes to learn which behaviour you have, because a rewrite is strictly better than a denial. Log one payload, then try emitting a rewrite and see whether the command changes:
# 1. capture a real payload
cat > /tmp/kimi-payload.json # as the hook command, for one run
jq . /tmp/kimi-payload.json # read the field names you actually got
# 2. if the field names match Claude Code's, try the native hook directly
# (tman requires tool_name == "Bash" and tool_input.command)
tman hook pretooluse < /tmp/kimi-payload.jsonIf the native hook prints a rewrite object and Kimi honours it, drop the adapter and point the hook straight at tman hook pretooluse. If it does not, the adapter and the shims are still doing the work.
Tell the model, in AGENTS.md
Shims and hooks catch the mechanical cases. The remaining gap is the model deciding to do something clever — running the suite through a subshell, or with an inline environment assignment, both of which tman deliberately refuses to rewrite because prefixing a string it did not parse changes what runs. A standing instruction in AGENTS.md closes that gap:
## Running tests and builds
Run every test, build, lint, and typecheck command through tman — never bare.
Prefer the repo-root shims (`./test`, `./lint`) when they exist; they carry
this project's `.tman.kdl` caps. Otherwise prefix explicitly:
tman run -- <command>
Never widen the caps from the command line. Exit 124 is a timeout, 125 a stall,
126 a resource cull — report those, do not retry with looser limits.Verify it is actually on
A supervision integration that silently does nothing is worse than none, because it stops you looking. Two checks, both of which fail visibly:
# 1. the classifier agrees this command is worth supervising
echo '{"tool_name":"Bash","cwd":"'"$PWD"'","tool_input":{"command":"npm test"}}' \
| tman hook pretooluse
# 2. a real run shows up while it is in flight
tman run --name smoke -- sleep 30 &
tman listThe first prints a JSON object naming the rewritten command. Empty output means tman decided to stay out of the way — no .tman.kdl above cwd, a command it does not classify as test or build work, or a shell string it refused to parse. The second must list the run; if tman list is empty, nothing is being supervised.
Caps worth setting for agent-driven work
An agent re-runs the same command far more often than a human does, and it does it while you are not watching. That changes which caps earn their keep:
| cap | why it matters more under an agent |
|---|---|
--name | The agent will start the suite again before the last one finished. A dedup lock refuses the duplicate instead of running two copies against the same port or database. |
--max-parallel 2 | The default, and the right one for most repos. Excess runs queue on a slot file rather than stampeding your cores. |
--max-time | The only cap that bounds a run which is genuinely working but will never finish — a wait on a peer that never answers looks identical to healthy IO. |
--stall 30m | The built-in. A hang backstop, not a runtime budget: it should sit well above the longest quiet stretch your slowest command legitimately has. |
--max-mem | Opt-in, and worth it where a leak mode exists — worker pools, browser fleets, long-lived daemons. Off by default because builds are supposed to be greedy. |
Per-command starting points live in the tuning guides — one page per test and lint tool, with the reasoning behind each number rather than just the number.
Frequently asked
Where does Kimi Code CLI keep its configuration?
`~/.kimi-code/` by default — `config.toml` for agent and runtime settings, `tui.toml` for the terminal UI — relocatable by setting KIMI_CODE_HOME. Workspace-specific settings go in `<project>/.kimi-code/local.toml`.
Can a Kimi PreToolUse hook rewrite the command?
The documentation describes hooks as gating risky tool calls and does not state that arguments can be modified. Treat it as a gate until you have confirmed otherwise on your build, and rely on the PATH shims — which sit below the agent — for supervision that does not depend on the answer.
Other agents
Sources
Vendor documentation this guide is written against — hook surfaces move, so check yours if something below does not match what you see.