Code mode gives the model one extra tool: run_code, which runs a LuaJIT
program that can call Wizard's own tools.
It is off by default.
# ~/.wizard/config.toml
code_mode = trueWIZARD_CODE_MODE=1 turns it on for one process, WIZARD_CODE_MODE=0 turns it
off. Anything else leaves the config alone.
Round trips. A model that wants to read forty files and keep the three that match makes forty tool calls, and the other thirty-seven land in its context on the way past. In code mode it writes one loop and reads three lines.
local hits = {}
for _, path in ipairs({"a.rs", "b.rs", "c.rs"}) do
local text = tool.read_file{path = path}
if text:find("TODO") then hits[#hits + 1] = path end
end
print(table.concat(hits, "\n"))The rule the tool's own description gives the model: use it when three or more calls would otherwise be a fixed sequence, or when the next call's arguments are computable from the previous call's output. Do not use it when you need to read a result before deciding what to do next, because then the round trip is the point.
The interpreter is the one already in the binary — the same LuaJIT that runs
scripted tools under ~/.wizard/tools/. There is nothing to install.
tool.<name>{...}dispatches a Wizard tool.tool.read_file{path="x"}. The name can also be spelledtool["playwright__browser_click"]{ref="e7"}, which is how MCP tools are reached.wizard.call(name, args)is the same thing as a status table ({ok=, content=, status=}) that never raises.wizard.tools()lists what is callable.print(...)is what comes back. Everything else is thrown away.args(an empty table),cwd,wizard.read_file,wizard.write_file,wizard.json_encode,wizard.json_decode,wizard.runtimeandwizard.versionare the same globals a scripted tool has.
wizard.read_file and tool.read_file are not the same call. The wizard.*
pair are raw filesystem helpers that no hook sees and no checkpoint covers,
which is what they have meant in every scripted tool ever written. The
dispatched read is tool.read_file. The namespace is the distinction.
| The tool | In Lua |
|---|---|
| ran and succeeded | returns the result string |
| ran and reported failure | returns nil, message |
| could not be run at all | raises |
The middle row is the one that matters. A failing build is diagnostic signal,
not a malfunction, so it comes back as a value the program can read and act on.
The bottom row means nothing happened on the machine, so nothing downstream
would be computing on real data; wrap it in pcall if you meant to probe.
One interpreter per run_code call, dropped when the call returns. Globals,
functions and loaded data are gone. A second call starts from nothing.
This is a decision, not a limitation waiting to be lifted. A persistent
interpreter would be a Lua heap that /rewind cannot restore, /resume cannot
replay, /fork cannot copy, and compaction cannot summarise — so the model
would believe things were defined that were not, at five separate points, in the
one feature Wizard sells as reversible.
State that has to outlive a program goes in a file, through wizard.write_file
or the memory tool. A file survives compaction, resume, fork, rewind and a
restart.
Success:
run_code ok (3 tool calls, 0.42s compute)
output:
parsed 1841 rows
3 files over the threshold
calls:
1 read_file {"path":"Cargo.toml"} -> ok, 3841 bytes
2 search_files {"pattern":"mlua"} -> ok, 812 bytes
3 edit_file {"path":"src/x.rs"} -> ok
Failure, every kind, the same shape: a header naming the kind, the message, whatever the program printed before it failed, and the ledger of calls that already ran. The ledger is the difference between the model retrying safely and writing the same file twice.
There are seven ways to fail, and they are kept apart on purpose because the right reaction differs:
| Header | What happened | What to do about it |
|---|---|---|
run_code compile: |
the program would not parse, so nothing in it ran | fix the Lua |
run_code error: |
the program raised | read the traceback and the ledger |
run_code denied: |
a tool call inside it was refused | stop trying; the Lua is fine |
run_code time: |
it used its compute budget | narrow the work |
run_code memory: |
it held more than 64 MB | stream instead of accumulating |
run_code calls: |
more than 64 dispatched calls | narrow the loop |
run_code interrupted: |
the user stopped the turn | nothing |
Only compile is treated as a fault by the circuit breakers, because it is the
only one where the call could not be made at all. error is a program's exit 1 and is bounded by the ordinary per-tool backstop instead.
Printing more than the output cap is not a failure. It truncates and spills the rest to a file, exactly like any other tool's output.
| Bound | Value |
|---|---|
| compute | 30 s by default, 120 s maximum, set per call with timeout_secs |
| wall clock | 600 s, never extended |
| memory | 64 MB of Lua heap, checked between instructions |
| printed output | 8 MB held, then further output is dropped |
| dispatched calls | 64 |
The compute budget does not count time spent inside a tool call. A program that runs a two-minute build has not spent two minutes of its own budget, because the work was the build's.
The wall clock is the backstop that makes that safe: it is never extended, so a program whose every call is slow cannot push its deadline forward forever.
The bounds are read between VM instructions, and a bound that fires stops the
program for good: catching it with pcall does not let it carry on, and a
coroutine it created is bounded like the main chunk. Two things follow from
"between instructions", and both are real limits rather than fine print:
- One allocation can pass the memory ceiling.
string.rep('x', 6e8)is a single instruction, so the check has no chance to run in the middle of it. The alternative — handing LuaJIT a failing allocator — crashes the process on some platforms, so it is not used. Build big strings in pieces if you build them at all. - Nothing fires inside a C call. A program parked in
os.execute("sleep 99999")cannot be stopped from inside. The turn is not held hostage by it — the host stops waiting a couple of seconds after the budget and reports what the program printed — but the thread runs until its call returns. The supported way to run a command from a program istool.execute, which has a timeout of its own.
Every tool a program calls goes through the same dispatch pipeline a direct call does:
pre_tool_usehooks can rewrite its arguments or veto it. A veto arrives asrun_code denied:. This coverstool.<name>{...}andwizard.call, which is every dispatched call — it is not a filesystem policy:io.openandwizard.write_fileare raw calls that no hook sees and no checkpoint covers, the same as in any scripted tool.tool.write_fileis the dispatched write.Edit-class calls are snapshotted under the parent's current turn, so/rewindundoes a program's edits.post_tool_usehooks run and their output is appended.
Plan mode refuses run_code outright rather than letting a program start and
hit a wall halfway: the tool is Execute-class, so the plan gate blocks it with
the message that names exit_plan.
A program cannot call run_code, spawn_subagent, evolve, publish,
exit_plan, interview or run_command. Programs do not nest and cannot
delegate.
A program is code the model wrote, running in-process, with your privileges and
the full Lua standard library. os and io are live. That is the same standing
a scripted tool you wrote yourself has, and the same standing the execute tool
already gives the model — a program that can call tool.execute is not made
safer by taking os.execute away from it.
So: what is bounded is time, memory and call count. Capability is not bounded,
with one exception — os.exit is removed, because there is no tool.exit and it
is the only call a program has that ends the host process rather than itself.
And the bound does not hold inside a C call, as above.
See SECURITY.md.
It is also never offered to a model without native tool calling, whatever the config says. Those models get their tool roster through a prompt-based JSON protocol, and a multi-line Lua program inside a JSON string, emitted by hand by a model that already struggles with two-field calls, does not fail loudly — it stalls the turn.