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.

HookWhereWhen it runs
Code before loadingGeneral tabFirst, in every game, before routing.
Fallback scriptKey & fallback tab → ScriptWhen no route matches the game. The loader stops afterwards.
Custom key UIKey & fallback tab → Key mode: CustomAfter 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 / functionWhat 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.KeyThe current key — from script_key until something changes it.
SA.KeyLinkYour key page link.
SA.ServiceName, SA.PlaceId, SA.GameId, SA.LoaderIdContext 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_key or a key saved on this device still works, your UI is not shown at all.
  • submit(key) trims the key, checks it and returns ok, message. Show message to the player when ok is false — it already contains the reason and its code, like Unknown key. [SA-K02].
  • An accepted key is saved on the device, so next time the player isn't asked again.
  • What build returns is cleaned up when the prompt ends: an Instance is destroyed, a function is called.
  • close() means the player gave up: SA.prompt returns false and the loader stops without a kick.

Always return SA.prompt(...) so that closing the window stops the loader.

Custom key windowlua
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.

Rayfieldlua
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.

Key from your settingslua
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 message and let them try again.
  • Don't wait() for the player outside SA.prompt: it already waits until a key is accepted or close() is called.
  • Need a different look per game? Use SA.PlaceId / SA.GameId inside build.
  • 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.

Code before loadinglua
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.")
end

SA.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.

Fallback scriptlua
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 consoleWhat 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 UIThe 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.