Skip to content

Halluacinate (Lua)

Halluacinate lets you teach FreeChaOS new tricks without recompiling. Drop a Lua script into the right folder and the LLM can call it as a tool.

Create a script at ~/.config/chaos/scripts/project_info.lua:

chaos.tool({
name = "project_info",
description = "Summarize the current session context",
input_schema = {
type = "object",
properties = {}
},
handler = function(_args)
return "cwd=" .. chaos.cwd .. " provider=" .. chaos.provider
end
})

Start a session. The LLM now has a project_info tool it can call.

Halluacinate also owns the TUI status line. ChaOS ships a built-in Lua renderer, and you can override it by registering your own chaos.statusline(...) script. The built-in script loads first, then user scripts, then project scripts - the last registered statusline wins.

Scripts are loaded automatically on session startup from two places:

Layer Path Priority
User ~/.config/chaos/scripts/ Lower
Project .chaos/scripts/ (relative to working directory) Higher

Project scripts load after user scripts and can override tools with the same name. Scripts load in alphabetical order within each layer. Only .lua files are loaded; WASM support is planned.

Read-only information about the current session:

chaos.session_id -- unique conversation ID
chaos.cwd -- working directory
chaos.provider -- provider name (e.g. "anthropic", "openai")
chaos.tool({
name = "tool_name",
description = "What it does - the LLM reads this",
input_schema = {
type = "object",
properties = {
param = { type = "string", description = "..." }
},
required = { "param" }
},
handler = function(args)
-- args is a Lua table with parsed parameters
-- return a string with the result
return "done"
end
})

The input_schema follows JSON Schema. The LLM uses it to understand what parameters to pass.

React to events during the session:

chaos.on("event_name", function(payload)
-- payload is a Lua table
chaos.log.info("event fired")
end)
chaos.log.info("message")
chaos.log.warn("message")
chaos.log.debug("message")

Logs go to the FreeChaOS log file (~/.chaos/log/).

Override the default TUI status line:

chaos.statusline(function(ctx)
return {
{ text = ctx.model, bold = true },
{ text = " · " },
{ text = tostring(ctx.context.remaining_pct) .. "% left" },
{ text = " · " },
{ text = ctx.cwd_display or ctx.cwd },
}
end)

Place a statusline script in either script layer (~/.config/chaos/scripts/statusline.lua or .chaos/scripts/statusline.lua); the project copy overrides the user copy because project scripts load later.

The built-in renderer is a Doom-style HUD. By default it shows:

  • HUD so it is obviously not the old Rust footer
  • HP, where remaining context is treated as health and color-coded
  • CRIT when context health gets dangerously low
  • WPN, showing the active model and reasoning effort
  • MAP, using the current branch, project root, or directory
  • ARM, showing sandbox and approval posture
  • CTX, showing effective context load above the fixed prompt baseline
  • AMMO, showing last-response output with optional context growth
  • DIR, when it adds extra path context beyond MAP

The ctx table includes ctx.model, ctx.reasoning_effort, ctx.provider, ctx.branch, ctx.cwd, ctx.cwd_display, ctx.project_root, ctx.approval, ctx.sandbox, ctx.version, ctx.session_id, ctx.context.{remaining_pct,used_pct,window_size}, ctx.tokens.* (including context_effective, last_output, and the legacy cumulative used/input/output counters), ctx.five_hour, and ctx.weekly. For statusline UX, prefer the explicit fields like ctx.tokens.context_effective and ctx.tokens.last_output over the legacy counters.

Scripts run in a sandbox. Available standard libraries:

  • string, table, math, utf8, coroutine
  • assert, error, ipairs, next, pairs, pcall, print, select, tonumber, tostring, type, unpack, xpcall

Blocked:

  • os, io, debug, package
  • require, load, loadfile, dofile, collectgarbage

Each script gets its own isolated environment - scripts cannot interfere with each other.

Limit Value
Memory per script 8 MiB
Execution time per tool call 10 seconds
Batch load timeout 30 seconds

If a script exceeds these limits, the call fails and FreeChaOS continues without it.

chaos.tool({
name = "reverse_string",
description = "Reverse a string",
input_schema = {
type = "object",
properties = {
text = { type = "string", description = "Text to reverse" }
},
required = { "text" }
},
handler = function(args)
return string.reverse(args.text)
end
})
chaos.on("tool_call", function(payload)
chaos.log.info("Tool called: " .. tostring(payload.name))
end)

Tool doesn’t appear - Check the file is in ~/.config/chaos/scripts/ or .chaos/scripts/ and has a .lua extension. Check logs for load errors.

Script fails silently - Look at ~/.chaos/log/ for error details. Common causes: syntax errors, exceeding memory or time limits.

Tool returns wrong result - The handler must return a string. If you return a table or nil, the result will be empty.