Hooks & Events
Hooks let a script inspect or change input and output, and respond to events
such as connecting or reloading. Rune also registers named hooks for triggers,
local echo styling, and history expansion; /hooks lists them. After all input
hooks finish, Rune handles the resulting text as normal command input or
verbatim input.
rune.hooks.on(event, handler, opts?)Reach for a trigger when you want to match particular text. Reach for a hook when you want every line regardless of content, to timestamp or log or mirror it, or when what you care about isn’t text at all, such as connecting, reloading, or a GMCP message.
Unlike triggers, aliases, and timers, a hook handler is always a Lua function; there is no string form. What the function receives and what its return value means depend on the event.
Data-flow events
Section titled “Data-flow events”Handlers run in priority order:
| Event | Handler receives | Notes |
|---|---|---|
input |
one physical line, context | Before echo and history: a string, including "", replaces the text for later handlers, false consumes that line, and any other value leaves it unchanged. context.mode is read-only and always "command" or "verbatim". |
output |
a line object | Once per complete line. false gags, a string rewrites. The core handler runs output triggers at priority 100. |
prompt |
a line object, confirmed |
Cumulative partial-line observation (false) or GA/EOR-confirmed prompt (true). The core runs prompt triggers at priority 100. |
echo |
one display-safe line of the final input | Like output but a plain string. Terminal controls have already been made visible. The > prefix is the core handler; replace it if you like. |
For every data-flow event, rewrites chain: a handler returning a string
replaces the text for subsequent handlers, nil or another value passes the
current text through, and false stops the chain. For input, it consumes only
that line, before local echo or dispatch; later lines still run.
The same input hooks receive one physical line in both Command and Verbatim
mode. Each line completes hooks, echo, and dispatch before the next begins.
A replacement must also be one line: LF and CR are rejected before later
handlers see the replacement. Tabs are allowed. Final Command replacements
must exclude invalid UTF-8 and terminal controls. Invalid rewrites
skip the affected line. Scripts intentionally sending several lines can call
rune.send or rune.send_raw, then return false.
Rune saves surviving lines together as one history entry after processing. Input hooks, echo hooks, and command handlers do not yet see that entry. Explicit script additions appear immediately in both history reads and later history expansion.
Compatibility: Input hooks previously received the whole draft in Verbatim
mode and could return multiline replacements. Both modes now call hooks per
line. History is also recorded later, after processing. Existing handlers that
accept only text continue to work because Lua ignores the context argument.
-- Timestamp every line, after triggers have runrune.hooks.on("output", function(line) return rune.style.gray(os.date("[%H:%M] ")) .. line:raw()end, { priority = 150 })-- A panic key: swallow all input while activelocal locked = falserune.hooks.on("input", function(text, context) if locked and text ~= "/unlock" then return false end if context.mode == "verbatim" then rune.echo("Verbatim input bypasses aliases and slash commands") endend, { priority = 1 })-- Rewrites chain: later input hooks, echo, history, and command processing-- all see "look".rune.hooks.on("input", function(text) if text == "l" then return "look" endend, { priority = 25 })-- Handle one command by sending two physical lines yourself.rune.hooks.on("input", function(text) if text == "combo" then rune.send_raw("kick\npunch") return false endend, { priority = 25 })All input handlers run in priority order, with lower numbers first. If none
returns false, Rune processes the final text after the last handler finishes.
In command mode that means slash commands, command separators, #N repeats,
and aliases. Verbatim input sends each line without any of that command
processing. All input handlers finish before Rune echoes or dispatches the result.
History expansion runs once before all input hooks. With the
default history character, it expands interactive command components such as
!, !!, and !k; see Input & History for the
full behavior. If your game uses bang commands, choose another character or
disable expansion with rune.config.set("history_character", "").
Prompt semantics
Section titled “Prompt semantics”The prompt hook drives the prompt overlay; its name does not mean every value
is a prompt. A partial line may be Username: or the first half of a longer
line, and rune cannot tell which until the server finishes it. So the same text
can reach you more than once: the observation repeats as the line grows, and if
GA/EOR arrives separately it runs again with confirmed changing from false
to true. Write handlers that replace state rather than accumulate it:
-- Replace state on each observation; partial lines may repeat.local current_prompt = ""rune.hooks.on("prompt", function(line) current_prompt = line:clean()end)If the text turns out to be an ordinary line, it arrives once more through
output as a complete line, and that complete line replaces its partial line.
An empty prompt boundary does not fire prompt. Rune uses no timer or prompt
pattern to infer one. A confirmed prompt is committed before later server text
and closes open spans before prompt handlers run, so span actions have
already fired by the time your handler sees the prompt.
Every command you submit finishes the partial line before input hooks and any echo, history, or command processing. This also applies to local slash commands, canceled commands, and commands that cannot be sent because you are disconnected. Commands sent by aliases, triggers, timers, and other scripts finish the partial line only after the connection accepts them. Failed sends and protocol traffic such as GMCP do not.
Notification events
Section titled “Notification events”All handlers run and returns are ignored. The ones you’ll reach for
most: connected (address), disconnected, and ready (after scripts load
during startup and reload). The full catalog — connection lifecycle, reload,
errors, GMCP — is in the
rune.hooks reference.
rune.hooks.on("connected", function(addr) rune.send("Ragnar") -- or see the auto-login cookbook recipeend)Priorities in practice
Section titled “Priorities in practice”The core’s output, prompt, and echo handlers sit at priority 100. Run before
them to see the pre-trigger/pre-style value, or after them to see their result;
the session logger, for example, is log-output at priority 200.
Input priorities order user handlers after history expansion. Every handler sees
resolved input; a rewrite containing !! does not expand again. Expansion is not
a registered hook and cannot be removed through hook management. Disable it with
rune.config.set("history_character", "") when implementing custom history syntax.
Options
Section titled “Options”Hooks take the common options name,
group, and priority (order among handlers for the event, lower
first, default 50).
Managing
Section titled “Managing”Every constructor returns a handle with :enable(), :disable(), and
:remove(). By name: rune.hooks.disable/enable/remove(name). The full
list is in the API reference. In
the client, /hooks lists every handler, including the core’s own, since
the client registers its behavior through the same API.
Gotchas
Section titled “Gotchas”- A handler that throws is skipped for that line and reported once; it cannot abort the chain. Three consecutive failures quarantine it.
- A newly added handler starts with the next event. If a handler removes or disables another handler that has not run yet, the removed handler is skipped immediately.
Related: rune.hooks reference, Triggers, GMCP