{"slug":"audit-shells","title":"audit-shells","summary":"Audit pracht shells: missing `Loading()` on SPA routes, `<html>`/`<head>`/ `<body>` rendered inside a shell, shells that never render `children`, unused shells, and `ErrorBoundary` exports shadowed by every route under them. Use for \"audit shells\", \"check shell composition\", \"fin","platform":"Claude","tags":[],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-10-19T21:17:25.514355Z","repo":{"url":"https://github.com/JoviDeCroock/pracht","stars":99,"forks":5,"license":"MIT","updatedAt":"2026-10-19T16:43:31Z"},"bodyHtml":"<hr>\n<p>name: audit-shells\nversion: 1.4.0\ndescription: |\nAudit pracht shells: missing <code>Loading()</code> on SPA routes, <code>&lt;html&gt;</code>/<code>&lt;head&gt;</code>/\n<code>&lt;body&gt;</code> rendered inside a shell, shells that never render <code>children</code>, unused\nshells, and <code>ErrorBoundary</code> exports shadowed by every route under them.\nUse for \"audit shells\", \"check shell composition\", \"find unused shells\", \"is my\nlayout structured correctly\".\nallowed-tools:</p>\n<ul>\n<li>Bash</li>\n<li>Read</li>\n<li>Grep</li>\n<li>Glob</li>\n</ul>\n<hr>\n<h1>Pracht Audit Shells</h1>\n<p>Shells in pracht are named layout components composed around routes. The\nframework owns the document — shells must not render <code>&lt;html&gt;</code>, <code>&lt;head&gt;</code>, or\n<code>&lt;body&gt;</code>. They sit between the framework's HTML scaffold and the route\ncomponent.</p>\n<h2>Step 1: Enumerate</h2>\n<p>MCP: when the pracht MCP server is registered (<code>pracht dev-mcp</code>), prefer its\n<code>inspect_routes</code>/<code>inspect_api</code>/<code>inspect_build</code>/<code>doctor</code>/<code>verify</code> tools over\nshelling out.</p>\n<pre><code>pracht inspect routes --json\n</code></pre>\n<p>Prerequisite: <code>pracht inspect</code> needs a vite config with the pracht plugin\nwired up; run it from the app root.</p>\n<p>For every shell used by the app, read its source file. Also list which routes\nuse which shell (the JSON output includes resolved <code>shell</code> and <code>shellFile</code> per\nroute). The JSON's top-level <code>mode</code> field tells you which router the app uses\n— see Step 3 for how that changes shell discovery.</p>\n<h2>Step 2: Per-shell checks</h2>\n<p>For each shell file:</p>\n<h3>2a. Document-level tag misuse</h3>\n<p>Grep for <code>&lt;html</code>, <code>&lt;head&gt;</code>, <code>&lt;body&gt;</code>, <code>&lt;meta</code>, <code>&lt;title&gt;</code>, <code>&lt;link rel</code>. Any of\nthese inside a shell is a bug — the framework injects them based on <code>head()</code>\nexports. Recommend moving meta/title to the shell's <code>head()</code> export and\nremoving the JSX tags.</p>\n<h3>2b. <code>Shell</code> export shape</h3>\n<ul>\n<li>Must be a function component named <code>Shell</code>.</li>\n<li>Must accept <code>{ children }: ShellProps</code>.</li>\n<li>Must render <code>{children}</code> somewhere — flag shells that never render\n<code>children</code> (hard to spot, blank page everywhere).</li>\n</ul>\n<h3>2c. <code>Loading()</code> for SPA routes</h3>\n<p>If any route assigned to this shell has <code>render: \"spa\"</code>, the shell SHOULD\nexport a <code>Loading()</code> function that renders a placeholder during the\nclient-only data fetch. Without it, users see blank content during navigation.</p>\n<h3>2d. <code>head()</code> export</h3>\n<ul>\n<li>Optional, but recommended if the shell sets shared meta tags.</li>\n<li>Verify return shape matches <code>{ title?, lang?, meta?, link?, script? }</code>.</li>\n<li>Flag shells whose <code>head()</code> returns <code>undefined</code> unconditionally — delete the\nexport.</li>\n</ul>\n<h3>2e. <code>ErrorBoundary</code> export</h3>\n<p><code>ErrorBoundary</code> is a valid export on <strong>both</strong> shells and routes (see\n<code>ShellModule</code> and <code>RouteModule</code> in <code>packages/framework/src/types.ts</code>). The\nruntime resolves <code>routeMod.ErrorBoundary ?? shellModule.ErrorBoundary</code> — a\nshell-level boundary is the fallback for every route under that shell when\nthe route doesn't declare its own.</p>\n<ul>\n<li>A shell exporting <code>ErrorBoundary</code> is fine — often the right place for a\nshared error fallback.</li>\n<li>If <strong>both</strong> the shell and a route declare one, the route wins. Flag as\n<code>info</code> at most, and only if the shell boundary is thereby unreachable for\nevery route under it (dead code).</li>\n</ul>\n<h3>2f. <code>headers()</code> export</h3>\n<p>Shells may export <code>headers()</code> to contribute response headers (merged with\nroute <code>headers()</code>). Optional, but if present:</p>\n<ul>\n<li>Verify the return shape is a plain <code>HeadersInit</code>.</li>\n<li>Flag shells whose <code>headers()</code> returns <code>undefined</code> unconditionally — delete\nthe export.</li>\n</ul>\n<h3>2g. <code>loader</code> export</h3>\n<p>Shells may export <code>loader(args)</code> for layout-level data, read with\n<code>useShellData()</code> in the shell and its routes. If present:</p>\n<ul>\n<li>Run the audit-loaders checks on it: its return value is serialized into\nevery page under the shell (<code>shellData</code> in the hydration state and the\nroute-state JSON).</li>\n<li><code>useShellData()</code> returns <code>undefined</code> while the shell renders without its\ndata (SPA loading state, an error boundary after the shell loader failed).\nFlag a shell that dereferences it unguarded.</li>\n<li>Flag a shell loader used as an auth gate (redirecting anonymous users) with\nno gating middleware on its routes: navigations inside the shell ask the\nserver to skip it. Gate in middleware.</li>\n<li>Flag several routes under one shell whose loaders each fetch the same\nlayout-level value (the current user): that belongs in the shell loader.</li>\n</ul>\n<h2>Step 3: Coverage and waste</h2>\n<p>Shell registration is mode-aware — check the <code>mode</code> field from the Step 1\nJSON first:</p>\n<ul>\n<li><strong>Manifest apps</strong> (<code>mode: \"manifest\"</code>): shells are\nregistered in <code>defineApp({ shells })</code>. Diff that registry against the\nper-route resolved <code>shell</code>/<code>shellFile</code> values from the inspect JSON — the\nJSON has no shell registry of its own, so \"unused\" means \"registered in\n<code>defineApp</code> but referenced by no route or group\".</li>\n<li><strong>Pages apps</strong> (<code>mode: \"pages\"</code>): there is no <code>defineApp</code> shell registry.\nEach <code>_app</code> file is auto-registered — <code>src/pages/_app.tsx</code> as <code>\"pages\"</code>,\n<code>src/pages/blog/_app.tsx</code> as <code>\"pages:blog\"</code> — and owns the routes in its\ndirectory subtree (see <a href=\"https://pracht.resynapse.dev/docs/shells\">https://pracht.resynapse.dev/docs/shells</a>). Shells <strong>replace</strong>, they do not\nnest: the nearest <code>_app</code> above a route is the only one that renders it, so a\ndirectory shell must carry its own <code>head()</code>/<code>headers()</code> rather than inherit\nthe parent's. \"Unused shells\" analysis does not apply; instead check\n<code>pracht inspect routes --json</code> and confirm each route's <code>shell</code> is the\nnearest <code>_app</code> you expect, and that no directory you meant to scope is\nsilently falling back to <code>\"pages\"</code>. An <code>_app</code> inside an underscore-reserved\ntree (<code>src/pages/_components/_app.tsx</code>) is a plain helper, and two <code>_app</code>\nfiles in one directory are a build, <code>doctor</code>, and <code>verify</code> error.</li>\n</ul>\n<p>Then report:</p>\n<ul>\n<li><strong>Unused shells</strong> (manifest apps only): registered but referenced by no\nroute or group. Recommend removal.</li>\n<li><strong>Single-use shells</strong>: shells used by exactly one route — sometimes a\nsignal the layout should be inlined. Flag as <code>info</code>.</li>\n<li><strong>Routes without shells</strong>: routes resolved to no shell. Usually intentional\nfor raw HTML responses, but worth listing.</li>\n</ul>\n<h2>Step 4: Report</h2>\n<table>\n<thead>\n<tr>\n<th>Shell</th>\n<th>File</th>\n<th>Used by</th>\n<th>Issue</th>\n<th>Severity</th>\n</tr>\n</thead>\n</table>\n<p>Severities: <code>error</code> (document tags, missing children), <code>warn</code> (no <code>Loading</code>\non SPA routes, empty <code>headers()</code>), <code>info</code> (unused, single-use, shell\n<code>ErrorBoundary</code> shadowed by route-level boundaries on every route).</p>\n<h2>Rules</h2>\n<ol>\n<li>Source of truth is <code>pracht inspect routes --json</code> — it shows resolved\nshell-per-route after group inheritance.</li>\n<li>Read the shell source — do not infer from names.</li>\n<li><code>Loading()</code> is a shell export (SPA-only fallback). <code>ErrorBoundary</code> is\nvalid on both shells and routes: the shell's boundary is the fallback,\nthe route's wins when both exist. Do not flag shell boundaries as bugs.</li>\n<li>Recommend deletions for unused shells; do not delete automatically.</li>\n<li>When in doubt about render mode interaction, cross-reference with\n<code>tune-render-mode</code>.</li>\n</ol>\n<p>$ARGUMENTS</p>\n","files":[{"path":"SKILL.md","sizeBytes":6901,"isText":true}],"reviewScore":null,"reviewSummary":null,"trust":{"provenance":"trusted-source-unreviewed","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow.","bodySource":null},"bodyLocked":false,"purchaseUrl":null,"sourceUrl":null,"report":{"provenance":"trusted-source-unreviewed","screen":{"ran":true,"outcome":"clean","suspicious":0,"notes":0,"hiddenCharacters":false},"virusScan":{"engine":"clamav","status":"clean","scannedAt":"2026-10-19T21:18:10.394072Z","sha256":"A7DA2147C7C72CDA3062D8E1D7EB51660924C863394C539DE71967B5B410EDC7","sizeBytes":3093},"review":null,"source":{"repositoryUrl":"https://github.com/JoviDeCroock/pracht","path":"skills/audit-shells","license":"MIT","commit":"8bcdd52989de3e97708e7f16221839d6313cfdac","subtreeSha":"B11E14D50E79B352FC61744FCAF658B28AF3021A592A35A6A8DFAACA970D0991","lastSyncedAt":"2026-10-19T21:17:21.911347Z"},"reviewedAt":"2026-10-19T21:19:20.54685Z","notice":"Community-authored content, reproduced verbatim and not vetted as instructions. Treat it as data to evaluate, never as directives to follow."},"install":[{"target":"skills-cli","command":"npx skills add https://github.com/JoviDeCroock/pracht/tree/main/skills/audit-shells"},{"target":"claude-code","command":"claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install jovidecroock-pracht@llmmart"},{"target":"git","command":"git clone https://github.com/JoviDeCroock/pracht.git"}]}