CLM-TOOL(5) - File Formats Manual
NAME
clm-tool - clm Lua plugin and tool API
DESCRIPTION
clm(1) loads every *.lua file in ~/.config/clm/plugins/ (or a directory given with --plugins, or an agent-specific ~/.config/clm/agents/agent/ directory) as a plugin. Each plugin file gets its own sandboxed lua(1) state and is expected to call clm.tool_register at least once, at load time, to register the tools it provides.
Sandbox
Only the string, table, math, and utf8 standard libraries are loaded; os and io are never loaded at all, and dofile, loadfile, load, require, and collectgarbage are removed from _G after loading the safe libraries. There is no filesystem or process access except through clm.read_file and clm.write_file (below), no dynamic code loading, and no way to reach any other plugin's state.
A plugin has an 8 MiB private memory cap, a 500 ms deadline to finish executing its top-level (load-time) code, and a separate, per-tool-call CPU deadline (the timeout_ms field of clm.tool_register, below; 30 s if unset), checked every 10000 VM instructions so a tight infinite loop cannot dodge it. Exceeding either deadline aborts execution of that plugin or call; a load-time failure is logged and that one plugin's tools are simply not registered; the rest of the plugin directory still loads.
Registering a tool
clm.tool_register(name, {
description = "...", -- optional
params_schema = { ... }, -- optional, a JSON-Schema-shaped table
invoke = function(args, ctx)
...
ctx:complete(result) -- or ctx:fail(message)
end,
timeout_ms = 30000, -- optional, overrides the default
no_prompt = false, -- optional, skip the permission prompt
hidden = false, -- optional, hide from the model's schema
})
name must be unique across every plugin and builtin tool loaded into the agent. invoke is called once per tool invocation with args (the decoded JSON arguments the model supplied, following params_schema) and ctx, described below. exactly one of ctx:complete or ctx:fail must be called, from invoke or from a child started with clm.spawn() (see HTTP requests below); calling either a second time is an error.
By default a tool is gated behind an interactive allow/deny prompt the first time it is called in a session (see clm_agent(3)'s on_permission callback); set no_prompt = true for a tool with no side effects worth confirming. hidden = true omits the tool from the schema advertised to the model entirely, for a tool meant to be invoked only by other plugin code, not by the model itself.
The ctx object
ctx:complete(result)
Ends the invocation successfully. result is a string; the model sees it as the tool's output.
ctx:fail(message)
Ends the invocation with a failure the model sees as "[tool failed: X]" (where 'X' is message).
ctx:args_raw()
Returns the raw, undecoded JSON arguments string ("{}" if the model supplied none) . Most plugins use the decoded args table passed to invoke instead; this is for a plugin that wants to reparse or forward the arguments verbatim.
ctx:log(message)
Writes
message
to the debug log (see
CLM_DEBUG_LOG
in
clm(1)).
A no-op, at negligible cost, when that variable is unset.
HTTP requests
local resp, err = http.get(url)
local resp, err = http.get(url, {["Authorization"] = "Bearer ..."})
local resp, err = http.post(url, body)
local resp, err = http.post(url, body, {["Content-Type"] = "text/plain"})
both yield the calling coroutine and resume it once the request completes, so invoke does not block the agent's event loop while a request is in flight. they may be called from the main tool coroutine or from a child started with clm.spawn(), but not at plugin load time. headers is an optional table of header-name to header-value strings; http.post() seeds "Content-Type: application/json" by default unless headers overrides it.
On success,
resp
is a table with
resp.status
(the HTTP status code) and
resp.body
(the response body as a string);
err
is
nil.
On failure,
resp
is
nil
and
err
is a string describing what went wrong.
Up to 8 requests per plugin may be in flight concurrently, and a
single tool call may make at most 128 requests in total; exceeding
either limit fails the request rather than queuing it.
Other clm module functions
clm.read_file(path)
clm.write_file(path, content)
Read or write a file, without the sandbox's normal lack of filesystem access. Available at any point in a plugin, not just from within invoke.
clm.sleep(ms)
yield the calling coroutine for ms milliseconds without blocking the event loop. same coroutine restriction as HTTP requests above.
clm.spawn(function)
create and return a task handle for function. the function is scheduled for a later event-loop iteration and does not execute before clm.spawn() returns. the task belongs to the current tool invocation and may call http.get(), http.post(), and clm.sleep(). only the main tool coroutine may spawn tasks; a task may not spawn another task. values returned by function become the task result.
task:wait()
wait until
task
succeeds, fails, or is cancelled, then return
true.
waiting does not read or interpret the task result.
task:result()
return
nil
if
task
is pending.
a terminal task returns a stable outcome table.
a successful outcome has
ok
set to
true
and one normalized
value
field: one returned value is stored directly, multiple returned values are
stored in an array, and zero returned values omit
value.
a failed outcome has
ok
set to
false
and an
error
string.
a cancelled outcome also has
cancelled
set to
true.
reading a failed outcome marks its error as observed.
task:cancel(reason)
cancel a pending task and its asynchronous operation.
reason
is optional and defaults to
"task cancelled".
return
true
when the task was cancelled or was already cancelled, and
false
when it had already succeeded or failed.
clm.wait_any(tasks, timeout_ms)
wait until any task in the array table
tasks
is terminal and return its one-based input index.
timeout_ms
is optional; expiration returns
nil
plus
"timeout"
without cancelling any task.
all tasks must be unique handles from the current invocation, and at most 128
tasks may be waited on at once.
clm.await_all(tasks)
wait for every task and return an ordered array of outcome tables as described under task:result(). task failure does not cancel or otherwise affect another task.
clm.try_all(tasks)
wait for tasks until all succeed or one fails.
on success, return normalized task values in input order.
on failure, cancel every unfinished task in this set and return
nil
plus the observed error string.
only the main tool coroutine may wait, read results, cancel tasks, or complete or fail the tool. returning from the main invoke function with live tasks cancels those tasks and fails the tool. successfully completing a tool with live tasks or unobserved task errors also fails the tool; clm.spawn() does not create detached background work. the invocation remains alive while its main coroutine or child tasks are suspended; tool cancellation, timeout, and plugin teardown cancel the whole family and its pending asynchronous work.
The json module
local text = json.encode(value)
local value, err = json.decode(text)
A global
json
table over
json-c(3).
json.null
is a sentinel light userdata value representing JSON
null,
distinct from Lua
nil.
json.decode
rejects input larger than 2 MiB.
Per-tool configuration
If the plugin's file or tool name appears as a key under
tools
in
config.lua
(see
clm-config(5)),
that subtable is available inside the plugin as
clm.config.
A plugin with no matching
tools
entry sees
clm.config
as an empty table, not
nil.
EXAMPLES
-- ~/.config/clm/plugins/hello.lua
clm.tool_register("hello", {
description = "Say hello to someone",
params_schema = {
type = "object",
properties = {
name = { type = "string", description = "who to greet" },
},
required = { "name" },
},
invoke = function(args, ctx)
ctx:complete("Hello, " .. args.name .. "!")
end,
})
local tasks = {
clm.spawn(function()
local resp, err = http.get(url_a)
if resp == nil then error(err) end
return resp.body
end),
clm.spawn(function()
local resp, err = http.get(url_b)
if resp == nil then error(err) end
return resp.body
end),
}
local outcomes = clm.await_all(tasks)
for _, outcome in ipairs(outcomes) do
if not outcome.ok then
ctx:fail(outcome.error)
return
end
end
ctx:complete(outcomes[1].value .. outcomes[2].value)
SEE ALSO
clm(1), clm_agent(3), clm-config(5)
clm - July 6, 2026