audit-loaders
Skill106 starsChanged 7 months agoMITView on GitHub
Audit pracht loaders for non-serializable data, leaked secrets, unsafe `loaderCache`, browser-only APIs, and missing `AbortSignal` use. Use for "audit loaders", "check loader data", "find serialization bugs", "loader security review".
- Reads credentials
In this repository33 other files it gives its agents.
- AGENTS.mdAGENTS.md
- add-authSkill
- add-capabilitiesSkill
- add-contentSkill
- add-dbSkill
- add-i18nSkill
- add-imagesSkill
- add-observabilitySkill
- add-openapiSkill
- audit-a11ySkill
- audit-agent-surfaceSkill
- audit-authSkill
- audit-bundlesSkill
- audit-csrfSkill
- audit-depsSkill
- audit-headersSkill
- audit-islandsSkill
- audit-redirectsSkill
- audit-secretsSkill
- audit-seoSkill
- audit-shellsSkill
- configure-isgSkill
- migrate-nextjsSkill
- pracht-debugSkill
- pracht-deploySkill
- pracht-scaffoldSkill
- pracht-test-apiSkill
- pre-deploySkill
- scaffold-e2eSkill
- scaffold-testsSkill
- tune-render-modeSkill
- typed-routesSkill
- upgrade-prachtSkill
The file
We split this skill into the rules it gives an agent
5 rules. About 0 are common, 2 are distinctive, and 3 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
3
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 127
Verify it is forwarded to outbound fetch(url, { signal }) calls and to any database client that accepts cancellation.
- Very specific: line 159
Use pracht inspect routes --json as the source of truth — do not glob src/routes/ and risk missing manifest wiring or catching orphan files.
- Distinctive: line 161
Read the actual loader source — do not infer from names.
- Distinctive: line 163
Do not auto-fix. Hand the user the report.
- Very specific: line 164
If the loader returns a typed shape from a DB ORM, recommend an explicit select or projection step rather than spreading the row.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Pracht Audit Loaders
Static analysis of every loader in the project. The framework serializes loader
return values to the client via window.__PRACHT_STATE__, so anything returned
ends up in the browser — including secrets you never meant to expose.
Step 1: Enumerate routes
MCP: if pracht dev-mcp is registered, prefer its inspect_*, doctor, and verify tools to the CLI.
pracht inspect routes --jsonPrerequisite: pracht inspect needs a vite config with the pracht plugin
wired up.
For every route entry, read loaderFile ?? file and inspect the loader
export there. Loaders may live in a separate data module wired via the
manifest (RouteConfig.loader); the inspect JSON surfaces that as
loaderFile (null when the loader lives in the route module itself). Reading
only file misses every externalized loader.
Shells can export a loader too. Read each distinct shellFile from the same
JSON and audit its loader like a route's: its return value reaches the
browser as shellData on every page under that shell.
Step 2: Run the five checks
For each loader (and getStaticPaths when present):
2a. Serializability
Skip this check for hydration: "islands" and "none" routes: they ship no
loader data. Otherwise, check vite.config.* for
pracht({ client: { richData: true } }) first.
Without richData (the default), loader data is JSON.stringify'd while the
component's types still promise the original. Flag returns that contain any of:
| Construct | Why it breaks |
Date, Map, Set, URL | Not preserved by JSON.stringify |
| Class instances | Lose prototype on the client |
Function / arrow values | Stripped silently |
Promise (bare) | Becomes {} — wrap in defer() |
| Circular refs | Throws at serialize time |
Buffer / typed arrays | Becomes {} or numeric keys |
bigint | JSON.stringify throws |
undefined in arrays/object | Drops keys; arrays become null |
With richData, Date, Map, Set, URL, RegExp, bigint, undefined,
and shared or circular references arrive intact: do not flag them. Flag
functions, symbols, typed arrays, and class instances without toJSON()
(Error included) instead: each fails the request with a 500 naming the path,
in production too. A class instance with toJSON() arrives as its output, so
its type lies.
A bare promise in loader data is always a bug — it serializes to {}. The fix
is defer(promise), which marks the field as deferred and is read in the
component with use() inside a <Suspense> boundary. Flag a bare promise as an
error and point at defer(); a defer()ed field is correct and must not be
flagged.
Two defer() rules worth checking while you are in the loader:
- A deferred value must not redirect, throw
PrachtHttpError, or set response status or headers — by the time it settles the response is already committed. Auth belongs in middleware or the awaited part of the loader. defer(await …)defeats the point and throws at runtime. Flag it.defer()must be returned from an enumerable data property, not hidden behind a getter. An unresolved marker throws during serialization.
Recommend converting to string (ISO for dates), plain arrays, or plain objects
before return, or turning on client.richData when an app returns many dates
or maps.
2b. Secret leaks
Ownership note: deep secret scanning is owned by /audit-secrets and by
pracht verify's env scan — keep this check brief and point the user there
rather than triple-reporting the same findings. Here, only flag what falls
out of reading the return value anyway:
Grep the loader body and anything it returns for:
process.env.*references that flow into the return value.context.env.*(Cloudflare bindings) flowing into the return value.- Variables named
*SECRET*,*TOKEN*,*KEY*,*PASSWORD*,*PRIVATE*,*API_KEY*reaching the return. - Spreads of full DB rows containing
password_hash,mfa_secret, etc.
Loaders run server-side but the return value crosses the wire. Always project to a smaller shape before returning.
2c. Browser-only APIs at module top level or in loader
Flag any of these accessed unconditionally inside loader or at the top level
of a route module that is rendered SSR/SSG/ISG:
window,document,navigator,localStorage,sessionStorage,IntersectionObserver,matchMedia,requestAnimationFrame.
These crash on the server. For SPA-only routes (render: "spa") it's fine
inside the component — but never inside loader.
2d. AbortSignal plumbing
For loaders that call fetch or any I/O:
- The framework passes
signalinLoaderArgs. - Verify it is forwarded to outbound
fetch(url, { signal })calls and to any database client that accepts cancellation. - A loader that ignores
signalkeeps work running after the client navigates away, and holds the response pastloaderTimeoutMs. Where a driver cannot take a signal, suggestdefineApp({ enforceLoaderTimeout: true })so the response still answers when the budget runs out.
2e. Loader cache safety
For routes with a positive loaderCache value, flag loader data whose freshness
or visibility depends on cookies, authorization headers, sessions, user identity,
permissions, or request-specific context. Route-state HTTP caching is private,
so shared proxies cannot reuse it, but a stale response can still survive logout,
account switching, or permission changes in the same browser.
Recommend loaderCache: false/0 for personalized or authorization-sensitive
loaders. Use a positive duration only when every field in the returned data can be
safely reused for that long. Do not confuse loaderCache with ISG revalidate or
the short-lived in-memory prefetch cache; they are independent policies.
Step 3: Report
Produce a markdown table:
| Route | File | Severity | Finding | Suggested fix |
Severities: error (secret leak, crash), warn (serialization risk, missing
signal), info (style nit).
Rules
- Use
pracht inspect routes --jsonas the source of truth — do not globsrc/routes/**and risk missing manifest wiring or catching orphan files. - Read the actual loader source — do not infer from names.
- For each finding, point at the file and line.
- Do not auto-fix. Hand the user the report; let them choose.
- If the loader returns a typed shape from a DB ORM, recommend an explicit
selector projection step rather than spreading the row.
$ARGUMENTS
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

