Automation reference
It is written in Lua, a small scripting language, but a few lines is all you need.
This document is both an explanation for humans and the specification that the “let an AI write it” button in the settings screen hands to the AI.
Translations live next to this file as docs/AUTOMATION.<code>.md (for example
docs/AUTOMATION.ja.md) and are picked automatically from your language setting.
1. When it runs (events)
Section titled “1. When it runs (events)”Put a file named after an event into the automation folder and it runs at that moment. Only add the ones you need.
| File name | When it runs |
|---|---|
on_start.lua | Right after the tab starts |
on_done.lua | When the AI has finished answering |
on_question.lua | When the AI asks something or offers choices |
on_exit.lua | When the session ends (including disconnects and crashes) |
on_busy.lua | When an answer starts (advanced) |
_shared.lua | Loaded before all of the above. Put shared helper functions here |
Write only the body of the work in the file. No function ... end wrapper.
-- example of on_done.luashikisha.send_to_tab(2, "Please review this code:\n" .. tab.output)2. Variables you can use
Section titled “2. Variables you can use”tab is available in every event.
| Variable | Contents |
|---|---|
tab.index | Tab number (starting at 1) |
tab.name | Tab name |
tab.output | The latest response text (no earlier history) |
tab.state | "BUSY" / "DONE" / "QUESTION" / "WAIT" / "EXIT" |
tab.profile | Name of the profile in effect |
tab.chain_depth | How many times this was handed on automatically. 0 means a human started it |
tab.locked | Whether input is locked |
Only on_question.lua gets a second variable, screen, holding the whole screen text.
3. Commands you can use
Section titled “3. Commands you can use”How to point at a tab
Section titled “How to point at a tab”Numbers change when you reorder tabs, so pointing by name is the default.
shikisha.send_to_tab("Review", "please review") -- recommendedshikisha.send_to_tab(2, "please review") -- numbers work too (they shift on reorder)If you plan to rename a tab, or if several tabs share a name, give it an
“automation name” (id) in the settings. With an id you can rename the tab freely
and the automation keeps working.
{ "name": "Review", "id": "reviewer", "command": "codex" }shikisha.send_to_tab("reviewer", "please review") -- survives renaming| Command | Description |
|---|---|
shikisha.send_to_tab(tab, "text") | Send to another tab and let it run (automatic chain +1) |
shikisha.send(tab, "text") | Send keystrokes to that tab (newline is \r) |
shikisha.wait(tab, "pattern", ms) | Wait until the text appears on screen; true if it did |
shikisha.sleep(ms) | Wait (other tabs keep running while you wait) |
shikisha.state(tab) | Read the state right now (use this as a loop condition) |
shikisha.wait_state(tab, "DONE", ms) | Wait until it reaches that state |
shikisha.notify("target", "text") | Notify Slack / Telegram (only configured targets) |
shikisha.restart(tab) | Restart that tab |
shikisha.log("text") | Record in logs/hooks.log |
shikisha.get_var("key") / shikisha.set_var("key", value) | Remembered variables, shared inside the workspace |
If on_question.lua returns a string, that string is sent automatically.
Returning nil (or nothing) leaves the decision to the human.
4. Common examples
Section titled “4. Common examples”Resume yesterday’s work just by starting (on_start.lua)
Section titled “Resume yesterday’s work just by starting (on_start.lua)”if not shikisha.wait(tab, "%$ $", 15000) then return endshikisha.send(tab, "cd /srv/myproj\r")shikisha.wait(tab, "%$ $", 5000)shikisha.send(tab, "claude --continue\r") -- pick the previous conversation back upTo choose which past conversation to resume, use claude --resume. It shows a list,
and picking from that list can be automated too:
shikisha.send(tab, "claude --resume\r")if shikisha.wait(tab, "[Ss]elect", 8000) then shikisha.send(tab, "\r") -- choose the topmost sessionendApprove automatically, but hand risky questions to a human (on_question.lua)
Section titled “Approve automatically, but hand risky questions to a human (on_question.lua)”if screen:match("delete") or screen:match("rm %-rf") then return nil -- leave it to the humanendreturn "1\r" -- pick choice 1Bounce a review between A and B, stopping after 5 rounds (on_done.lua)
Section titled “Bounce a review between A and B, stopping after 5 rounds (on_done.lua)”-- do nothing when a human gave the instruction directlyif tab.chain_depth == 0 then return end
local rounds = shikisha.get_var("rounds") or 0if tab.output:match("LGTM") or rounds >= 5 then shikisha.notify("slack", "Review finished (" .. rounds .. " rounds)") return -- doing nothing = the loop endsendshikisha.set_var("rounds", rounds + 1)shikisha.send_to_tab(1, "Please fix these points:\n" .. tab.output)Reconnect automatically after a disconnect (on_exit.lua)
Section titled “Reconnect automatically after a disconnect (on_exit.lua)”local n = (shikisha.get_var("retry") or 0) + 1if n > 5 then shikisha.notify("slack", tab.name .. " keeps dying") returnendshikisha.set_var("retry", n)shikisha.sleep(2000)shikisha.restart(tab) -- after the restart, on_start runs againCheck in periodically (on_busy.lua)
Section titled “Check in periodically (on_busy.lua)”The screen and the other tabs keep running while you sleep. You choose the interval.
-- while it is working, record every 30 secondswhile shikisha.state(tab) == "BUSY" do shikisha.sleep(30000) shikisha.log(tab.name .. " is still working")endtab.state is the state at the moment you were called, so use
shikisha.state(tab) (the state right now) as the loop condition.
When a tab exits or restarts, waiting loops are discarded automatically.
Just notify Slack when it is done (on_done.lua)
Section titled “Just notify Slack when it is done (on_done.lua)”shikisha.notify("slack", tab.name .. " finished:\n" .. tab.output)5. Safety mechanisms
Section titled “5. Safety mechanisms”Several brakes keep automation from running away.
- Automatic chain limit … the number of consecutive automatic hand-offs between AIs is counted and stops at the limit (10 by default). Typing something yourself resets it to 0
- Manual work wins … nothing is sent automatically for 5 seconds after you touch a tab
- Emergency stop …
Ctrl+B xhalts all automation at once,Ctrl+B atoggles it - Input lock … put 🔒 on the middle tabs so nobody instructs them by mistake
- Sandbox … automation can neither touch files nor reach the internet by default. Notifications only go to the Slack / Telegram targets you registered
6. Files and network access (advanced, off by default)
Section titled “6. Files and network access (advanced, off by default)”When you really need it, register a “gateway” in config.json and it becomes available.
It cannot be edited from the settings screen (the impact is large, so it is only for
people who edit the file directly).
"capabilities": { "files": { "reports": { "dir": "reports", "read": true, "write": true } }, "http": { "github-issue": { "url": "https://api.github.com/repos/me/proj/issues", "method": "POST", "auth_from_secrets": "github_token" } }}shikisha.write_file("reports", "review.md", tab.output)local prev = shikisha.read_file("reports", "review.md")shikisha.http("github-issue", '{"title":"Findings","body":"..."}')| Command | Description |
|---|---|
shikisha.write_file(gateway, filename, text) | Write into a registered folder |
shikisha.read_file(gateway, filename) | Read from a registered folder |
shikisha.http(gateway, body) | Send to a registered URL (the app adds the credentials) |
Why this is safe: scripts cannot assemble paths or URLs — they can only call registered
names. Auth tokens are invisible to scripts; the app attaches them.
config.json, secrets.json, .env and .lua files can never be read or written, even
when they sit inside an allowed folder.
If you need more freedom, raw paths and raw URLs are available too (empty by default = everything denied):
"capabilities": { "allow_dirs": ["reports"], "allow_hosts": ["api.example.com"]}shikisha.write_path("reports/a.md", "text")shikisha.http_raw("https://api.example.com/hook", '{"x":1}')Hosts are matched exactly and only https is allowed
(tricks like api.example.com.evil.com are rejected).
Every file and network operation is recorded in logs/hooks.log.
7. Tips for writing it
Section titled “7. Tips for writing it”- Join strings with
..(not+) tab.outputholds only the latest response, never the earlier conversation- Lua patterns are their own thing:
%d(digit),%s(space),.-(shortest match). Write%d, not\d - When you want to do nothing, write
returnand it stops right there - If you get lost, sprinkle
shikisha.log()and readlogs/hooks.log