Skip to content

Automation reference

A way to run something automatically when a tab changes state.

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.


Put a file named after an event into the automation folder and it runs at that moment. Only add the ones you need.

File nameWhen it runs
on_start.luaRight after the tab starts
on_done.luaWhen the AI has finished answering
on_question.luaWhen the AI asks something or offers choices
on_exit.luaWhen the session ends (including disconnects and crashes)
on_busy.luaWhen an answer starts (advanced)
_shared.luaLoaded 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.lua
shikisha.send_to_tab(2, "Please review this code:\n" .. tab.output)

tab is available in every event.

VariableContents
tab.indexTab number (starting at 1)
tab.nameTab name
tab.outputThe latest response text (no earlier history)
tab.state"BUSY" / "DONE" / "QUESTION" / "WAIT" / "EXIT"
tab.profileName of the profile in effect
tab.chain_depthHow many times this was handed on automatically. 0 means a human started it
tab.lockedWhether input is locked

Only on_question.lua gets a second variable, screen, holding the whole screen text.


Numbers change when you reorder tabs, so pointing by name is the default.

shikisha.send_to_tab("Review", "please review") -- recommended
shikisha.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
CommandDescription
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.


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 end
shikisha.send(tab, "cd /srv/myproj\r")
shikisha.wait(tab, "%$ $", 5000)
shikisha.send(tab, "claude --continue\r") -- pick the previous conversation back up

To 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 session
end

Approve 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 human
end
return "1\r" -- pick choice 1

Bounce 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 directly
if tab.chain_depth == 0 then return end
local rounds = shikisha.get_var("rounds") or 0
if tab.output:match("LGTM") or rounds >= 5 then
shikisha.notify("slack", "Review finished (" .. rounds .. " rounds)")
return -- doing nothing = the loop ends
end
shikisha.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) + 1
if n > 5 then
shikisha.notify("slack", tab.name .. " keeps dying")
return
end
shikisha.set_var("retry", n)
shikisha.sleep(2000)
shikisha.restart(tab) -- after the restart, on_start runs again

The screen and the other tabs keep running while you sleep. You choose the interval.

-- while it is working, record every 30 seconds
while shikisha.state(tab) == "BUSY" do
shikisha.sleep(30000)
shikisha.log(tab.name .. " is still working")
end

tab.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)

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 stopCtrl+B x halts all automation at once, Ctrl+B a toggles 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":"..."}')
CommandDescription
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.


  • Join strings with .. (not +)
  • tab.output holds 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 return and it stops right there
  • If you get lost, sprinkle shikisha.log() and read logs/hooks.log