agentleFS
JoviDeCroockJoviDeCroock/pracht/skills/audit-headers/SKILL.md

audit-headers

Skill106 starsChanged 7 months agoMITView on GitHub

Audit security headers in a pracht app: `headers()` exports that weaken the defaults, static hosts that drop them, and HSTS and CSP choices. Use for "audit security headers", "check CSP", "harden headers", "set up HSTS".

The file

We split this skill into the rules it gives an agent

9 rules. About 0 are common, 2 are distinctive, and 7 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈2

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 7

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Only route modules and shells have a headers() export — API handlers do not (there is no headers in the API module shape.

    line 64
  2. Very specific:

    Ask how dist/client is deployed.

    line 83
  3. Very specific:

    Only recommend preload if the user confirms they want to commit to HTTPS permanently and submit to the preload list.

    line 97
  4. Very specific:

    Start from the starter policy in <https://pracht.resynapse.dev/docs/recipes/csp>, then adjust it for speculation and add observed origins:

    line 102
  5. Very specific:

    Check which routes use speculation: read the speculation field from pracht inspect routes --json (requires a current @pracht/cli.

    line 105
  6. Very specific:

    If the user mentions SharedArrayBuffer, WASM threading, or high-precision timers, recommend:

    line 153
  7. Very specific:

    Never recommend a CSP with 'unsafe-eval' unless the user has documented why they need it.

    line 183
  8. Distinctive:

    Treat prerender header manifests as public artifacts.

    line 190
  9. Distinctive:

    Do not auto-edit. Headers are policy.

    line 191

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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 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: if pracht dev-mcp is registered, prefer its inspect_*, doctor, and verify tools to the CLI.

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

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.