Skip to content

Lua scripting

Shell syntax gets awkward for multi-step transforms, structured-data manipulation, or logic that would sprawl across a chain of pipes. Reach for lua when it does. The shell runs a sandboxed Lua 5.4 interpreter with the virtual filesystem wired into its io and os libraries, so a script reads and writes the same sandboxed filesystem your commands see, mediated by the same Kernel boundary.

lua is a command like any other in the shell. Run a script from a file in the VFS, or pipe one in:

Terminal window
lua transform.lua

The working directory, environment, and VFS carry over from the surrounding shell, and a script’s file access goes through the Kernel just as a command’s does.

The interpreter is Lua 5.4 with a restricted global environment:

  • io and os are backed by the VFS, so file access stays inside the sandbox.
  • Metatables are disabled: setmetatable and getmetatable are removed.
  • os.date and the debug library are unavailable.
  • The escape hatches that would compile arbitrary chunks or reach around the sandbox are removed, including load, collectgarbage, the raw accessors (rawget, rawset, rawequal, rawlen), and warn.

Under a bare strands-shell -c, a Lua script has no wall-clock timeout. Set a timeout on the shell, or pass a per-request timeout_ms through the MCP shell tool, to bound a long-running script.

A shell config can declare other MCP servers under [[mcp]] entries. Each one becomes a Lua module inside the shell, so a script can call your existing MCP tools through the same sandboxed shell, with each tool’s response arriving as an ordinary Lua table.

Declare the servers in your TOML config, then require a server by name from lua. A server named my-tools is exposed as the module my_tools (hyphens become underscores):

local tools = require("my_tools")
local result = tools.search({ query = "deny by default" })
print(result)

Nested MCP servers are a native-target feature. They aren’t available under the WASM build, which has no MCP client and no --config flag.