audit-shells
Skill106 starsChanged 7 months agoMITView on GitHub
Audit pracht shells: missing `Loading()` on SPA routes, document tags inside a shell, shells that drop `children`, unused shells, and shadowed `ErrorBoundary` exports. Use for "audit shells", "check shell composition", "find unused shells", "is my layout structured correctly".
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-loadersSkill
- audit-redirectsSkill
- audit-secretsSkill
- audit-seoSkill
- 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
9 rules. About 0 are common, 1 is distinctive, and 8 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
8
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 53
Must be a function component named Shell.
- Very specific: line 55
Must render {children} somewhere — flag shells that never render children (hard to spot, blank page everywhere).
- Very specific: line 60
If any route assigned to this shell has render: "spa", the shell SHOULD export a Loading() function that renders a placeholder during the …
- Very specific: line 67
Verify return shape matches { title?, lang?, meta?, link?, script?
- Very specific: line 81
If both the shell and a route declare one, the route wins.
- Very specific: line 89
Verify the return shape is a plain HeadersInit.
- Very specific: line 97
Run the audit-loaders checks on it: its return value is serialized into every page under the shell (shellData in the hydration state and …
- Distinctive: line 154
Read the shell source — do not infer from names.
- Very specific: line 159
When in doubt about render mode interaction, cross-reference with tune-render-mode.
≈ “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 Shells
Shells in pracht are named layout components composed around routes. The
framework owns the document — shells must not render <html>, <head>, or
<body>. They sit between the framework's HTML scaffold and the route
component.
Step 1: Enumerate
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; run it from the app root.
For every shell used by the app, read its source file. Also list which routes
use which shell (the JSON output includes resolved shell and shellFile per
route). The JSON's top-level mode field tells you which router the app uses
— see Step 3 for how that changes shell discovery.
Step 2: Per-shell checks
For each shell file:
2a. Document-level tag misuse
Grep for <html, <head>, <body>, <meta, <title>, <link rel. Any of
these inside a shell is a bug — the framework injects them based on head()
exports. Recommend moving meta/title to the shell's head() export and
removing the JSX tags.
2b. Shell export shape
- Must be a function component named
Shell. - Must accept
{ children }: ShellProps. - Must render
{children}somewhere — flag shells that never renderchildren(hard to spot, blank page everywhere).
2c. Loading() for SPA routes
If any route assigned to this shell has render: "spa", the shell SHOULD
export a Loading() function that renders a placeholder during the
client-only data fetch. Without it, users see blank content during navigation.
2d. head() export
- Optional, but recommended if the shell sets shared meta tags.
- Verify return shape matches
{ title?, lang?, meta?, link?, script? }. - Flag shells whose
head()returnsundefinedunconditionally — delete the export.
2e. ErrorBoundary export
ErrorBoundary is a valid export on both shells and routes (see
the ShellModule and RouteModule types exported by @pracht/core). The
runtime resolves routeMod.ErrorBoundary ?? shellModule.ErrorBoundary — a
shell-level boundary is the fallback for every route under that shell when
the route doesn't declare its own.
- A shell exporting
ErrorBoundaryis fine — often the right place for a shared error fallback. - If both the shell and a route declare one, the route wins. Flag as
infoat most, and only if the shell boundary is thereby unreachable for every route under it (dead code).
2f. headers() export
Shells may export headers() to contribute response headers (merged with
route headers()). Optional, but if present:
- Verify the return shape is a plain
HeadersInit. - Flag shells whose
headers()returnsundefinedunconditionally — delete the export.
2g. loader export
Shells may export loader(args) for layout-level data, read with
useShellData() in the shell and its routes. If present:
- Run the audit-loaders checks on it: its return value is serialized into
every page under the shell (
shellDatain the hydration state and the route-state JSON). useShellData()returnsundefinedwhile the shell renders without its data (SPA loading state, an error boundary after the shell loader failed). Flag a shell that dereferences it unguarded.- Flag a shell loader used as an auth gate (redirecting anonymous users) with no gating middleware on its routes: navigations inside the shell ask the server to skip it. Gate in middleware.
- Flag several routes under one shell whose loaders each fetch the same layout-level value (the current user): that belongs in the shell loader.
Step 3: Coverage and waste
Shell registration is mode-aware — check the mode field from the Step 1
JSON first:
- Manifest apps (
mode: "manifest"): shells are registered indefineApp({ shells }). Diff that registry against the per-route resolvedshell/shellFilevalues from the inspect JSON — the JSON has no shell registry of its own, so "unused" means "registered indefineAppbut referenced by no route or group". - Pages apps (
mode: "pages"): there is nodefineAppshell registry. Each_appfile is auto-registered —src/pages/_app.tsxas"pages",src/pages/blog/_app.tsxas"pages:blog"— and owns the routes in its directory subtree (see https://pracht.resynapse.dev/docs/shells). Shells replace, they do not nest: the nearest_appabove a route is the only one that renders it, so a directory shell must carry its ownhead()/headers()rather than inherit the parent's. "Unused shells" analysis does not apply; instead checkpracht inspect routes --jsonand confirm each route'sshellis the nearest_appyou expect, and that no directory you meant to scope is silently falling back to"pages". An_appinside an underscore-reserved tree (src/pages/_components/_app.tsx) is a plain helper, and two_appfiles in one directory are a build,doctor, andverifyerror.
Then report:
- Unused shells (manifest apps only): registered but referenced by no route or group. Recommend removal.
- Single-use shells: shells used by exactly one route — sometimes a
signal the layout should be inlined. Flag as
info. - Routes without shells: routes resolved to no shell. Usually intentional for raw HTML responses, but worth listing.
Step 4: Report
| Shell | File | Used by | Issue | Severity |
Severities: error (document tags, missing children), warn (no Loading
on SPA routes, empty headers()), info (unused, single-use, shell
ErrorBoundary shadowed by route-level boundaries on every route).
Rules
- Source of truth is
pracht inspect routes --json— it shows resolved shell-per-route after group inheritance. - Read the shell source — do not infer from names.
Loading()is a shell export (SPA-only fallback).ErrorBoundaryis valid on both shells and routes: the shell's boundary is the fallback, the route's wins when both exist. Do not flag shell boundaries as bugs.- Recommend deletions for unused shells; do not delete automatically.
- When in doubt about render mode interaction, cross-reference with
tune-render-mode.
$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.

