Directives
The static linter behind the check, rules CK000 to CK017, what each one rejects and the fix, and when a ck-allow suppression is acceptable.
tools/directives.py enforces the repository rules a compiler cannot: strict mode, no polling or legacy scheduling, current engine APIs, no raw remotes or direct DataStore access in game code, validated channels, a consistent service registry, Logger-only warnings in the framework, no em dashes in agent prose or runtime strings. It is plain Python with no dependencies, matches source text with regular expressions, and scans the whole tree (296 files at 0.9.1) in about a second. It runs as the directives step of Check and, in Claude Code, on every Luau file an agent edits. CK014 and CK015 read the pinned engine reference.
What it scans
Section titled “What it scans”- Every
*.luaufile undersrc/andtemplates/. Before matching, comments are blanked, and a second copy also blanks string contents, so a pattern inside a comment never counts and each rule chooses whether strings count. - The agent prose:
AGENTS.md,CLAUDE.md, every.mdunder.claude/(agent worktrees excluded), and every file underdocs/agents/anddocs/game/.
Rules apply by scope, decided from the path:
| Scope | Paths |
|---|---|
| Framework | src/Shared/ChloeKernel/, src/Server/ChloeKernelServer/ |
| Game | src/<side>/Game/, templates/ |
| Tests | any path containing /Tests/, /TestKit/ or /Benches/, and any .spec.luau file |
A file can be in more than one scope. A game spec in src/Server/Game/Tests is both game and tests.
Running it
Section titled “Running it”python tools/directives.py # the whole treepython tools/directives.py src/Server/Game # report findings under these paths onlypython tools/directives.py --rule CK004,CK005 # only these rulespython tools/directives.py --json # {count, scanned, findings: [{path, line, rule, message}]}python tools/directives.py --list-rulesEach finding prints as path:line: CKxxx message with a repository-relative path, then a summary: directives: N finding(s) in M file(s); K files scanned, or directives: clean (K files scanned). The exit status is 0 when clean, 1 with findings and 2 on a usage error. --root DIR points it at another checkout.
| Rule | Applies to | Rejects | Fix |
|---|---|---|---|
| CK000 | Everything | A ck-allow whose reason is missing or shorter than 8 characters. It also suppresses nothing. |
Write the reason. |
| CK001 | All Luau | A first line other than --!strict. Tests may use --!nonstrict. |
Start the file with --!strict. |
| CK002 | All Luau | Bare wait(, spawn(, delay(, tick( calls. Methods, fields, locals of the same name and function definitions are ignored. |
task.wait, task.spawn, task.delay, os.clock. |
| CK003 | All Luau | Superseded APIs: RenderStepped, RunService.Stepped, FilterDescendantsInstances, :AddToFilter(, assigning .FilterType, HeartbeatTimeMs, PhysicsStepTimeMs, :ReserveServer(, PhysicsService collision-group methods, :GetProductInfo(, :PlayerOwnsAsset(, :LoadCharacter(, :ApplyDescription(. |
The replacement in the message: PreRender or BindToRenderStep, PreSimulation, ExcludeInstances/IncludeInstances, the seconds Stats properties, the *Async versions, the Workspace collision-group methods. |
| CK004 | All non-test Luau | task.wait inside a while or repeat loop: a polling loop. |
Wait on an event or a Scheduler deadline. |
| CK005 | Game code | Any task.wait. |
kernel.Scheduler:after, Process.sleep or an event. |
| CK006 | Game code | .Heartbeat, .PreRender, .PreSimulation or .PostSimulation with :Connect, :ConnectParallel or :Once. |
kernel.Scheduler:every or kernel:phaseScheduler, connected only while active. |
| CK007 | Game code | Instance.new of RemoteEvent, RemoteFunction, UnreliableRemoteEvent, BindableEvent or BindableFunction. |
A channel in ReplicatedStorage.Game.Channels, or kernel.Bus. |
| CK008 | Game code, and receipts everywhere outside Commerce/ |
GetService of DataStoreService, MemoryStoreService or MessagingService in game code. ProcessReceipt or BindReceiptHandler anywhere outside src/Server/ChloeKernelServer/Commerce/, as a name or a string. |
DataDriver, MemoryDriver, MessageDriver, and Receipts for purchases. A bound receipt handler takes precedence over ProcessReceipt and would bypass the Receipts ledger. |
| CK009 | Non-test src/Shared and src/Client |
Any reference to ServerScriptService or ServerStorage, as a name or a string. |
Shared and client code cannot see server containers. Move the code, or send what it needs over a channel. |
| CK010 | src/Server/Game/Registry.luau and Services/, src/Client/Game/Registry.luau and Controllers/ |
A module in Services or Controllers that Registry.luau does not require, a require that is duplicated or resolves to nothing, a missing or mismatched Name field, or a name that does not end in Service or Controller. |
Add require(script.Parent.Services.<Name>) to the registry and make Name equal the module name. |
| CK011 | All Luau | A file name that is not PascalCase (after stripping .server, .client, .spec, .bench; init is allowed), or a directory under src/ that is not PascalCase. |
Rename it. |
| CK012 | src/Shared/Game/Channels.luau |
A channel name with characters outside letters, digits, ., /, -, _, longer than 94 characters, or starting with RBX (names become attribute names behind a prefix of up to 6 characters). An Intent, PredictedIntent or Request channel without RateLimit, or without Validate rules or Open = true. Validate = {} counts as no rules (“checks nothing: add rules or Open = true”). |
Rename the channel; add RateLimit and Validate (or Open = true with a reason). |
| CK013 | Agent prose, and string literals in all Luau | An em dash in prose, or inside a Luau string literal (warnings, errors, UI text, test names), since those reach players, the Output window and logs. Comments are not checked. | A colon, a comma, parentheses or a new sentence. |
| CK014 | All Luau | A deprecated or nonexistent engine name, checked against tools/engine-api.json. Details below. |
The replacement the message names. Look it up with python tools/engine.py. |
| CK015 | All Luau | GetService of a plugin-only service (every one of its methods needs Plugin security or stricter), and calls to settings(). |
Move the code into a Studio plugin. Game scripts cannot run it. |
| CK016 | Non-test framework modules | A raw warn( call. Logger.luau and the Packet port under Net/Packet/ are exempt. |
Logger.scope("<Module>").warn(...), so sinks see it and specs can expect it with T.expectWarning. |
| CK017 | All Luau | In src/Shared/ChloeKernel/Debug/NetTap.luau: a NetTap.OwnerUserIds = { ... } table without [7241279999] = true (CryptedChloes, the framework’s only developer), or a missing local FrameworkOwner = 7241279999 check (reported on line 1). In any other file: assigning OwnerUserIds (replacing the table), setting OwnerUserIds[7241279999] to nil or false, table.clear on it, or the same removal through rawset. Comments and strings do not count. |
Keep the entry and the check. Other debug operators are added with NetTap.OwnerUserIds[userId] = true. |
What CK014 checks
Section titled “What CK014 checks”CK014 reads tools/engine-api.json once and flags, per line:
- a deprecated global function:
getfenv,setfenv,ypcall,elapsedTime,stats,collectgarbage,printidentity,version,DebuggerManager(CK002 already ownswait,spawnanddelay), Instance.newof a legacy class the reference does not tag deprecated:Motor,VelocityMotor,Accoutrement,BlockMesh,FileMesh,CharacterMesh,AdvancedDragger. Only creation is flagged, because live classes inherit some of them (Motor6Dis aMotor), soIsAchecks stay legal,GetServiceorFindServiceof a name that is not an engine service, or a deprecated one,Instance.newof a class that does not exist, is deprecated or is not creatable,- class names passed to
IsA,FindFirstChildOfClass,FindFirstChildWhichIsA,FindFirstAncestorOfClass,FindFirstAncestorWhichIsAor compared with.ClassName, when the class does not exist or is deprecated, GetPropertyChangedSignal("X")when no class has a propertyX, or every class’sXis deprecated,Enum.XandEnum.X.Ywhen the enum or item does not exist, or is deprecated,- a
:Method(call or an.Event:Connect(whose name is deprecated on every class that has it, - a member deprecated only on the class a receiver is named after. A local called
humanoidpins the class, sohumanoid:LoadAnimation(...)is flagged althoughAnimator:LoadAnimationis live.game,CurrentCameraandLocalPlayermap toDataModel,CameraandPlayer.
When tools/engine-api.json is absent, CK014 and CK015 skip silently.
At 0.9.1 CK015’s plugin-only list holds 13 services, among them ChangeHistoryService, Selection, StudioService and ScriptEditorService. StudioTestService is not on it, because GetTestArgs and EndTest need no plugin security; that is how the spec gate ends harness sessions from the server.
Suppressions
Section titled “Suppressions”Suppress a finding with a comment that names the rule and gives a reason of at least 8 characters:
task.wait(0.25) -- ck-allow CK004: retry backoff between failed shutdown saves, bounded by DeadlineA trailing comment on a code line applies to that line. A comment on a line of its own applies to itself and the next line. One comment can name several rules: -- ck-allow CK004, CK005: <reason>. In agent prose, use an HTML comment, which covers its own line and the next: <!-- ck-allow CK013: quoting a title verbatim -->. A suppression without a real reason is reported as CK000 and suppresses nothing.
AGENTS.md sets the bar: never add a suppression to get green, and give each one a reason a reviewer would accept. The review-game skill removes game-code suppressions that fail that bar. The framework’s own suppressions show what passes:
| Where | Rule | Why it is acceptable |
|---|---|---|
Scheduler.luau |
CK003 | Reads HeartbeatTimeMs and PhysicsStepTimeMs only as the fallback for engines before 0.649, which lack the seconds properties. |
Data/init.luau |
CK004 | The shutdown drain inside BindToClose and its retry backoff are bounded by a deadline, and the lock-wait backoff is bounded and runs only when no handoff channel reaches the lock holder. |
ActorPool/Worker.server.luau |
CK016 | The pool Actor runs in its own VM, which the main VM’s Logger sinks never reach. |
Tests/Commerce.spec.luau |
CK008 | A MarketplaceService double that records what Receipts binds. |
Each one names a constraint the rule’s author could not see from the text: an old engine, a shutdown path with a deadline, a separate VM, a double. “It works” or “needed here” is not a reason.
Gotchas
Section titled “Gotchas”tools/test_directives.py exercises every rule, suppression parsing and the prose check. It runs in the tooling step of the check.