Claude Skill

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

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Download jovidecroock-pracht-skills_audit-headers-8bcdd52.zip · 3 KB

Install

skills CLI npx skills add https://github.com/JoviDeCroock/pracht/tree/main/skills/audit-headers
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install jovidecroock-pracht@llmmart
Git 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-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

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):

"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

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.

No comments yet.

Reviews (0)

No reviews yet.