{"slug":"audit-headers","title":"audit-headers","summary":"Audit security headers in a pracht app. The framework sets four defaults on every response path; this covers the exceptions — static output served outside first-party adapters, `headers()` exports that weaken defaults, and the choices only you can make (HSTS, CSP). Use for \"audit","platform":"Claude","tags":[],"authorName":"LLM Mart","authorSlug":"llm-mart","score":0,"source":"github","price":null,"verified":false,"createdAt":"2026-10-19T21:17:24.32627Z","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-headers\nversion: 1.2.1\ndescription: |\nAudit security headers in a pracht app. The framework sets four defaults on\nevery response path; this covers the exceptions — static output served outside\nfirst-party adapters, <code>headers()</code> exports that weaken defaults, and the choices\nonly you can make (HSTS, CSP).\nUse for \"audit security headers\", \"check CSP\", \"harden headers\", \"set up HSTS\",\n\"review header policy\".\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 Headers</h1>\n<p>What the framework guarantees: four default security headers are applied\nautomatically on EVERY framework response path — SSR pages, API responses,\n404s, and static/ISG output on the first-party adapters (<code>adapter-node</code>,\n<code>adapter-cloudflare</code>) as well as the Vercel headers config generated at build\ntime:</p>\n<ul>\n<li><code>permissions-policy</code> (disables device sensors)</li>\n<li><code>referrer-policy: strict-origin-when-cross-origin</code></li>\n<li><code>x-content-type-options: nosniff</code></li>\n<li><code>x-frame-options: SAMEORIGIN</code></li>\n</ul>\n<p>The helper behind this (<code>applyDefaultSecurityHeaders</code>) only sets a header\n<strong>when missing</strong> — so a route or shell <code>headers()</code> export always wins,\nincluding when it weakens a default. The framework does NOT set\n<code>strict-transport-security</code>, <code>content-security-policy</code>, or <code>cross-origin-*</code>\nheaders — those need a project decision.</p>\n<p>One deliberate exception: <strong>protocol-switch responses</strong> (a <code>101</code> WebSocket\nhandshake, or any response carrying a <code>webSocket</code> handle) are returned exactly\nas the handler produced them, with no headers applied. This is not a gap —\ncopying the response would destroy the socket, and a handshake has no body for\na sniffing or framing policy to protect. Do not report it as a finding.</p>\n<p>The audit surface is therefore:</p>\n<ul>\n<li><strong>(a)</strong> <code>dist/client</code> served by a custom CDN/host outside the first-party\nadapters — nothing applies the defaults there.</li>\n<li><strong>(b)</strong> <code>headers()</code> exports that override a default with a weaker value.</li>\n<li><strong>(c)</strong> HSTS and CSP, which genuinely need user action.</li>\n</ul>\n<p>Prerequisites: <code>pracht inspect</code> requires a vite config that registers the\npracht plugin; <code>pracht inspect build</code> and\n<code>dist/server/headers-manifest.json</code> require a prior <code>pracht build</code>. Every\nserverful target currently publishes a client copy at\n<code>dist/client/_pracht/headers.json</code>; only Cloudflare reads it there. Pure static\nexports deliberately do not publish that copy.</p>\n<h2>Step 1: Inventory header sources</h2>\n<pre><code>pracht inspect routes --json\npracht inspect api --json\n</code></pre>\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<p>Only route modules and shells have a <code>headers()</code> export — API handlers do not\n(there is no <code>headers</code> in the API module shape; API handlers set headers\ninline on the <code>Response</code> they build, and the runtime applies the defaults on\ntop). Inventory:</p>\n<ul>\n<li><code>headers()</code> exports in route files, loader files (<code>loaderFile</code>), and shells\n(<code>shellFile</code>).</li>\n<li>Hand-rolled <code>Response</code> constructions in API handlers and middleware where\nsecurity-relevant headers are set inline.</li>\n</ul>\n<h2>Step 2: Find weakened defaults and uncovered static hosting</h2>\n<p>Defaults are framework-applied, so do not build a \"which route is covered\"\nmatrix — instead:</p>\n<ol>\n<li>For each <code>headers()</code> source from Step 1, flag any of the four default\nheader names set to a weaker value (e.g. <code>x-frame-options: ALLOWALL</code>,\nan over-permissive <code>permissions-policy</code>, <code>referrer-policy: unsafe-url</code>).\nBecause the defaults are set-when-missing, the route value wins.</li>\n<li>Ask how <code>dist/client</code> is deployed. On <code>adapter-node</code>, <code>adapter-cloudflare</code>,\nor the generated Vercel config, static/ISG responses get the defaults. If\na custom CDN or host serves <code>dist/client</code> directly, flag it (<code>warn</code>) and\nrecommend replicating the four defaults in that host's header config.</li>\n</ol>\n<h2>Step 3: HSTS</h2>\n<p>Grep for <code>strict-transport-security</code>. If absent everywhere, recommend adding\nit (e.g. via a shell <code>headers()</code> export or the host's config):</p>\n<pre><code>\"strict-transport-security\": \"max-age=63072000; includeSubDomains; preload\"\n</code></pre>\n<p>Only recommend <code>preload</code> if the user confirms they want to commit to HTTPS\npermanently and submit to the preload list.</p>\n<h2>Step 4: Content-Security-Policy</h2>\n<p>Start from the starter policy in <a href=\"https://pracht.resynapse.dev/docs/recipes/csp\">https://pracht.resynapse.dev/docs/recipes/csp</a>,\nthen adjust it for speculation and add observed origins:</p>\n<ol>\n<li>Check which routes use <code>speculation</code>: read the <code>speculation</code> field from\n<code>pracht inspect routes --json</code> (requires a current <code>@pracht/cli</code>; on older\nCLIs, grep the manifest for <code>speculation:</code> instead). Those routes emit an\ninline <code>&lt;script type=\"speculationrules\"&gt;</code>, so add\n<code>'inline-speculation-rules'</code> to <code>script-src</code> unless you use a nonce.</li>\n<li>Grep the app for fetch URLs, image URLs, CSS <code>@import</code>, <code>&lt;script src&gt;</code>,\n<code>&lt;link href&gt;</code>, font URLs, iframe sources.</li>\n<li>Group by directive: <code>default-src</code>, <code>script-src</code>, <code>style-src</code>, <code>img-src</code>,\n<code>font-src</code>, <code>connect-src</code>, <code>frame-src</code>; always include <code>'self'</code> per\ndirective, per the starter policy.</li>\n<li>Pracht injects hydration state in a non-executable\n<code>&lt;script id=\"pracht-state\" type=\"application/json\"&gt;</code> — it does not need\n<code>'unsafe-inline'</code>. Do not add <code>'unsafe-inline'</code> unless the app truly emits\nexecutable inline scripts and the tradeoff is documented.</li>\n<li>Routes with <code>streaming: true</code> emit executable inline scripts for deferred\ndata and boundary swaps. Under a nonce policy, return the request nonce as\n<code>scriptNonce</code> from the shell <code>head()</code> and allow it in <code>script-src</code>; it also\ncovers the speculation rules script.</li>\n</ol>\n<p>Output a draft CSP referencing <a href=\"https://pracht.resynapse.dev/docs/recipes/csp\">https://pracht.resynapse.dev/docs/recipes/csp</a> and explain what each origin is\nfor. Do not hand the user a CSP that breaks their site — present as draft.</p>\n<h2>Step 5: Prerendered header manifest safety</h2>\n<p>For SSG/ISG output, the framework refuses to prerender any route whose\ndocument headers include dangerous names (<code>set-cookie</code>, <code>authorization</code>,\n<code>proxy-authorization</code>, <code>www-authenticate</code>, <code>proxy-authenticate</code>) or\nsecret-shaped custom <code>x-*</code> names (token/secret/key/credential patterns) — a\nbuild containing them hard-fails. So do not hunt for those at runtime; audit\nthe <code>headers()</code> sources statically to catch them <strong>before</strong> a build failure,\nand report each as <code>error</code> with the prerender failure it would cause.</p>\n<p>The real target is the <code>warn</code> class the framework cannot catch: innocuously\nnamed headers carrying user-specific values in\n<code>dist/server/headers-manifest.json</code>, which serverful adapters may replay across\nusers on static responses. Every serverful build currently also copies that\nmanifest into public client output; Cloudflare reads it through the assets\nbinding, while the other adapters retain the public copy for compatibility. A\npure static export omits the client copy and has no runtime to replay the server\nmanifest; instead, verify that the deployment's host-header configuration\nmirrors only safe, non-user-specific entries. Missing host configuration means\nroute <code>headers()</code> values are not applied at all.</p>\n<p>Secret VALUES in headers are owned by <code>audit-secrets</code>; this skill owns policy\nheaders. Cross-reference <code>audit-secrets</code> for value-level findings.</p>\n<h2>Step 6: Cross-origin isolation (optional)</h2>\n<p>If the user mentions <code>SharedArrayBuffer</code>, WASM threading, or high-precision\ntimers, recommend:</p>\n<ul>\n<li><code>cross-origin-opener-policy: same-origin</code></li>\n<li><code>cross-origin-embedder-policy: require-corp</code></li>\n<li><code>cross-origin-resource-policy: same-origin</code> on assets</li>\n</ul>\n<p>Otherwise leave these unset — they break embedded third-party content.</p>\n<h2>Step 7: Report</h2>\n<p>Two outputs:</p>\n<ol>\n<li><strong>Findings table</strong> (source × header × severity).</li>\n<li><strong>Recommendations</strong>, in priority order:\n<ul>\n<li><code>error</code>: <code>headers()</code> values that would fail prerender (Step 5).</li>\n<li><code>error</code>: <code>headers()</code> exports that weaken a default.</li>\n<li><code>warn</code>: <code>dist/client</code> served by a host that does not apply the defaults.</li>\n<li><code>warn</code>: Missing HSTS in production-grade apps.</li>\n<li><code>warn</code>: User-specific headers in prerendered static output.</li>\n<li><code>info</code>: CSP draft, COOP/COEP suggestions.</li>\n</ul>\n</li>\n</ol>\n<p>Show the exact code change required (e.g. the <code>headers()</code> export to fix, or\nthe CDN header config to add).</p>\n<h2>Rules</h2>\n<ol>\n<li>The four defaults are framework-applied — do not recommend re-adding them\nin app code; audit for weakening and for hosting paths outside the\nfirst-party adapters.</li>\n<li>Never recommend a CSP with <code>'unsafe-eval'</code> unless the user has documented\nwhy they need it.</li>\n<li><code>applyDefaultSecurityHeaders</code> is set-when-missing, so per-route <code>headers()</code>\nwins over the defaults — weakened values are the primary finding, not\nmissing ones.</li>\n<li>Static/ISG responses already get the defaults on first-party adapters and\nthe generated Vercel config; only flag static hosting that bypasses those.</li>\n<li>Treat prerender header manifests as public artifacts.</li>\n<li>Do not auto-edit. Headers are policy; surface gaps, propose patches.</li>\n</ol>\n<p>$ARGUMENTS</p>\n","files":[{"path":"SKILL.md","sizeBytes":8949,"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:17:57.158159Z","sha256":"D29E471E8AD252039115F27DF05FCC69867A4759598251FB12500279686768A4","sizeBytes":3953},"review":null,"source":{"repositoryUrl":"https://github.com/JoviDeCroock/pracht","path":"skills/audit-headers","license":"MIT","commit":"8bcdd52989de3e97708e7f16221839d6313cfdac","subtreeSha":"8AF3A283D1FDE529D2E51B6ECA3BAC5622BD03269EB16A8F3792892F4C4D7C6C","lastSyncedAt":"2026-10-19T21:17:21.911347Z"},"reviewedAt":"2026-10-19T21:19:00.410992Z","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-headers"},{"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"}]}