Roblox logic is usually tried out by pressing Play in Studio, which an agent can't do after every change. Rules that never touch the DataModel can be checked by the standalone Luau tools instead: luau-analyze type-checks them in strict mode, and luau runs their tests from the command line.
This loadout sets out the split. Prices, inventory, damage and cooldown maths go in src/shared/pure as --!strict modules that require each other by string (require("./Money"), which Roblox and the luau CLI both resolve). Server and client scripts stay as thin edges that own services, Instances and remotes. A standard-library Python scanner flags Roblox-only calls in the pure folder (game, workspace, script.Parent requires, Instance.new, services, task and the deprecated wait/spawn/delay, Roblox datatypes) as errors, and math.random, os.time and print as warnings. A small test runner prints PASS or FAIL per case and exits non-zero on failure. The verifier runs it all with the official Luau 0.743 tools on a Rojo shop project, and shows a broken rule failing the run.
What's in it
- Pure --!strict modules in src/shared/pure, thin edge scripts for Roblox APIs
- Scanner flags game, script.Parent requires, Instance.new and task.* in pure code
- luau-analyze --mode=strict checks the rules and their tests outside Studio
- A tiny test runner for the luau CLI that exits non-zero on failure
- String requires that work in both Roblox and the luau CLI
You'll need
- Tools:
python3,luau
Verification
| Check | Result | What we found |
|---|---|---|
| Listinglisting.yaml matches the schema and declares every file in the folder. | Pass | Schema valid; 8 files declared, none missing or extra
|
| Files parseJSON parses; SKILL.md and .mdc front matter is valid YAML with the fields each agent needs; code fences are closed. | Pass | All 8 files parse; front matter valid |
| AdaptersCLAUDE.md imports @AGENTS.md, GEMINI.md imports @./AGENTS.md, and both match the generator. | Pass | CLAUDE.md and GEMINI.md import AGENTS.md and are up to date
|
| Install pathsEvery file lands where each agent documents it, with no collisions, and the laid-out project is re-discovered correctly. | Pass | Correct paths for all 6 agents; layouts re-discovered
|
| MCP configmcp.json is structurally valid, converts to each agent's format, declares no literal secrets, and referenced packages exist on npm/PyPI. | Not applicable | No MCP servers in this loadout |
| Safety scanThe intake scanner finds no credentials, risky shell, hidden Unicode, instructions hidden in HTML comments or after long blank padding, or prompt-injection and covert wording (such as telling the agent to keep something from you), and the listing's network flag is honest. | Pass | Clean: no credentials, risky shell, hidden text or covert instructions
|
| ReferencesLinks and file paths between the loadout's files resolve, including for each agent's installed layout. | Pass | 1 reference between files resolves for every agent |
| Size limitsFile count and sizes are within the submission limits; SKILL.md bodies stay under 500 lines. | Pass | 8 files, 13.9 KB total; largest skills/luau-pure-tests/scripts/pure_luau_guard.py 5.7 KB
|
| LicenceA recognised SPDX licence, with the licence text available. | Pass | MIT, by SetupBay; text in the repository LICENSE |
| Starter buildA minimal fixture project with the loadout applied installs and builds or type-checks, and the loadout's own code examples compile. | Pass | pure_luau_guard.py passes the shop's pure modules and flags every Roblox-only line in the skill's Studio-coupled sample: 13 errors and 2 warnings. With the official Luau 0.743 tools, luau-analyze --mode=strict passes the pure modules and their tests and rejects the sample's game, script and Instance
|
What this means: a script checked the files themselves: they parse, they install where each agent looks, any MCP config is sound, the safety scan is clean, references between files resolve, and, where a free and repeatable fixture exists, a small starter project builds with the loadout applied. No AI agent was run. Per-agent results from agent runs are separate and stay untested until an agent actually runs.
Install
Choose your agent. Run the commands from your project root, or download a zip already laid out in the right folders.
Claude Code reads CLAUDE.md (imports AGENTS.md with @AGENTS.md) and skills from .claude/skills/.
curl -fsSL --create-dirs -o AGENTS.md https://setupbay.com/raw/roblox-luau-pure-modules/1.0.0/AGENTS.md curl -fsSL --create-dirs -o CLAUDE.md https://setupbay.com/raw/roblox-luau-pure-modules/1.0.0/CLAUDE.md curl -fsSL --create-dirs -o .claude/skills/luau-pure-tests/SKILL.md https://setupbay.com/raw/roblox-luau-pure-modules/1.0.0/skills/luau-pure-tests/SKILL.md curl -fsSL --create-dirs -o .claude/skills/luau-pure-tests/scripts/pure_luau_guard.py https://setupbay.com/raw/roblox-luau-pure-modules/1.0.0/skills/luau-pure-tests/scripts/pure_luau_guard.py curl -fsSL --create-dirs -o .claude/skills/luau-pure-tests/references/run.luau https://setupbay.com/raw/roblox-luau-pure-modules/1.0.0/skills/luau-pure-tests/references/run.luau curl -fsSL --create-dirs -o .claude/skills/luau-pure-tests/references/studio-coupled-sample.luau.txt https://setupbay.com/raw/roblox-luau-pure-modules/1.0.0/skills/luau-pure-tests/references/studio-coupled-sample.luau.txt
| File | Goes to |
|---|---|
AGENTS.md | AGENTS.md |
CLAUDE.md | CLAUDE.md |
skills/luau-pure-tests/SKILL.md | .claude/skills/luau-pure-tests/SKILL.md |
skills/luau-pure-tests/scripts/pure_luau_guard.py | .claude/skills/luau-pure-tests/scripts/pure_luau_guard.py |
skills/luau-pure-tests/references/run.luau | .claude/skills/luau-pure-tests/references/run.luau |
skills/luau-pure-tests/references/studio-coupled-sample.luau.txt | .claude/skills/luau-pure-tests/references/studio-coupled-sample.luau.txt |
Already have an AGENTS.md or CLAUDE.md? The commands above overwrite it — download the file instead and merge the sections you want. Which file wins when several are present: what Claude Code reads.
Codex CLI reads AGENTS.md and skills from .agents/skills/.
curl -fsSL --create-dirs -o AGENTS.md https://setupbay.com/raw/roblox-luau-pure-modules/1.0.0/AGENTS.md curl -fsSL --create-dirs -o .agents/skills/luau-pure-tests/SKILL.md https://setupbay.com/raw/roblox-luau-pure-modules/1.0.0/skills/luau-pure-tests/SKILL.md curl -fsSL --create-dirs -o .agents/skills/luau-pure-tests/scripts/pure_luau_guard.py https://setupbay.com/raw/roblox-luau-pure-modules/1.0.0/skills/luau-pure-tests/scripts/pure_luau_guard.py curl -fsSL --create-dirs -o .agents/skills/luau-pure-tests/references/run.luau https://setupbay.com/raw/roblox-luau-pure-modules/1.0.0/skills/luau-pure-tests/references/run.luau curl -fsSL --create-dirs -o .agents/skills/luau-pure-tests/references/studio-coupled-sample.luau.txt https://setupbay.com/raw/roblox-luau-pure-modules/1.0.0/skills/luau-pure-tests/references/studio-coupled-sample.luau.txt
| File | Goes to |
|---|---|
AGENTS.md | AGENTS.md |
skills/luau-pure-tests/SKILL.md | .agents/skills/luau-pure-tests/SKILL.md |
skills/luau-pure-tests/scripts/pure_luau_guard.py | .agents/skills/luau-pure-tests/scripts/pure_luau_guard.py |
skills/luau-pure-tests/references/run.luau | .agents/skills/luau-pure-tests/references/run.luau |
skills/luau-pure-tests/references/studio-coupled-sample.luau.txt | .agents/skills/luau-pure-tests/references/studio-coupled-sample.luau.txt |
Already have an AGENTS.md or CLAUDE.md? The commands above overwrite it — download the file instead and merge the sections you want. Which file wins when several are present: what Codex CLI reads.
Cursor reads AGENTS.md + .cursor/rules/*.mdc and skills from .agents/skills/.
curl -fsSL --create-dirs -o AGENTS.md https://setupbay.com/raw/roblox-luau-pure-modules/1.0.0/AGENTS.md curl -fsSL --create-dirs -o .cursor/rules/luau-pure-modules.mdc https://setupbay.com/raw/roblox-luau-pure-modules/1.0.0/.cursor/rules/luau-pure-modules.mdc curl -fsSL --create-dirs -o .agents/skills/luau-pure-tests/SKILL.md https://setupbay.com/raw/roblox-luau-pure-modules/1.0.0/skills/luau-pure-tests/SKILL.md curl -fsSL --create-dirs -o .agents/skills/luau-pure-tests/scripts/pure_luau_guard.py https://setupbay.com/raw/roblox-luau-pure-modules/1.0.0/skills/luau-pure-tests/scripts/pure_luau_guard.py curl -fsSL --create-dirs -o .agents/skills/luau-pure-tests/references/run.luau https://setupbay.com/raw/roblox-luau-pure-modules/1.0.0/skills/luau-pure-tests/references/run.luau curl -fsSL --create-dirs -o .agents/skills/luau-pure-tests/references/studio-coupled-sample.luau.txt https://setupbay.com/raw/roblox-luau-pure-modules/1.0.0/skills/luau-pure-tests/references/studio-coupled-sample.luau.txt
| File | Goes to |
|---|---|
AGENTS.md | AGENTS.md |
.cursor/rules/luau-pure-modules.mdc | .cursor/rules/luau-pure-modules.mdc |
skills/luau-pure-tests/SKILL.md | .agents/skills/luau-pure-tests/SKILL.md |
skills/luau-pure-tests/scripts/pure_luau_guard.py | .agents/skills/luau-pure-tests/scripts/pure_luau_guard.py |
skills/luau-pure-tests/references/run.luau | .agents/skills/luau-pure-tests/references/run.luau |
skills/luau-pure-tests/references/studio-coupled-sample.luau.txt | .agents/skills/luau-pure-tests/references/studio-coupled-sample.luau.txt |
Already have an AGENTS.md or CLAUDE.md? The commands above overwrite it — download the file instead and merge the sections you want. Which file wins when several are present: what Cursor reads.
Gemini CLI reads GEMINI.md (imports AGENTS.md with @./AGENTS.md) and skills from .agents/skills/.
curl -fsSL --create-dirs -o AGENTS.md https://setupbay.com/raw/roblox-luau-pure-modules/1.0.0/AGENTS.md curl -fsSL --create-dirs -o GEMINI.md https://setupbay.com/raw/roblox-luau-pure-modules/1.0.0/GEMINI.md curl -fsSL --create-dirs -o .agents/skills/luau-pure-tests/SKILL.md https://setupbay.com/raw/roblox-luau-pure-modules/1.0.0/skills/luau-pure-tests/SKILL.md curl -fsSL --create-dirs -o .agents/skills/luau-pure-tests/scripts/pure_luau_guard.py https://setupbay.com/raw/roblox-luau-pure-modules/1.0.0/skills/luau-pure-tests/scripts/pure_luau_guard.py curl -fsSL --create-dirs -o .agents/skills/luau-pure-tests/references/run.luau https://setupbay.com/raw/roblox-luau-pure-modules/1.0.0/skills/luau-pure-tests/references/run.luau curl -fsSL --create-dirs -o .agents/skills/luau-pure-tests/references/studio-coupled-sample.luau.txt https://setupbay.com/raw/roblox-luau-pure-modules/1.0.0/skills/luau-pure-tests/references/studio-coupled-sample.luau.txt
| File | Goes to |
|---|---|
AGENTS.md | AGENTS.md |
GEMINI.md | GEMINI.md |
skills/luau-pure-tests/SKILL.md | .agents/skills/luau-pure-tests/SKILL.md |
skills/luau-pure-tests/scripts/pure_luau_guard.py | .agents/skills/luau-pure-tests/scripts/pure_luau_guard.py |
skills/luau-pure-tests/references/run.luau | .agents/skills/luau-pure-tests/references/run.luau |
skills/luau-pure-tests/references/studio-coupled-sample.luau.txt | .agents/skills/luau-pure-tests/references/studio-coupled-sample.luau.txt |
Already have an AGENTS.md or CLAUDE.md? The commands above overwrite it — download the file instead and merge the sections you want. Which file wins when several are present: what Gemini CLI reads.
Antigravity CLI reads AGENTS.md (read natively; GEMINI.md is also read) and skills from .agents/skills/.
curl -fsSL --create-dirs -o AGENTS.md https://setupbay.com/raw/roblox-luau-pure-modules/1.0.0/AGENTS.md curl -fsSL --create-dirs -o .agents/skills/luau-pure-tests/SKILL.md https://setupbay.com/raw/roblox-luau-pure-modules/1.0.0/skills/luau-pure-tests/SKILL.md curl -fsSL --create-dirs -o .agents/skills/luau-pure-tests/scripts/pure_luau_guard.py https://setupbay.com/raw/roblox-luau-pure-modules/1.0.0/skills/luau-pure-tests/scripts/pure_luau_guard.py curl -fsSL --create-dirs -o .agents/skills/luau-pure-tests/references/run.luau https://setupbay.com/raw/roblox-luau-pure-modules/1.0.0/skills/luau-pure-tests/references/run.luau curl -fsSL --create-dirs -o .agents/skills/luau-pure-tests/references/studio-coupled-sample.luau.txt https://setupbay.com/raw/roblox-luau-pure-modules/1.0.0/skills/luau-pure-tests/references/studio-coupled-sample.luau.txt
| File | Goes to |
|---|---|
AGENTS.md | AGENTS.md |
skills/luau-pure-tests/SKILL.md | .agents/skills/luau-pure-tests/SKILL.md |
skills/luau-pure-tests/scripts/pure_luau_guard.py | .agents/skills/luau-pure-tests/scripts/pure_luau_guard.py |
skills/luau-pure-tests/references/run.luau | .agents/skills/luau-pure-tests/references/run.luau |
skills/luau-pure-tests/references/studio-coupled-sample.luau.txt | .agents/skills/luau-pure-tests/references/studio-coupled-sample.luau.txt |
Already have an AGENTS.md or CLAUDE.md? The commands above overwrite it — download the file instead and merge the sections you want. Which file wins when several are present: what Antigravity CLI reads.
GitHub Copilot reads AGENTS.md and skills from .agents/skills/.
curl -fsSL --create-dirs -o AGENTS.md https://setupbay.com/raw/roblox-luau-pure-modules/1.0.0/AGENTS.md curl -fsSL --create-dirs -o .agents/skills/luau-pure-tests/SKILL.md https://setupbay.com/raw/roblox-luau-pure-modules/1.0.0/skills/luau-pure-tests/SKILL.md curl -fsSL --create-dirs -o .agents/skills/luau-pure-tests/scripts/pure_luau_guard.py https://setupbay.com/raw/roblox-luau-pure-modules/1.0.0/skills/luau-pure-tests/scripts/pure_luau_guard.py curl -fsSL --create-dirs -o .agents/skills/luau-pure-tests/references/run.luau https://setupbay.com/raw/roblox-luau-pure-modules/1.0.0/skills/luau-pure-tests/references/run.luau curl -fsSL --create-dirs -o .agents/skills/luau-pure-tests/references/studio-coupled-sample.luau.txt https://setupbay.com/raw/roblox-luau-pure-modules/1.0.0/skills/luau-pure-tests/references/studio-coupled-sample.luau.txt
| File | Goes to |
|---|---|
AGENTS.md | AGENTS.md |
skills/luau-pure-tests/SKILL.md | .agents/skills/luau-pure-tests/SKILL.md |
skills/luau-pure-tests/scripts/pure_luau_guard.py | .agents/skills/luau-pure-tests/scripts/pure_luau_guard.py |
skills/luau-pure-tests/references/run.luau | .agents/skills/luau-pure-tests/references/run.luau |
skills/luau-pure-tests/references/studio-coupled-sample.luau.txt | .agents/skills/luau-pure-tests/references/studio-coupled-sample.luau.txt |
Already have an AGENTS.md or CLAUDE.md? The commands above overwrite it — download the file instead and merge the sections you want. Which file wins when several are present: what GitHub Copilot reads.
Or as a Claude Code plugin
Adds the skill to Claude Code for all your projects, without copying files. Plugins can't carry project rules, so add AGENTS.md and CLAUDE.md with the commands above. About the plugin marketplace.
/plugin marketplace add https://setupbay.com/marketplace.json /plugin install roblox-luau-pure-modules@setupbay
Files
Rules AGENTS.md The pure/edge split, the rules for each, and the done checklist. 39 lines
# AGENTS.md: Roblox game with pure Luau modules checked outside Studio
Game rules (prices, inventory, damage, cooldown maths, matchmaking) live in **pure modules** in `src/shared/pure/`.
A pure module never touches the DataModel, so the plain `luau` CLI can type-check and test it in seconds, with no
Studio open. Scripts in `src/server/` and `src/client/` are the **edge**: they get services, read and write
Instances, handle remotes, then call the pure modules with plain values.
## Layout (Rojo)
```
src/shared/pure/ → ReplicatedStorage.Shared.pure pure ModuleScripts, --!strict, no Roblox APIs
src/shared/ → ReplicatedStorage.Shared other shared modules and Remotes
src/server/ → ServerScriptService.Server server edge scripts (*.server.luau)
src/client/ → StarterPlayerScripts client edge scripts (*.client.luau)
tests/ (not synced) *.spec.luau and run.luau, for the luau CLI
```
## Rules for pure modules
- First line `--!strict`. Export the types callers need (`export type State = {...}`).
- No `game`, `workspace`, `script`, `Instance`, services, `task.*`, `wait`/`spawn`/`delay`, `tick()`, or Roblox
datatypes (`Vector3`, `CFrame`, `Color3`, `Enum`, `Random`). Take what you need as parameters.
- Require other pure modules by string: `require("./Money")`. Roblox (require-by-string) and the luau CLI both
resolve it relative to the file. Never `require(script.Parent.Money)` inside `pure/`.
- No `print`/`warn`, no `math.random` or `os.time` inside rules. Return values and errors, and take an rng or the
current time as a parameter, so tests are repeatable.
- Validate inputs in the pure function too (whole numbers, ranges, NaN). The edge script checks types from remotes;
the rules decide what's allowed.
## Rules for edge scripts
- Keep them thin: read Instances and remote arguments, call the pure module, apply the result.
- Use `task.wait` / `task.spawn` / `task.delay`, never the deprecated `wait` / `spawn` / `delay`.
- The server decides: remotes carry intent, and the server runs the pure rules before changing any state.
## Before you say "done"
1. `python3 .agents/skills/luau-pure-tests/scripts/pure_luau_guard.py src/shared/pure` reports `0 error(s)`.
2. `luau-analyze --mode=strict src/shared/pure tests` reports nothing.
3. `luau tests/run.luau` ends with `0 failed`. Add a test for every rule you change.
4. List what the human should play-test in Studio for the edge scripts. If Roblox Studio's built-in MCP server is
connected, you can run that play-test through it, but the pure tests above still come first. Adapter CLAUDE.md Imports AGENTS.md for Claude Code. 11 lines
# CLAUDE.md
@AGENTS.md
## Claude Code notes
- Skills from this loadout live in `.claude/skills/`: `luau-pure-tests`. Use them when their description matches the task.
- Follow AGENTS.md above; where this file and AGENTS.md disagree, AGENTS.md wins.
<!-- From SetupBay "Roblox pure Luau modules checked outside Studio" v1.0.0 — https://setupbay.com/loadouts/roblox-luau-pure-modules/ --> Adapter GEMINI.md Imports AGENTS.md for Gemini CLI. 11 lines
# GEMINI.md
@./AGENTS.md
## Gemini CLI notes
- Skills from this loadout live in `.agents/skills/`: `luau-pure-tests`. Use them when their description matches the task.
- Follow AGENTS.md above; where this file and AGENTS.md disagree, AGENTS.md wins.
<!-- From SetupBay "Roblox pure Luau modules checked outside Studio" v1.0.0 — https://setupbay.com/loadouts/roblox-luau-pure-modules/ --> Cursor rule .cursor/rules/luau-pure-modules.mdc Never/instead table for pure modules and tests. 20 lines
---
description: Pure Luau modules in src/shared/pure must run outside Studio
globs: "src/shared/pure/**/*.luau,tests/**/*.luau"
alwaysApply: false
---
Files in `src/shared/pure/` are pure: the luau CLI must be able to run them with no Roblox engine.
| Never in `pure/` | Instead |
|---|---|
| `game:GetService(...)`, `workspace`, `script.Parent` | pass the values in from the edge script |
| `require(script.Parent.Money)` | `require("./Money")` |
| `Instance.new`, `Vector3`, `CFrame`, `Enum`, `Random` | plain numbers and tables |
| `task.wait`, `wait`, `spawn`, `delay`, `tick()` | return a value; the edge script schedules |
| `math.random()`, `os.time()` | take `rng` / `now` as parameters |
| `print`, `warn` | return `false, "reason"` |
- `--!strict` on the first line, with exported types.
- Every change gets a case in `tests/*.spec.luau`. Then run the `luau-pure-tests` skill's checks. Skill skills/luau-pure-tests/SKILL.md Check purity, type-check in strict mode, then run the tests with the luau CLI. 56 lines
---
name: luau-pure-tests
description: Keep a Roblox game's rules in pure Luau modules and run their tests outside Studio with the luau CLI, checking purity with a scanner and types with luau-analyze in strict mode. Use when writing or changing game logic (prices, inventory, damage, cooldowns), when asked to add tests to a Roblox project, or when a module can't run in a test because it calls Roblox APIs.
---
# Pure Luau modules, checked outside Studio
Roblox logic is usually tried out by pressing Play. Rules that don't touch the DataModel can be checked by the
standalone Luau tools instead: `luau-analyze` type-checks them, and `luau` runs their tests in milliseconds.
That's something an agent can run after every change.
## Tools
The Luau release (https://github.com/luau-lang/luau/releases) ships `luau` (runs scripts) and
`luau-analyze` (type-checks and lints) for Linux, macOS and Windows. Rokit or Aftman can install them per project;
otherwise download the release zip. Check with `luau-analyze --help`.
## 1. Check purity
```bash
python3 .agents/skills/luau-pure-tests/scripts/pure_luau_guard.py src/shared/pure
```
(Use the path the skill was installed to: `.claude/skills/...` for Claude Code, `.agents/skills/...` for the others.)
Run it on the pure folders only; edge scripts are meant to use Roblox APIs. Each `error` is a Roblox-only call that
stops the module running outside Studio. Move that call into the edge script and pass the value in. `warn` lines
(`math.random`, `os.time`, `print`) make tests unrepeatable; take an rng or the time as a parameter.
## 2. Type-check in strict mode
```bash
luau-analyze --mode=strict src/shared/pure tests
```
It must print nothing. `Unknown global 'game'` or `Unknown require: unsupported path` means a Roblox API or an
Instance-path require got into a pure module.
## 3. Run the tests
Copy `references/run.luau` to `tests/run.luau` once and list each spec file in it. A spec returns a list of cases:
```luau
--!strict
local Inventory = require("../src/shared/pure/Inventory")
return {
{ name = "Inventory: the stack limit holds", run = function()
local s = Inventory.new(500)
assert(Inventory.buy(s, { id = "sword", price = 150, stack = 1 }, 1))
assert(not Inventory.buy(s, { id = "sword", price = 150, stack = 1 }, 1))
end },
}
```
```bash
luau tests/run.luau
```
It prints `PASS`/`FAIL` per case and `N passed, M failed`, and exits non-zero on any failure.
## 4. Report
Say which rules changed, which tests you added, and that the scanner, `luau-analyze --mode=strict` and
`luau tests/run.luau` were clean. Then list what still needs a Studio play-test: the edge scripts, remotes and
anything that touches Instances. Skill file skills/luau-pure-tests/scripts/pure_luau_guard.py Read-only scanner for Roblox-only calls in pure Luau modules. 134 lines
#!/usr/bin/env python3
"""Find Roblox-only calls in Luau modules meant to be pure (no DataModel), so they run and test outside Studio. Run it on the pure folders only, such as src/shared.
Usage: python3 pure_luau_guard.py [PATH ...] (default: the current folder)
python3 pure_luau_guard.py --list (print every rule)
A "guard: ok" comment on a line silences its warnings (never its errors).
Prints one line per finding: path:line: level [rule] what is wrong -> what to use
Exit 0 when nothing at "error" level is found, 1 when something is, 2 on bad usage.
Standard library only. Read-only: it never edits a file and makes no network calls.
"""
import fnmatch
import os
import re
import sys
# (rule id, level, regex, message, file patterns). Patterns run against each line with comments removed.
RULES = [
('datamodel', 'error', '\\b(game|plugin)\\b\\s*[:.]|\\bworkspace\\b|\\bscript\\.(Parent|Name)\\b|\\bInstance\\.new\\(',
"pure modules can't touch the DataModel (game, workspace, script, Instance) -> pass plain values in from the server or client script", []),
('instance-require', 'error', '\\brequire\\(\\s*(script|game)\\b',
'Instance-path require only works in Studio -> require("./Sibling") or require("../Folder/Module"), which Roblox and the luau CLI both resolve', []),
('roblox-services', 'error', '\\b(Players|ReplicatedStorage|ServerStorage|RunService|DataStoreService|HttpService|TweenService|UserInputService|MarketplaceService|MemoryStoreService)\\b\\s*[:.]',
'a Roblox service in a pure module -> keep services in the edge script and pass what you need in', []),
('task-library', 'error', '\\btask\\.(wait|spawn|delay|defer|cancel)\\(',
'the task library is Roblox-only -> keep scheduling in the edge script; pure functions return values', []),
('deprecated-scheduler', 'error', '(?<![\\w.:])(wait|spawn|delay)\\(',
'wait/spawn/delay are deprecated in Roblox and missing in the luau CLI -> task.wait/task.spawn/task.delay in edge scripts only', []),
('roblox-types', 'error', '\\b(Vector3|CFrame|Color3|UDim2|BrickColor|TweenInfo|Enum|Random|DateTime)\\b\\s*[.(]',
'a Roblox datatype in a pure module -> use plain numbers and tables, or pass the value in', []),
('tick', 'error', '(?<![\\w.:])tick\\(\\)',
'tick() is deprecated -> os.clock() for timing, or pass the time in', []),
('connect-lowercase', 'error', ':connect\\(',
':connect is deprecated -> :Connect, and only in edge scripts', []),
('math-random', 'warn', '\\bmath\\.random\\(',
'random numbers make pure functions hard to test -> take an rng function or a seed as a parameter', []),
('os-time', 'warn', '\\bos\\.(time|clock)\\(',
'reading the clock makes pure functions hard to test -> take the time as a parameter', []),
('print-in-module', 'warn', '(?<![\\w.:])(print|warn)\\(',
'printing from a pure module -> return the value or an error and let the caller log', []),
]
FILES = ['*.luau', '*.lua']
COMMENT = '--'
SKIP_DIRS = {".git", ".venv", "venv", "node_modules", "__pycache__", ".agents", ".claude", ".cursor", "dist", "build", "target", ".godot"}
COMPILED = [(rid, lvl, re.compile(rx), msg, globs) for rid, lvl, rx, msg, globs in RULES]
def strip_comment(line):
"""Drop a trailing comment, leaving string literals alone."""
q = None
i = 0
while i < len(line):
c = line[i]
if q:
if c == "\\":
i += 2
continue
if c == q:
q = None
elif c in "\"'`":
q = c
elif line.startswith(COMMENT, i):
return line[:i]
i += 1
return line
def wanted(name):
return any(fnmatch.fnmatch(name, g) for g in FILES)
def walk(paths):
for p in paths:
if os.path.isfile(p):
yield p
continue
for root, dirs, files in os.walk(p):
dirs[:] = sorted(d for d in dirs if d not in SKIP_DIRS)
for f in sorted(files):
if wanted(f):
yield os.path.join(root, f)
def scan(path):
out = []
name = os.path.basename(path)
try:
with open(path, encoding="utf-8", errors="replace") as fh:
lines = fh.read().splitlines()
except OSError as e:
return [(path, 0, "error", "unreadable", str(e))]
for n, raw in enumerate(lines, 1):
code = strip_comment(raw)
if not code.strip():
continue
accepted = "guard: ok" in raw # a person accepted the warnings on this line
for rid, lvl, rx, msg, globs in COMPILED:
if accepted and lvl == "warn":
continue
if globs and not any(fnmatch.fnmatch(name, g) for g in globs):
continue
if rx.search(code):
out.append((path, n, lvl, rid, msg))
return out
def main(argv):
if "--list" in argv:
for rid, lvl, _, msg, globs in RULES:
print(f"{lvl:5} [{rid}] {msg}" + (f" (only {', '.join(globs)})" if globs else ""))
return 0
if any(a.startswith("-") for a in argv):
print(__doc__)
return 2
paths = argv or ["."]
for p in paths:
if not os.path.exists(p):
print(f"no such path: {p}", file=sys.stderr)
return 2
files = list(walk(paths))
findings = []
for f in files:
findings.extend(scan(f))
for path, n, lvl, rid, msg in findings:
print(f"{os.path.relpath(path)}:{n}: {lvl} [{rid}] {msg}")
errors = sum(1 for x in findings if x[2] == "error")
warns = sum(1 for x in findings if x[2] == "warn")
print(f"pure_luau_guard: {len(files)} file(s), {errors} error(s), {warns} warning(s)")
return 1 if errors else 0
if __name__ == "__main__":
sys.exit(main(sys.argv[1:])) Skill file skills/luau-pure-tests/references/run.luau Minimal test runner for spec files under the luau CLI. 22 lines
--!strict
-- Minimal test runner for the luau CLI: luau tests/run.luau
local specs = { require("./Inventory.spec"), require("./Money.spec") }
local passed, failed = 0, 0
for _, spec in specs do
for _, case in spec do
local ok, err = pcall(case.run)
if ok then
passed += 1
print(`PASS {case.name}`)
else
failed += 1
print(`FAIL {case.name}: {err}`)
end
end
end
print(`{passed} passed, {failed} failed`)
if failed > 0 then
error(`{failed} test(s) failed`, 0)
end Skill file skills/luau-pure-tests/references/studio-coupled-sample.luau.txt A Studio-coupled inventory module for checking the guard. 30 lines
-- A Studio-coupled inventory module, used to check the guard. It only runs inside Roblox.
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Money = require(script.Parent.Money)
local Config = require(game.ReplicatedStorage.Shared.Config)
local Inventory = {}
function Inventory.buy(player, item, quantity)
local stats = player:FindFirstChild("leaderstats")
if stats.Coins.Value < item.price * quantity then
warn("not enough coins")
return false
end
stats.Coins.Value -= item.price * quantity
local drop = Instance.new("Part")
drop.Position = Vector3.new(0, 10, 0)
drop.Parent = workspace
if math.random() < Config.bonusChance then
spawn(function()
wait(2)
stats.Coins.Value += 5
end)
end
player:SetAttribute("lastBuy", tick())
ReplicatedStorage.Remotes.Bought:FireClient(player, item.id)
return true
end
return Inventory All files: source zip · Licence MIT · By SetupBay · First listed
Credits
Public work that inspired this loadout. The wording here is our own; follow the links for the originals.
Installing and checks
How do I install Roblox pure Luau modules checked outside Studio in Claude Code?
From your project root, put AGENTS.md, CLAUDE.md, skills/luau-pure-tests/SKILL.md at .claude/skills/luau-pure-tests/SKILL.md, skills/luau-pure-tests/scripts/pure_luau_guard.py at .claude/skills/luau-pure-tests/scripts/pure_luau_guard.py, skills/luau-pure-tests/references/run.luau at .claude/skills/luau-pure-tests/references/run.luau, skills/luau-pure-tests/references/studio-coupled-sample.luau.txt at .claude/skills/luau-pure-tests/references/studio-coupled-sample.luau.txt. The "Claude Code" tab on this page has the curl commands, or download https://setupbay.com/download/roblox-luau-pure-modules/claude-code.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.
How do I install Roblox pure Luau modules checked outside Studio in Codex CLI?
From your project root, put AGENTS.md, skills/luau-pure-tests/SKILL.md at .agents/skills/luau-pure-tests/SKILL.md, skills/luau-pure-tests/scripts/pure_luau_guard.py at .agents/skills/luau-pure-tests/scripts/pure_luau_guard.py, skills/luau-pure-tests/references/run.luau at .agents/skills/luau-pure-tests/references/run.luau, skills/luau-pure-tests/references/studio-coupled-sample.luau.txt at .agents/skills/luau-pure-tests/references/studio-coupled-sample.luau.txt. The "Codex CLI" tab on this page has the curl commands, or download https://setupbay.com/download/roblox-luau-pure-modules/codex.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.
How do I install Roblox pure Luau modules checked outside Studio in Cursor?
From your project root, put AGENTS.md, .cursor/rules/luau-pure-modules.mdc, skills/luau-pure-tests/SKILL.md at .agents/skills/luau-pure-tests/SKILL.md, skills/luau-pure-tests/scripts/pure_luau_guard.py at .agents/skills/luau-pure-tests/scripts/pure_luau_guard.py, skills/luau-pure-tests/references/run.luau at .agents/skills/luau-pure-tests/references/run.luau, skills/luau-pure-tests/references/studio-coupled-sample.luau.txt at .agents/skills/luau-pure-tests/references/studio-coupled-sample.luau.txt. The "Cursor" tab on this page has the curl commands, or download https://setupbay.com/download/roblox-luau-pure-modules/cursor.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.
How do I install Roblox pure Luau modules checked outside Studio in Gemini CLI?
From your project root, put AGENTS.md, GEMINI.md, skills/luau-pure-tests/SKILL.md at .agents/skills/luau-pure-tests/SKILL.md, skills/luau-pure-tests/scripts/pure_luau_guard.py at .agents/skills/luau-pure-tests/scripts/pure_luau_guard.py, skills/luau-pure-tests/references/run.luau at .agents/skills/luau-pure-tests/references/run.luau, skills/luau-pure-tests/references/studio-coupled-sample.luau.txt at .agents/skills/luau-pure-tests/references/studio-coupled-sample.luau.txt. The "Gemini CLI" tab on this page has the curl commands, or download https://setupbay.com/download/roblox-luau-pure-modules/gemini-cli.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.
How do I install Roblox pure Luau modules checked outside Studio in Antigravity CLI?
From your project root, put AGENTS.md, skills/luau-pure-tests/SKILL.md at .agents/skills/luau-pure-tests/SKILL.md, skills/luau-pure-tests/scripts/pure_luau_guard.py at .agents/skills/luau-pure-tests/scripts/pure_luau_guard.py, skills/luau-pure-tests/references/run.luau at .agents/skills/luau-pure-tests/references/run.luau, skills/luau-pure-tests/references/studio-coupled-sample.luau.txt at .agents/skills/luau-pure-tests/references/studio-coupled-sample.luau.txt. The "Antigravity CLI" tab on this page has the curl commands, or download https://setupbay.com/download/roblox-luau-pure-modules/antigravity-cli.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.
How do I install Roblox pure Luau modules checked outside Studio in GitHub Copilot?
From your project root, put AGENTS.md, skills/luau-pure-tests/SKILL.md at .agents/skills/luau-pure-tests/SKILL.md, skills/luau-pure-tests/scripts/pure_luau_guard.py at .agents/skills/luau-pure-tests/scripts/pure_luau_guard.py, skills/luau-pure-tests/references/run.luau at .agents/skills/luau-pure-tests/references/run.luau, skills/luau-pure-tests/references/studio-coupled-sample.luau.txt at .agents/skills/luau-pure-tests/references/studio-coupled-sample.luau.txt. The "GitHub Copilot" tab on this page has the curl commands, or download https://setupbay.com/download/roblox-luau-pure-modules/copilot.zip, which is already laid out. Do not overwrite an existing AGENTS.md or CLAUDE.md; merge it.
What has been checked in Roblox pure Luau modules checked outside Studio?
It passed the Level 1 automated install checks on 2026-10-29: the files parse, install to each agent's documented paths, and pass the safety scan. No AI agent has run it yet, so no agent pass or fail is claimed.
More for Claude Code, Codex, Cursor, Gemini CLI, Antigravity CLI and GitHub Copilot.