Protect your script
Loader hooks & custom key UI
Your own Lua in a loader — a custom key window or UI library, code before loading and a fallback script — with the SA helpers.
On this page
A loader runs three optional pieces of your Lua. Each one is set in the loader editor (Obfuscation → Loaders → New loader or Edit) and each editor has Start from an example buttons that paste working code you can change.
| Hook | Where | When it runs |
|---|---|---|
| Code before loading | General tab | First, in every game, before routing. |
| Fallback script | Key & fallback tab → Script | When no route matches the game. The loader stops afterwards. |
| Custom key UI | Key & fallback tab → Key mode: Custom | After routing, before the script is downloaded. |
Order at runtime: code before loading → route by game.PlaceId, then game.GameId → (no route: fallback) → key stage → key check → download and run the routed script.
The SA table
Every hook gets a table called SA. You don't declare it — just use it.
| Field / function | What it does |
|---|---|
SA.prompt(build) | Runs a key UI and waits for it. See Custom key UI. |
SA.mountGui(screenGui) | Puts a ScreenGui into gethui(), falling back to CoreGui and then PlayerGui, and turns off ResetOnSpawn. Returns the same ScreenGui. |
SA.copyKeyLink() | Copies your key page link to the clipboard. false when the executor has no setclipboard — show SA.KeyLink then. |
SA.check(key) | Checks a key for the routed script: true, or false, message, code (e.g. "Key expired. [SA-K04]", "SA-K04"). Does not save the key. Only works in the key hook — before routing there is no script to check against. |
SA.setKey(key) | Sets the key the loader checks after your hook (also sets script_key). |
SA.savedKey() / SA.saveKey(key) / SA.forgetKey() | The key saved on this device (santiauth/<loader-id>.key, needs writefile). The built-in key window uses the same file. |
SA.stop(message) | Ends the loader quietly; message goes to the console. |
SA.kick(message) | Ends the loader and kicks the player with message. |
SA.Key | The current key — from script_key until something changes it. |
SA.KeyLink | Your key page link. |
SA.ServiceName, SA.PlaceId, SA.GameId, SA.LoaderId | Context for your UI and checks. |
Return values. A hook that returns false (or calls SA.stop / SA.kick) stops the loader. Anything else lets it continue. A hook that throws an error, or doesn't compile, is reported in the console with [SantiAuth] and the loader carries on.
Custom key UI
Pick Key mode: Custom and wrap your UI in SA.prompt. You only draw the window and pass the typed key to submit — everything else is done for you:
- If
script_keyor a key saved on this device still works, your UI is not shown at all. submit(key)trims the key, checks it and returnsok, message. Showmessageto the player whenokisfalse— it already contains the reason and its code, likeUnknown key. [SA-K02].- An accepted key is saved on the device, so next time the player isn't asked again.
- What
buildreturns is cleaned up when the prompt ends: anInstanceis destroyed, a function is called. close()means the player gave up:SA.promptreturnsfalseand the loader stops without a kick.
Always return SA.prompt(...) so that closing the window stops the loader.
return SA.prompt(function(submit, close)
local gui = SA.mountGui(Instance.new("ScreenGui"))
local frame = Instance.new("Frame")
frame.AnchorPoint = Vector2.new(0.5, 0.5)
frame.Position = UDim2.fromScale(0.5, 0.5)
frame.Size = UDim2.fromOffset(320, 150)
frame.Parent = gui
local input = Instance.new("TextBox")
input.PlaceholderText = "Key"
input.Text = ""
input.ClearTextOnFocus = false
input.Position = UDim2.fromOffset(16, 16)
input.Size = UDim2.new(1, -32, 0, 36)
input.Parent = frame
local status = Instance.new("TextLabel")
status.Text = "Paste your key and press Enter."
status.TextWrapped = true
status.BackgroundTransparency = 1
status.Position = UDim2.fromOffset(16, 60)
status.Size = UDim2.new(1, -32, 0, 30)
status.Parent = frame
local getKey = Instance.new("TextButton")
getKey.Text = "Get key"
getKey.Position = UDim2.fromOffset(16, 100)
getKey.Size = UDim2.new(1, -32, 0, 34)
getKey.Parent = frame
input.FocusLost:Connect(function(enterPressed)
if not enterPressed then return end
status.Text = "Checking..."
local ok, message = submit(input.Text)
if not ok then status.Text = message end
end)
getKey.MouseButton1Click:Connect(function()
status.Text = SA.copyKeyLink() and "Link copied. Open it in your browser." or SA.KeyLink
end)
return gui
end)The Key window example in the editor is a fuller version with a title, two buttons and a close button.
With a UI library
Any UI library (Rayfield, Orion, Fluent, your own) works the same way: create the window inside build, call submit(key) from the library's callback, and return a function that removes the window.
return SA.prompt(function(submit, close)
local Rayfield = loadstring(game:HttpGet("https://sirius.menu/rayfield"))()
local window = Rayfield:CreateWindow({ Name = SA.ServiceName, LoadingTitle = SA.ServiceName })
local tab = window:CreateTab("Key")
local typed = ""
tab:CreateInput({
Name = "Key",
PlaceholderText = "Paste your key",
RemoveTextAfterFocusLost = false,
Callback = function(text) typed = text end,
})
tab:CreateButton({
Name = "Check key",
Callback = function()
local ok, message = submit(typed)
if not ok then Rayfield:Notify({ Title = "Key", Content = message, Duration = 6 }) end
end,
})
return function() Rayfield:Destroy() end
end)Load the library inside build: then it isn't downloaded at all for players whose saved key still works.
Without a UI
If your hub already has its own settings or key system, just hand the key over. The loader checks it right after your hook and kicks the player with the reason when it's wrong or missing.
local settings = getgenv().MyHub or {}
SA.setKey(settings.Key or SA.Key)Rules of thumb
- Don't kick the player yourself on a wrong key — show
messageand let them try again. - Don't
wait()for the player outsideSA.prompt: it already waits until a key is accepted orclose()is called. - Need a different look per game? Use
SA.PlaceId/SA.GameIdinsidebuild. - Hooks are only minified, not obfuscated — anyone with the loader line can read them. Never put secrets in them.
- To reset a saved key on a device, call
SA.forgetKey()(for example from a "Change key" button in your hub).
Code before loading
Runs first, in every game — including games without a route. Use it to set globals your scripts read or to refuse an environment early.
getgenv().HubSettings = { AutoFarm = true, Theme = "Dark" }
local executor = type(identifyexecutor) == "function" and identifyexecutor() or "Unknown"
if executor:find("Solara") then
SA.stop(executor .. " is not supported. Use another executor.")
endSA.check doesn't work here yet — the game isn't routed.
Fallback script
With Fallback action: Script, your Lua runs when no route matches game.PlaceId or game.GameId; the loader stops afterwards. If the field is empty, the loader shows the fallback message instead.
game:GetService("StarterGui"):SetCore("SendNotification", {
Title = SA.ServiceName,
Text = "This game is not supported yet.",
Duration = 8,
})Loading a free universal script here is fine, but it isn't protected by SantiAuth — route the game to an obfuscated script if it needs a key.
Troubleshooting
| In the console | What it means |
|---|---|
[SantiAuth] key hook does not compile: … | Syntax error in your custom key code. The loader skips the hook and checks script_key. |
[SantiAuth] key hook failed: … / key UI failed: … | Your code threw an error at runtime. The message names the line (key:12 = line 12 of your code). |
No key provided. … [SA-K01] kick right after your UI | The hook ended without an accepted key — you didn't return SA.prompt(...) or forgot to call submit. |
[SA-N01] | The key server could not be reached — the player's connection or the executor's request failed. |
Other codes are listed in Status codes and on Obfuscation & loaders.