audit-headers
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
Install
npx skills add https://github.com/JoviDeCroock/pracht/tree/main/skills/audit-headers
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install jovidecroock-pracht@llmmart
git clone https://github.com/JoviDeCroock/pracht.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole jovidecroock/pracht collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Pracht Audit Headers
What the framework guarantees: four default security headers are applied
automatically on EVERY framework response path — SSR pages, API responses,
404s, and static/ISG output on the first-party adapters (adapter-node,
adapter-cloudflare) as well as the Vercel headers config generated at build
time:
permissions-policy(disables device sensors)referrer-policy: strict-origin-when-cross-originx-content-type-options: nosniffx-frame-options: SAMEORIGIN
The helper behind this (applyDefaultSecurityHeaders) only sets a header
when missing — so a route or shell headers() export always wins,
including when it weakens a default. The framework does NOT set
strict-transport-security, content-security-policy, or cross-origin-*
headers — those need a project decision.
One deliberate exception: protocol-switch responses (a 101 WebSocket
handshake, or any response carrying a webSocket handle) are returned exactly
as the handler produced them, with no headers applied. This is not a gap —
copying the response would destroy the socket, and a handshake has no body for
a sniffing or framing policy to protect. Do not report it as a finding.
The audit surface is therefore:
- (a)
dist/clientserved by a custom CDN/host outside the first-party adapters — nothing applies the defaults there. - (b)
headers()exports that override a default with a weaker value. - (c) HSTS and CSP, which genuinely need user action.
Prerequisites: pracht inspect requires a vite config that registers the
pracht plugin; pracht inspect build and
dist/server/headers-manifest.json require a prior pracht build. Every
serverful target currently publishes a client copy at
dist/client/_pracht/headers.json; only Cloudflare reads it there. Pure static
exports deliberately do not publish that copy.
Step 1: Inventory header sources
pracht inspect routes --json
pracht inspect api --json
MCP: when the pracht MCP server is registered (pracht dev-mcp), prefer its
inspect_routes/inspect_api/inspect_build/doctor/verify tools over
shelling out.
Only route modules and shells have a headers() export — API handlers do not
(there is no headers in the API module shape; API handlers set headers
inline on the Response they build, and the runtime applies the defaults on
top). Inventory:
headers()exports in route files, loader files (loaderFile), and shells (shellFile).- Hand-rolled
Responseconstructions in API handlers and middleware where security-relevant headers are set inline.
Step 2: Find weakened defaults and uncovered static hosting
Defaults are framework-applied, so do not build a "which route is covered" matrix — instead:
- For each
headers()source from Step 1, flag any of the four default header names set to a weaker value (e.g.x-frame-options: ALLOWALL, an over-permissivepermissions-policy,referrer-policy: unsafe-url). Because the defaults are set-when-missing, the route value wins. - Ask how
dist/clientis deployed. Onadapter-node,adapter-cloudflare, or the generated Vercel config, static/ISG responses get the defaults. If a custom CDN or host servesdist/clientdirectly, flag it (warn) and recommend replicating the four defaults in that host's header config.
Step 3: HSTS
Grep for strict-transport-security. If absent everywhere, recommend adding
it (e.g. via a shell headers() export or the host's config):
"strict-transport-security": "max-age=63072000; includeSubDomains; preload"
Only recommend preload if the user confirms they want to commit to HTTPS
permanently and submit to the preload list.
Step 4: Content-Security-Policy
Start from the starter policy in https://pracht.resynapse.dev/docs/recipes/csp, then adjust it for speculation and add observed origins:
- Check which routes use
speculation: read thespeculationfield frompracht inspect routes --json(requires a current@pracht/cli; on older CLIs, grep the manifest forspeculation:instead). Those routes emit an inline<script type="speculationrules">, so add'inline-speculation-rules'toscript-srcunless you use a nonce. - Grep the app for fetch URLs, image URLs, CSS
@import,<script src>,<link href>, font URLs, iframe sources. - Group by directive:
default-src,script-src,style-src,img-src,font-src,connect-src,frame-src; always include'self'per directive, per the starter policy. - Pracht injects hydration state in a non-executable
<script id="pracht-state" type="application/json">— it does not need'unsafe-inline'. Do not add'unsafe-inline'unless the app truly emits executable inline scripts and the tradeoff is documented. - Routes with
streaming: trueemit executable inline scripts for deferred data and boundary swaps. Under a nonce policy, return the request nonce asscriptNoncefrom the shellhead()and allow it inscript-src; it also covers the speculation rules script.
Output a draft CSP referencing https://pracht.resynapse.dev/docs/recipes/csp and explain what each origin is for. Do not hand the user a CSP that breaks their site — present as draft.
Step 5: Prerendered header manifest safety
For SSG/ISG output, the framework refuses to prerender any route whose
document headers include dangerous names (set-cookie, authorization,
proxy-authorization, www-authenticate, proxy-authenticate) or
secret-shaped custom x-* names (token/secret/key/credential patterns) — a
build containing them hard-fails. So do not hunt for those at runtime; audit
the headers() sources statically to catch them before a build failure,
and report each as error with the prerender failure it would cause.
The real target is the warn class the framework cannot catch: innocuously
named headers carrying user-specific values in
dist/server/headers-manifest.json, which serverful adapters may replay across
users on static responses. Every serverful build currently also copies that
manifest into public client output; Cloudflare reads it through the assets
binding, while the other adapters retain the public copy for compatibility. A
pure static export omits the client copy and has no runtime to replay the server
manifest; instead, verify that the deployment's host-header configuration
mirrors only safe, non-user-specific entries. Missing host configuration means
route headers() values are not applied at all.
Secret VALUES in headers are owned by audit-secrets; this skill owns policy
headers. Cross-reference audit-secrets for value-level findings.
Step 6: Cross-origin isolation (optional)
If the user mentions SharedArrayBuffer, WASM threading, or high-precision
timers, recommend:
cross-origin-opener-policy: same-origincross-origin-embedder-policy: require-corpcross-origin-resource-policy: same-originon assets
Otherwise leave these unset — they break embedded third-party content.
Step 7: Report
Two outputs:
- Findings table (source × header × severity).
- Recommendations, in priority order:
error:headers()values that would fail prerender (Step 5).error:headers()exports that weaken a default.warn:dist/clientserved by a host that does not apply the defaults.warn: Missing HSTS in production-grade apps.warn: User-specific headers in prerendered static output.info: CSP draft, COOP/COEP suggestions.
Show the exact code change required (e.g. the headers() export to fix, or
the CDN header config to add).
Rules
- The four defaults are framework-applied — do not recommend re-adding them in app code; audit for weakening and for hosting paths outside the first-party adapters.
- Never recommend a CSP with
'unsafe-eval'unless the user has documented why they need it. applyDefaultSecurityHeadersis set-when-missing, so per-routeheaders()wins over the defaults — weakened values are the primary finding, not missing ones.- Static/ISG responses already get the defaults on first-party adapters and the generated Vercel config; only flag static hosting that bypasses those.
- Treat prerender header manifests as public artifacts.
- Do not auto-edit. Headers are policy; surface gaps, propose patches.
$ARGUMENTS
Files (pracht)
-
SKILL.md 8.7 KB
--- name: audit-headers version: 1.2.1 description: | 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 security headers", "check CSP", "harden headers", "set up HSTS", "review header policy". allowed-tools: - Bash - Read - Grep - Glob --- # Pracht Audit Headers What the framework guarantees: four default security headers are applied automatically on EVERY framework response path — SSR pages, API responses, 404s, and static/ISG output on the first-party adapters (`adapter-node`, `adapter-cloudflare`) as well as the Vercel headers config generated at build time: - `permissions-policy` (disables device sensors) - `referrer-policy: strict-origin-when-cross-origin` - `x-content-type-options: nosniff` - `x-frame-options: SAMEORIGIN` The helper behind this (`applyDefaultSecurityHeaders`) only sets a header **when missing** — so a route or shell `headers()` export always wins, including when it weakens a default. The framework does NOT set `strict-transport-security`, `content-security-policy`, or `cross-origin-*` headers — those need a project decision. One deliberate exception: **protocol-switch responses** (a `101` WebSocket handshake, or any response carrying a `webSocket` handle) are returned exactly as the handler produced them, with no headers applied. This is not a gap — copying the response would destroy the socket, and a handshake has no body for a sniffing or framing policy to protect. Do not report it as a finding. The audit surface is therefore: - **(a)** `dist/client` served by a custom CDN/host outside the first-party adapters — nothing applies the defaults there. - **(b)** `headers()` exports that override a default with a weaker value. - **(c)** HSTS and CSP, which genuinely need user action. Prerequisites: `pracht inspect` requires a vite config that registers the pracht plugin; `pracht inspect build` and `dist/server/headers-manifest.json` require a prior `pracht build`. Every serverful target currently publishes a client copy at `dist/client/_pracht/headers.json`; only Cloudflare reads it there. Pure static exports deliberately do not publish that copy. ## Step 1: Inventory header sources ```bash pracht inspect routes --json pracht inspect api --json ``` MCP: when the pracht MCP server is registered (`pracht dev-mcp`), prefer its `inspect_routes`/`inspect_api`/`inspect_build`/`doctor`/`verify` tools over shelling out. Only route modules and shells have a `headers()` export — API handlers do not (there is no `headers` in the API module shape; API handlers set headers inline on the `Response` they build, and the runtime applies the defaults on top). Inventory: - `headers()` exports in route files, loader files (`loaderFile`), and shells (`shellFile`). - Hand-rolled `Response` constructions in API handlers and middleware where security-relevant headers are set inline. ## Step 2: Find weakened defaults and uncovered static hosting Defaults are framework-applied, so do not build a "which route is covered" matrix — instead: 1. For each `headers()` source from Step 1, flag any of the four default header names set to a weaker value (e.g. `x-frame-options: ALLOWALL`, an over-permissive `permissions-policy`, `referrer-policy: unsafe-url`). Because the defaults are set-when-missing, the route value wins. 2. Ask how `dist/client` is deployed. On `adapter-node`, `adapter-cloudflare`, or the generated Vercel config, static/ISG responses get the defaults. If a custom CDN or host serves `dist/client` directly, flag it (`warn`) and recommend replicating the four defaults in that host's header config. ## Step 3: HSTS Grep for `strict-transport-security`. If absent everywhere, recommend adding it (e.g. via a shell `headers()` export or the host's config): ```ts "strict-transport-security": "max-age=63072000; includeSubDomains; preload" ``` Only recommend `preload` if the user confirms they want to commit to HTTPS permanently and submit to the preload list. ## Step 4: Content-Security-Policy Start from the starter policy in <https://pracht.resynapse.dev/docs/recipes/csp>, then adjust it for speculation and add observed origins: 1. Check which routes use `speculation`: read the `speculation` field from `pracht inspect routes --json` (requires a current `@pracht/cli`; on older CLIs, grep the manifest for `speculation:` instead). Those routes emit an inline `<script type="speculationrules">`, so add `'inline-speculation-rules'` to `script-src` unless you use a nonce. 2. Grep the app for fetch URLs, image URLs, CSS `@import`, `<script src>`, `<link href>`, font URLs, iframe sources. 3. Group by directive: `default-src`, `script-src`, `style-src`, `img-src`, `font-src`, `connect-src`, `frame-src`; always include `'self'` per directive, per the starter policy. 4. Pracht injects hydration state in a non-executable `<script id="pracht-state" type="application/json">` — it does not need `'unsafe-inline'`. Do not add `'unsafe-inline'` unless the app truly emits executable inline scripts and the tradeoff is documented. 5. Routes with `streaming: true` emit executable inline scripts for deferred data and boundary swaps. Under a nonce policy, return the request nonce as `scriptNonce` from the shell `head()` and allow it in `script-src`; it also covers the speculation rules script. Output a draft CSP referencing <https://pracht.resynapse.dev/docs/recipes/csp> and explain what each origin is for. Do not hand the user a CSP that breaks their site — present as draft. ## Step 5: Prerendered header manifest safety For SSG/ISG output, the framework refuses to prerender any route whose document headers include dangerous names (`set-cookie`, `authorization`, `proxy-authorization`, `www-authenticate`, `proxy-authenticate`) or secret-shaped custom `x-*` names (token/secret/key/credential patterns) — a build containing them hard-fails. So do not hunt for those at runtime; audit the `headers()` sources statically to catch them **before** a build failure, and report each as `error` with the prerender failure it would cause. The real target is the `warn` class the framework cannot catch: innocuously named headers carrying user-specific values in `dist/server/headers-manifest.json`, which serverful adapters may replay across users on static responses. Every serverful build currently also copies that manifest into public client output; Cloudflare reads it through the assets binding, while the other adapters retain the public copy for compatibility. A pure static export omits the client copy and has no runtime to replay the server manifest; instead, verify that the deployment's host-header configuration mirrors only safe, non-user-specific entries. Missing host configuration means route `headers()` values are not applied at all. Secret VALUES in headers are owned by `audit-secrets`; this skill owns policy headers. Cross-reference `audit-secrets` for value-level findings. ## Step 6: Cross-origin isolation (optional) If the user mentions `SharedArrayBuffer`, WASM threading, or high-precision timers, recommend: - `cross-origin-opener-policy: same-origin` - `cross-origin-embedder-policy: require-corp` - `cross-origin-resource-policy: same-origin` on assets Otherwise leave these unset — they break embedded third-party content. ## Step 7: Report Two outputs: 1. **Findings table** (source × header × severity). 2. **Recommendations**, in priority order: - `error`: `headers()` values that would fail prerender (Step 5). - `error`: `headers()` exports that weaken a default. - `warn`: `dist/client` served by a host that does not apply the defaults. - `warn`: Missing HSTS in production-grade apps. - `warn`: User-specific headers in prerendered static output. - `info`: CSP draft, COOP/COEP suggestions. Show the exact code change required (e.g. the `headers()` export to fix, or the CDN header config to add). ## Rules 1. The four defaults are framework-applied — do not recommend re-adding them in app code; audit for weakening and for hosting paths outside the first-party adapters. 2. Never recommend a CSP with `'unsafe-eval'` unless the user has documented why they need it. 3. `applyDefaultSecurityHeaders` is set-when-missing, so per-route `headers()` wins over the defaults — weakened values are the primary finding, not missing ones. 4. Static/ISG responses already get the defaults on first-party adapters and the generated Vercel config; only flag static hosting that bypasses those. 5. Treat prerender header manifests as public artifacts. 6. Do not auto-edit. Headers are policy; surface gaps, propose patches. $ARGUMENTS
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.