Lifecycle
Every script follows a predictable lifecycle:OnStart()/OnStop()— set up and tear down (start timers, cancel tasks, release keys).OnFocus(window)/OnBlur(window)— fire when a targeted app gains or loses focus (see Targeting).OnTick(dtMs)— runs on a fixed cadence while the script is active; default 1,000/sec, up to 8,000/sec with thetick_rate=8000modeline.dtMsis the real time elapsed since the previousOnTick(milliseconds, fractional;0on the first call) — use it for frame-rate-independent motion.OnDown/OnUp/OnMove/OnScroll— input events, covered next.
OnFocus/OnBlur never fire. See Hooks for the exact signatures.
Input and blocking
Input hooks fire when physical input arrives from your captured devices. Their return value decides whether your PC sees the input:return true— input passes through normally.return false— input is swallowed; the PC never sees it.- no return /
return nil— same asreturn true.
HID.
PreferBindis the declarative shortcut for simple remaps and uses the opposite default — it blocks unless youreturn true. See Bind.
Bind when you only care about specific keys. Because OnDown/OnUp
may block anything, defining either one makes Rebind intercept every key
system-wide and re-emit what your script passes through — and tools that only
trust real device input (Keyboard Maestro, Karabiner, some games) will not see
re-emitted keys while your script runs. Bind intercepts only its declared
trigger keys and leaves the rest of the keyboard untouched. A hook script that
never blocks anything can declare key_block=false in its modeline to observe
without intercepting at all.
Mouse moves are handled differently, and they need hardware. Any script that
defines OnMove requires a Rebind device — Rebind refuses to load an
OnMove script in software mode, because the OS draws the cursor upstream of
anything a host process could intercept in time. (Key, button, and scroll blocking
work in both modes; only mouse movement is hardware-gated.)
On a device, OnMove fires asynchronously by default — the move is forwarded to
your PC immediately and the return value is ignored, so script time never sits in
the mouse path. To modify or swallow moves (acceleration curves, cursor smoothing),
add mouse_block=true to the modeline; then the return value matters:
"A", "LCtrl", "Mouse1", …). The complete list — every alias and which keys are input- or output-only — is in the Key reference.
Sending output
TheHID namespace sends keyboard and mouse output through the active transport; it appears as real input to your PC.
HID.Down/HID.Uphold and release a key across time.HID.Combofires a whole chord in one call;HID.Presstaps a key (optional hold in ms);HID.Typeinserts a string instantly.- Use
+for modifier combos:HID.Combo("LCtrl+V")presses in order and releases in reverse.
HID.Combo,HID.Down,HID.Up, andHID.Typeare non-blocking and safe anywhere — including an input hook or aBindaction.HID.PressandHID.Typewriteruse an internalSleep, so they must run inside aRun()coroutine or anAsync()handler.
HID.Type inserts text instantly. For a human-like per-character effect, use HID.Typewriter(text, delayMs?) (coroutine only). For very long text, paste is still an option:
Doing things over time
For sequences, delays, and loops, useRun() and Sleep():
Run() launches a coroutine that executes concurrently; Sleep() pauses that coroutine without blocking anything else, and is only valid inside a Run() block. Reach for:
After(ms, fn)— a one-off delay without theRun/Sleepboilerplate.Async(fn)— wrap a callback so it runs in a coroutine; use it forBindcallbacks that needSleep.Timer.After/Timer.Every— simple delayed or repeated callbacks, when you don’t want to manage a coroutine loop yourself.
Coroutines are cooperative and tick-driven.Sleepresumes on the next engine tick past its deadline, so its timing is tick-granular rather than exact milliseconds, and aRun()body that never callsSleepruns straight through on a single tick — an infinite loop with noSleepstalls the script (input freezes) until the runtime’s per-tick budget (~200 ms) cuts it off. Always put aSleepinside long-running loops.
Run() returns a handle (task:Cancel(), task:IsRunning()). Cancel long-running tasks in OnStop/OnBlur so they don’t leak:
Reading state
When you need to know what’s happening right now rather than waiting for an event, poll it:Inputreports currently held keys, modifiers, and how long a key has been down — useful insideOnTickor aRun()loop.Systemgives the current time, cursor position, screen size, and focused window, refreshed each tick.
System.Time in use.
Config panels
A script can define a settings panel that appears in the Rebind UI, so users tune it without editing code.UI.Schema declares the controls; you read and write values through the returned handle, and they persist automatically (keyed to the script’s file path):
Splitting across files (require)
As a script grows, pull shared logic into its own file and load it withrequire(). A module is just a .luau / .lua file that returns a value —
usually a table:
require resolves relative to the script’s own directory, dots are path
separators (require("lib.math") loads lib/math.luau), modules are cached
after the first load, and anything outside the script’s folder is rejected —
local files only, no external packages. The exact resolution order and
sandboxing rules are in the require reference.
Only the script you run needs amin_sdkmodeline. Arequire’d module is loaded through that script rather than run directly, so it needs no header.
Macros
Macro.Play plays back a recorded input sequence — a table of move/press/scroll/sleep actions. Reach for a macro when you have a fixed sequence to replay; use raw HID calls when the output depends on logic. Patterns can be transformed before playback (Math.Scale, Math.Spline, Math.Resample). See the Macro reference for the action format and playback modes.
Targeting and the always-on model
Restrict a script to specific applications with thewindow= and process= modeline keys. window= matches a case-insensitive substring of the focused window’s title (so window=code also matches “Visual Studio Code”); process= matches the process name the same way. A script activates if any window= or any process= pattern matches — the two lists combine as OR — and it deactivates when the matching window loses focus:
OnTick never fire. So you can keep hundreds of scripts loaded at once and only the script(s) matching the focused window run, with no interference or wasted CPU:
z_index controls which sees input first (a false return blocks lower-priority scripts), and instance controls what happens when a script loads while a copy is already running. Both are in the Modeline reference.
Patterns
Common shapes that compose the pieces above.Toggle
Hold loop
Double-tap
Validation
The runtime lints your script on load and warns (in the Logs tab, without blocking the load) about common issues:- No hooks defined — the script loads but never runs any logic.
Net.Get/Net.Postin a high-frequencyOnTick— synchronous HTTP blocks the main thread; move it insideRun(). WebSocket handlers (Net.WSListen/Net.WSConnect) are exempt — their I/O runs on dedicated threads.
Type checking
The SDK ships a type definition file (types/rebind.d.luau) for the Luau Language Server, giving you autocomplete, hover docs, and type checking in VS Code. Add a .luaurc next to your script: