From roblox-agent-skills
Filesystem-based Roblox development with Rojo + Wally + Luau LSP + StyLua + selene. Use when setting up, modifying, or debugging .project.json sync, package manifests, toolchain pinning, sourcemap regen, editor integration, CI builds, or any filesystem-to-DataModel question. Tooling layer only — language rules live in luau-expert; runtime behavior lives in roblox-dev; UI libraries live in roblox-ui.
How this skill is triggered — by the user, by Claude, or both
Slash command
/roblox-agent-skills:roblox-toolchainThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
How to develop Roblox games with files on disk instead of inside Studio. Sources: [rojo.space](https://rojo.space) (official Rojo docs), [github.com/rojo-rbx/rojo](https://github.com/rojo-rbx/rojo) (Rojo source), [github.com/rojo-rbx/rokit](https://github.com/rojo-rbx/rokit) (Rokit toolchain manager — use this, not Aftman/Foreman), [github.com/UpliftGames/wally](https://github.com/UpliftGames/w...
How to develop Roblox games with files on disk instead of inside Studio. Sources: rojo.space (official Rojo docs), github.com/rojo-rbx/rojo (Rojo source), github.com/rojo-rbx/rokit (Rokit toolchain manager — use this, not Aftman/Foreman), github.com/UpliftGames/wally (Wally package manager), github.com/pesde-pkg/pesde (Pesde — newer alternative package manager). Tooling: stylua, selene, luau-lsp.
For deep dives see:
references/project-format.md — full .project.json schemareferences/sync-mapping.md — every file extension Rojo recognizes and what it becomesreferences/wally-manifest.md — full wally.toml + lockfile + command referencereferences/pesde-manifest.md — full pesde.toml schema, environments, Wally bridgereferences/luau-lsp-setup.md — sourcemap regen, LSP configurationThis skill describes preferred patterns; it does not authorize rewriting existing code. Apply rules to new code and to changes the user explicitly asks for. Match the codebase's existing toolchain. If the project uses Aftman, don't migrate to Rokit unprompted. If it uses Wally, don't suggest Pesde. If default.project.json follows a particular structure, extend it — don't rewrite it. Surface conflicts once and default to the existing setup.
Full rules: ../../shared/integration-policy.md.
rokit.toml lists exact versions of rojo, wally (or pesde), stylua, selene, luau-lsp. Reproducible builds beat "works on my machine".main means rojo build produces a valid .rbxl and lints pass.Packages/ is generated. Wally writes it; you don't hand-edit it; it's .gitignore'd.Use Rokit (toolchain manager, successor to Foreman/Aftman) to pin versions per project:
# rokit.toml — drop-in compatible with foreman.toml / aftman.toml format
[tools]
rojo = "rojo-rbx/[email protected]"
wally = "UpliftGames/[email protected]"
stylua = "JohnnyMorganz/[email protected]"
selene = "Kampfkarren/[email protected]"
Then rokit install in the project root pulls those exact versions. Don't rely on globally-installed tools — versions drift between machines.
Why Rokit, not Aftman: Aftman is unmaintained (third-party author no longer working in Roblox); Foreman is maintained by Roblox but oriented toward internal use. Rokit is community-maintained, drop-in compatible with both manifest formats, and substantially faster to install. Migrate by renaming aftman.toml → rokit.toml.
Common Rokit commands: rokit init, rokit add OWNER/REPO, rokit install, rokit update, rokit list, rokit self-update.
# Live sync — Studio plugin connects to this server, applies changes as you save.
rojo serve [path/to/project.json]
# One-shot build — produces a .rbxl (place) or .rbxm (model).
rojo build [path/to/project.json] --output build/place.rbxl
rojo serve is for development. rojo build is for CI and final publish. The Studio plugin (install via Studio's plugin tab, version-matched to the CLI) is what actually applies live changes.
Rojo looks for default.project.json in the current directory. Pass an explicit path to use any other .project.json file. A repo with multiple builds (game, plugin, library) typically has one project per target.
default.project.json Structure{
"name": "MyGame",
"tree": {
"$className": "DataModel",
"ReplicatedStorage": {
"$className": "ReplicatedStorage",
"$path": "src/ReplicatedStorage"
},
"ServerScriptService": {
"$className": "ServerScriptService",
"$path": "src/ServerScriptService"
},
"StarterPlayer": {
"$className": "StarterPlayer",
"StarterPlayerScripts": {
"$className": "StarterPlayerScripts",
"$path": "src/StarterPlayerScripts"
}
},
"Workspace": {
"$className": "Workspace",
"$properties": {
"Gravity": 196.2
}
}
}
}
name (required) — used as the build artifact name.tree (required) — root Instance description.tree:
$className — Instance class. Optional if $path resolves it or if the key is a service name.$path — folder/file on disk to mount under this Instance.$properties — properties to set (e.g. Gravity, HttpEnabled).$ignoreUnknownInstances — if true, Rojo leaves Instances it didn't sync alone instead of deleting them.Other top-level fields: servePort (default 34872), servePlaceIds (allowlist of place IDs to prevent accidentally syncing into the wrong game), placeId, gameId, serveAddress, globIgnorePaths, emitLegacyScripts.
emitLegacyScripts controls whether *.server.luau/*.client.luau produce legacy Script/LocalScript Instances or Script Instances with RunContext set. Documented default is true; verify against your installed Rojo version (rojo --version + release notes) before assuming. New projects should set false to use modern RunContext — see roblox-dev script-types section.
Full schema: references/project-format.md.
This is the rule set you'll consult constantly. Memorize the common ones.
| Path on disk | Becomes |
|---|---|
dir/ (any directory) | Folder |
foo.luau | ModuleScript named foo (Rojo also accepts legacy .lua) |
foo.server.luau | Script named foo (server) |
foo.client.luau | LocalScript named foo (client) |
dir/init.luau | ModuleScript named dir (the directory becomes the script) |
dir/init.server.luau | Script named dir (directory becomes server script) |
dir/init.client.luau | LocalScript named dir (directory becomes local script) |
foo.rbxm / foo.rbxmx | Model from binary/XML |
foo.csv | LocalizationTable |
foo.txt | StringValue |
foo.json (not .model.json / .project.json) | ModuleScript returning the table |
foo.toml | ModuleScript returning the table |
foo.model.json | JSON-described model (hand-written, useful for RemoteEvent etc.) |
foo.project.json | Nested project (composes into parent) |
foo.meta.json | Metadata sidecar attached to the matching file/dir |
init.luau semanticsA directory + init.luau becomes one ModuleScript, where the directory's other children become children of that script. Lets a script have submodules without a separate folder.
src/Inventory/
├── init.luau # ModuleScript "Inventory"
├── add.luau # ModuleScript "add" (child of Inventory)
├── remove.luau # ModuleScript "remove"
└── types.luau # ModuleScript "types"
Only one init.* file per directory (init.luau xor init.server.luau xor init.client.luau).
foo.meta.json attaches Rojo-specific metadata to foo.*. Common uses:
// init.meta.json — turn the parent folder into a Tool with a Grip
{
"className": "Tool",
"properties": {
"Grip": [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1]
}
}
// foo.meta.json — disable a script
{
"properties": { "Disabled": true }
}
// hello.meta.json — let Rojo leave Instances under hello.txt alone
{
"ignoreUnknownInstances": true
}
className works only in init.meta.json. properties works on any non-model file.
Full mapping table: references/sync-mapping.md.
my-game/
├── rokit.toml # toolchain pinning
├── default.project.json # Rojo project file
├── wally.toml # package manifest
├── wally.lock # lockfile (committed)
├── stylua.toml # formatter config
├── selene.toml # linter config
├── sourcemap.json # generated; .gitignore'd
├── Packages/ # wally output; .gitignore'd
├── ServerPackages/ # wally output; .gitignore'd
├── DevPackages/ # wally output; .gitignore'd
├── .github/workflows/ci.yml # CI
├── (editor config) # .vscode/settings.json, .nvim, etc. — editor-specific
├── .gitignore
├── README.md
└── src/
├── ReplicatedStorage/
│ ├── Shared/
│ │ ├── init.luau
│ │ └── ...
│ └── Client/ # (optional) client-only scripts via RunContext
├── ServerScriptService/
│ ├── init.server.luau
│ └── Systems/
└── StarterPlayer/
└── StarterPlayerScripts/
└── init.client.luau
Match src/ directory names to their DataModel mount points. Reading the path tells you exactly where the code runs.
Install via Rokit (don't pull from a global). Add to rokit.toml:
[tools]
wally = "UpliftGames/[email protected]"
Then rokit install. Commands:
wally init # scaffold wally.toml
wally install # download deps into Packages/
wally install --locked # CI; fail if lockfile is stale
wally update [name...] # bump deps
wally publish # push your package to the registry
wally search <query> # find packages
wally.toml[package]
name = "yourname/coolpkg" # SCOPE/NAME, lowercase, dashes ok
version = "1.0.0" # SemVer
license = "MIT" # SPDX
authors = ["Your Name <[email protected]>"]
realm = "shared" # "shared" or "server"
registry = "https://github.com/UpliftGames/wally-index"
[dependencies]
React = "jsdotlua/[email protected]"
Promise = "evaera/[email protected]"
[server-dependencies]
ProfileService = "loleris/[email protected]"
[dev-dependencies]
Jest = "jsdotlua/[email protected]"
shared — replicates to clients. Mount under ReplicatedStorage.server — server-only. Mount under ServerStorage or ServerScriptService.client realm exists. Client-only deps live in shared and run on the client by import."ReplicatedStorage": {
"$className": "ReplicatedStorage",
"Packages": {
"$path": "Packages"
}
},
"ServerScriptService": {
"$className": "ServerScriptService",
"Packages": {
"$path": "ServerPackages"
}
}
Packages get installed into separate folders by realm. Shared → Packages/, server → ServerPackages/, dev → DevPackages/.
wally.lock pins exact versions of every dep (transitive included). Commit it. wally install --locked in CI ensures everyone builds the same thing.
Full manifest fields, lockfile format, command list: references/wally-manifest.md.
Pesde is a newer Luau-first package manager — designed to prevent runtime lock-in. It targets multiple Luau runtimes (luau, lune, roblox, roblox_server) from one tool, and can pull from existing Wally registries via a bridge. Worth using when:
pesde.tomlname = "yourname/coolpkg"
version = "1.0.0"
license = "MIT"
[target]
environment = "roblox" # luau | lune | roblox | roblox_server
lib = "src/init.luau"
[indices]
default = "https://github.com/pesde-pkg/index"
[wally_indices]
default = "https://github.com/UpliftGames/wally-index"
[dependencies]
# Native pesde dep
hello = { name = "pesde/hello", version = "^1.0.0" }
# Wally dep via the bridge
React = { wally = "jsdotlua/react", version = "^17.1.0" }
[dev_dependencies]
Jest = { wally = "jsdotlua/jest", version = "^3.10.0" }
pesde init # scaffold pesde.toml
pesde add SCOPE/NAME # add + install a dep
pesde install # install all deps; writes pesde.lock
pesde run # run [scripts] entries (uses Lune)
pesde publish
roblox_packages/, luau_packages/, etc.).pesde.lock is the lockfile — commit it.rojo sourcemap.| Need | Choice |
|---|---|
| Mature, Roblox-only, large existing ecosystem | Wally |
| Multi-runtime (Lune CLI tools, Roblox, plain Luau) | Pesde |
| Cargo-style ergonomics | Wally |
| npm/pnpm-style ergonomics + Wally bridge | Pesde |
| Existing project on Wally with no pain | Stay |
Wally and Pesde aren't drop-in equivalents — manifest formats and resolution semantics differ. Don't switch mid-project without reason.
Luau LSP needs to know where files map to in the DataModel for proper autocomplete and require() resolution. Rojo generates the map.
# Watch mode — regenerates on filesystem change
rojo sourcemap --watch --output sourcemap.json default.project.json
# One-shot
rojo sourcemap --output sourcemap.json default.project.json
Then point luau-lsp at it. Editor-specific:
luau-lsp extension): "luau-lsp.sourcemap.sourcemapFile": "sourcemap.json" in .vscode/settings.json (or workspace-level config).--definitions=... / --sourcemap=...) or via your LSP client's settings; the luau-lsp binary documents the flags.luau-lsp analyze --sourcemap sourcemap.json .... Verify the exact flag against your installed luau-lsp --help.Regenerate after:
wally install (new package paths).default.project.json mounts.A common workflow: run rojo sourcemap --watch in a terminal alongside rojo serve. Both run for the duration of your session.
sourcemap.json is generated output — .gitignore it.
LSP setup details: references/luau-lsp-setup.md.
# stylua.toml
column_width = 100
line_endings = "Unix"
indent_type = "Tabs" # or "Spaces" with indent_width = 4
quote_style = "AutoPreferDouble"
call_parentheses = "Always"
[sort_requires]
enabled = true # alphabetize the require block; pairs with luau-expert guidance
Run via stylua src/. Most editors auto-format on save once the StyLua extension is installed.
# selene.toml
std = "roblox" # roblox stdlib definitions
# Optional: per-rule tweaks
[config]
incorrect_standard_library_use = "warn"
std = "roblox" is the key bit — without it, selene flags every Instance.new and task.wait. Run via selene src/.
luau-lsp powers in-editor type checking, hover info, autocomplete. Combine with --!strict per-file (see luau-expert skill) for max coverage.
Project-level config (.luaurc):
{
"languageMode": "strict",
"lint": {
"*": true
}
}
Verify the schema against luau.org/lint — .luaurc fields evolve.
A minimum-viable CI run:
rokit install) to install pinned tool versions.wally install --locked — fail if lockfile drift.stylua --check src/ — fail if formatting drift.selene src/ — lint.rojo sourcemap --output sourcemap.json default.project.json — generate map.luau-lsp analyze with the sourcemap — type-check. Exact flag depends on installed version (--sourcemap or --definitions); check luau-lsp --help.rojo build default.project.json --output build/place.rbxl — build artifact.place.rbxl as artifact / publish.Each step short-circuits the next. A green CI run means: pinned tools, locked deps, formatted, lint-clean, type-clean, builds.
GitHub Actions example sketched in examples/ci.yml.
.project.json field names from memory beyond name, tree, $className, $path, $properties. The full schema is short — check references/project-format.md before committing to less-common fields.^ default; specifics for ~, =, * should be checked against Wally docs before recommending non-default version specifiers.emitLegacyScripts default has shifted across releases. Confirm what version of Rojo the project uses (rojo --version) before assuming the Script vs Script(RunContext=Server) distinction..vscode/settings.json examples in references are illustrative; users on Neovim/Helix/Sublime configure equivalent fields.RunContext placement / replication / DataModel semantics belong to roblox-dev. This skill stops at "the file ends up at this DataModel path".| ❌ Don't | ✅ Do |
|---|---|
Globally-installed rojo / wally / stylua | Pin per-project via rokit.toml |
| New projects on Aftman or Foreman | Rokit (drop-in successor; community-maintained, faster) |
Hand-edit Packages/ | Treat as generated; wally install regenerates |
Commit Packages/ / ServerPackages/ / DevPackages/ | .gitignore them |
Commit sourcemap.json | .gitignore; regenerate on demand |
Forget wally.lock in git | Commit it; CI uses --locked |
Multiple init.* in one folder | One init script per dir; use submodules instead |
init.client.luau placed in ServerScriptService mount | Match script type to container |
selene without std = "roblox" | Always set; otherwise every Roblox API trips lint |
| Edit Studio + filesystem simultaneously | Filesystem is canonical; close Studio when not running rojo serve |
rojo build producing a place file checked in | CI artifact only; binary place files don't diff |
Mounting Packages only under ReplicatedStorage and using server deps | Mount ServerPackages separately under server containers |
Bare *.luau in a StarterPlayerScripts mount expecting client behavior | *.client.luau (legacy) or Script with RunContext=Client |
Manually rerun rojo sourcemap after every save | Use --watch in a long-running terminal |
wally publish from a dirty working tree | Tag a clean commit; publish from CI |
luau-lsp without sourcemap | Generate sourcemap; LSP requires it for require() resolution |
examples/default.project.json — typical place project covering all standard mounts.examples/wally.toml — annotated Wally manifest with [dependencies], [server-dependencies], [dev-dependencies].examples/pesde.toml — annotated Pesde manifest with [target], [indices], [wally_indices], [dependencies].examples/rokit.toml — toolchain pinning.examples/file-tree.md — full repo layout showing how filesystem maps to DataModel.examples/ci.yml — GitHub Actions workflow: install Rokit → wally install --locked → stylua check → selene → rojo build.npx claudepluginhub afrxo/roblox-agent-skills --plugin roblox-agent-skillsProvides behavioral guidelines to reduce common LLM coding mistakes, focusing on simplicity, surgical changes, assumption surfacing, and verifiable success criteria.
Searches, retrieves, and installs Agent Skills from prompts.chat registry using MCP tools like search_skills and get_skill. Activates for finding skills, browsing catalogs, or extending Claude.
Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.