Snail OS · Lua
An app is a file on the card.
Put a .lua file in /apps on the SD card and the launcher lists it with its own name and icon. Delete it and it is gone. No build, no cable, no reflash.
--!name Hello
--!icon book
local n = 0
function start()
snail.hint("OK counts BACK menu")
end
function key(k)
if k == "ok" then n = n + 1 end
end
function draw()
snail.center(true)
snail.title("Hello")
snail.gap()
snail.text("Pressed " .. n .. " times.")
end
Three functions§
An app defines start(), which runs once when the app is opened, key(k), which runs on a button press, and draw(), which runs whenever the screen needs painting. Only draw() is required. The keys are up, down, left, right, ok and top; BACK never reaches the app, because it always leaves.
draw() must be a pure function of the app's state. It can run at any moment, and more than once for a single press. Decide things in key() and paint in draw().
Most apps are written by telling an agent§
Describe the app to Claude Code or Codex and hand it this platform: snailos.org/llms.txt is the whole reference in one plain-text fetch, and /dev/SKILL.md is the same thing as an installable skill. The device ships with a linter that plays the app against a stand-in for the device using the firmware's own copy of Lua, so the agent can grade its work before the file reaches the card. Tell your agent has the exact thing to paste. The pages below are the same material, for reading.
Why this can exist at all§
The X4's ESP32-C3 executes from flash through the MMU's instruction cache. The SD card sits behind SPI and is not in the address space, so no instruction can ever be fetched from it. That is why a compiled app must go into the firmware image; the restriction applies only to machine code.
A book is not machine code either, and the Reader opens books off the card all day. A card app is the same trick. The file on the card is source, and the interpreter that runs it is compiled into the firmware. The launcher scans /apps at boot and lists whatever the card carries.
There is a ceiling on how many it lists, and tools/sim_check.sh counts the .lua files against the compile-time constant, so a catalogue that outgrows it fails on a laptop instead of on a device.
Why Lua§
Two languages were written for this job before Lua was tried: a document format with no loops or assignment, and a bounded interpreter with literal loop counts and a call graph that could not recurse. Both existed because a real language was assumed not to fit on a chip with 64 KB of usable heap. That assumption was measured against the real firmware on 9 August 2026, one #include apart.
| Measurement | Lua | The interpreter written to avoid it |
|---|---|---|
| Flash | +114 KB | — |
| Peak RAM, 2048 at 400 moves | 23–31 KB | 43,472 B, whatever the game is |
| Largest single allocation | 1.5–2 KB | 10,240 B |
| Static RAM | +4.4 KB | — |
Lua ran the same game in half the RAM and asked the allocator for a block five times smaller. On this device contiguity fails before capacity, so the largest-allocation row is the one that decides whether an app runs at all. The whole argument for hand-rolling a language was memory. Both engines were deleted the same day, before either shipped.
The Store§
The device's Store app fetches snailos.org/apps/index.json and installs any listed app straight onto the card. The same files are on Get the apps, with their sources readable in the browser. An app you write is a text file; send it to someone and they can run it.
This section§
- Tell your agent — the paste, the skill, and the write-lint-fix loop.
- Quickstart — from nothing to an app on your device.
- Reference — every call on the snail table, and every key.
- Fetching — snail.fetch, flat data, and connecting an account.
- Limits — the numbers, and the reason for each one.
- One app, walked — billy.lua read end to end.
- The Store — one JSON file is the catalogue, and its order is the ranking.
- Compiled apps — what a card app cannot do, and what to do then.
- Get the apps — the published apps, with source.