Skip to content

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.

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 run
rune.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 active
local locked = false
rune.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")
end
end, { 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" end
end, { 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
end
end, { 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", "").

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.

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 recipe
end)

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.

Hooks take the common options name, group, and priority (order among handlers for the event, lower first, default 50).

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.

  • 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