Skip to content

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.

  • Every *.luau file under src/ and templates/. 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 .md under .claude/ (agent worktrees excluded), and every file under docs/agents/ and docs/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.

Terminal window
python tools/directives.py # the whole tree
python tools/directives.py src/Server/Game # report findings under these paths only
python tools/directives.py --rule CK004,CK005 # only these rules
python tools/directives.py --json # {count, scanned, findings: [{path, line, rule, message}]}
python tools/directives.py --list-rules

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

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 owns wait, spawn and delay),
  • Instance.new of 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 (Motor6D is a Motor), so IsA checks stay legal,
  • GetService or FindService of a name that is not an engine service, or a deprecated one,
  • Instance.new of a class that does not exist, is deprecated or is not creatable,
  • class names passed to IsA, FindFirstChildOfClass, FindFirstChildWhichIsA, FindFirstAncestorOfClass, FindFirstAncestorWhichIsA or compared with .ClassName, when the class does not exist or is deprecated,
  • GetPropertyChangedSignal("X") when no class has a property X, or every class’s X is deprecated,
  • Enum.X and Enum.X.Y when 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 humanoid pins the class, so humanoid:LoadAnimation(...) is flagged although Animator:LoadAnimation is live. game, CurrentCamera and LocalPlayer map to DataModel, Camera and Player.

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.

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 Deadline

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

tools/test_directives.py exercises every rule, suppression parsing and the prose check. It runs in the tooling step of the check.

ChloeKernel v0.9.1. Access to the framework is private and granted individually by Chloe. Games built with it credit CryptedChloes in their description.