In code mode the model gets one more tool, codemode. Its input is a
JavaScript program. The program calls Kit’s other tools, combines and filters
their results, and returns a small answer. Only the program’s output goes
back to the model — the results of the tool calls inside the program do not.
Use it to:
Code mode is on by default. Tool exposure stays direct: the model can
call tools directly or through a script, including MCP tools.
kit --codemode=false
Or in .kit.yml:
codemode:
enabled: false
--no-core-tools disables code mode along with the other core tools.
An explicit --codemode enables it with --no-core-tools, so scripts can
call MCP tools. MCP tools remain directly available unless you change their
exposure. Tool include and exclude lists apply to code mode too; an explicit
--codemode adds it back. An explicit disable takes precedence over naming
codemode in the tool list.
const files = (await tools.ls({})).split("\n").filter(f => f.endsWith(".go"))
const contents = await Promise.all(files.map(f => tools.read({ path: f })))
return files.map((file, i) => ({ file, todo: (contents[i].match(/TODO: (.*)/) || [])[1] }))
The model receives:
Script completed in 1ms.
Tool calls: 7 — ls ×1, read ×6
Output:
[ { "file": "f1.go", "todo": "item 1" }, ... ]
async function. Use await, and use return to
send the result. Strings go back as-is; other values go back as JSON.tools.<name>(args). MCP tools are grouped by server:
the tool github__list_issues is tools.github.list_issues(args).JSON.parse when a tool returns JSON.Error whose name is "ToolError", with the
properties tool and output. Catch it with try/catch, or use
Promise.allSettled.| Helper | Purpose |
|---|---|
text(value) |
Add to the output |
console.log/info/warn/error(...) |
Add log lines (shown to the model and streamed to the TUI) |
call(name, args) |
Call a tool by its full name, e.g. call("github__list_issues", {...}) |
searchTools(query, {limit, namespace}) |
Find tools by keyword |
describeTool(name) |
Show a tool’s full signature and input schema |
ALL_TOOLS |
Every tool a script can call |
store(key, value) / load(key) |
Keep JSON values across scripts for the life of the Kit instance. store(key, undefined) deletes |
sleep(ms) |
Wait |
Calls made inside a script go through the same tool wrappers as direct calls.
Extension OnToolCall / OnToolResult handlers and SDK
OnBeforeToolCall / OnAfterToolResult hooks fire for each one, so a hook
that blocks a tool blocks it inside scripts too. The codemode tool itself
cannot be called from a script.
By default the model still sees every tool directly, and scripts can call them too. Large MCP servers can cost thousands of tokens of tool schemas on every request. Exposure rules move tools out of the model’s tool list and into the code mode catalog:
| Exposure | Model sees it as a tool | Scripts can call it | Listed in the code mode catalog |
|---|---|---|---|
direct (default) |
yes | yes | short form |
codemode |
no | yes | full signature |
deferred |
no | yes | no — found with searchTools() |
model-only |
yes | no | no |
codemode:
enabled: true
mcp-exposure: codemode # default for every MCP tool
exposure: # glob rules; the most specific pattern wins
"github__*": codemode
"github__create_issue": direct
"linear__*": deferred
subagent: model-only
Exact names win over globs, and longer patterns win over shorter ones. Matching ignores case.
The catalog in the tool description is limited by catalog-budget. Namespaces
share the budget round-robin, so one big server cannot crowd out the others.
Tools left out are still callable; the description tells the model to use
searchTools().
| Setting | Default | Meaning |
|---|---|---|
timeout |
30 |
Seconds per script |
max-tool-calls |
50 |
Tool calls per script |
max-concurrency |
8 |
Parallel tool calls per script |
max-output-bytes |
100000 |
Result size the model receives. The full output goes to a temporary file, and the result gives its path |
memory-limit-mb |
256 |
Approximate heap growth limit; -1 disables it |
catalog-budget |
12000 |
Bytes of tool list in the tool description |
A script that passes a limit stops with a typed error the model can act on:
TimeoutExceeded, ToolCallLimitExceeded, MemoryLimitExceeded. Other
kinds are ParseError, UnknownTool (with suggestions), InvalidToolInput
(a required argument is missing), ToolFailure, ExecutionFailure,
UnsettledPromise and Cancelled. Errors carry the line and column in the
submitted code.
Press Esc twice within two seconds to stop a running script. The first press arms cancellation; the second cancels the step and its tool calls in flight. Whether a call stops immediately also depends on the tool honoring its context.
::: info The JavaScript engine has no per-script heap limit. The memory limit is a watchdog on process heap growth while the script runs. It stops runaway scripts; it is not a precise byte budget. :::
While a script runs, the transcript streams its call log:
· Running script
▸ ls
✓ ls (45µs)
▸ read {"path":"f1.go"}
✓ read (210µs)
When it finishes, the block shows the script with line numbers and syntax highlighting, followed by the result.
host, err := kit.New(ctx, &kit.Options{
CodeMode: &kit.CodeModeOptions{
Enabled: true,
MCPExposure: kit.CodeModeExposureScriptOnly,
Exposure: map[string]string{"subagent": kit.CodeModeExposureModelOnly},
Timeout: time.Minute,
},
})
Code mode is enabled by default in the SDK. Set
CodeMode: &kit.CodeModeOptions{Disabled: true} to disable it. Disabled
takes precedence over Enabled and configuration. Other zero fields fall
back to the codemode config section. To use code mode with
a custom tool set, add kit.NewCodeModeTool(opts) to Options.Tools or
Options.ExtraTools; Kit gives it the live tool set.
Each code mode ToolResultEvent carries Metadata.CodeMode, with the nested
calls (name, arguments, status, duration, error), the wall time, the error
kind, and the path of the full output file when the result was truncated.