hash-static
Generate a CSP policy by scanning static HTML files for inline content — no browser or crawl required.
Use this as a post-build step when you ship a static site: it hashes every inline <script>, <style>, style="..." attribute, and on*="..." event handler across the built HTML, dedupes across files, and either prints the policy or writes it straight into each <head> as a <meta> tag.
When to use this vs crawl
| Scenario | Use |
|---|---|
| You have a static build on disk and want deterministic hashes | hash-static |
| You need to capture inline content injected by JS at runtime | crawl (dynamic) |
| You want both: static build + framework-injected inline content | hash-static with --extra / --merge-json seeded from a one-off crawl |
hash-static is fast (no Playwright, no network) and ideal for CI. It only sees what is in the HTML on disk — content added by JavaScript at runtime (e.g. some framework hydration scripts) is invisible to it. For those cases, run crawl once against a preview server, export as JSON, and feed the result back via --merge-json (or individual sources via --extra).
Usage
csp-analyser hash-static <path>... [options]<path> may be a file or a directory; directories are walked recursively for *.html files. Multiple paths are allowed.
Options
| Option | Default | Description |
|---|---|---|
--inject | false | Rewrite each scanned HTML in place to include the generated CSP as a <meta http-equiv="Content-Security-Policy"> immediately after <head>. Any existing CSP <meta> is replaced. |
--format <fmt> | meta | Output format when not using --inject (see export formats). |
--report-only | false | Emit Content-Security-Policy-Report-Only instead of the enforcing header. |
--extra <directive>=<src> | — | Extra source for a CSP fetch or navigation directive. Repeatable. e.g. --extra connect-src=https://api.example.com. See supported directives. |
--merge-json <path> | — | Merge directives from a previously exported JSON file (the { directives: { ... } } format from --format json). Repeatable. Also accepts a bare directive map. |
--policy-directive <d>=<v> | — | Set a CSP document directive verbatim. Repeatable. e.g. --policy-directive report-uri=/csp-report. Value-less directives like upgrade-insecure-requests omit the =. See supported directives. |
--static-profile react-expo | — | Collapse excessive style-src-attr hashes to 'unsafe-inline' when used with --collapse-hash-threshold, while keeping script hashes and <style> hashes intact. |
What it captures
For each HTML file under the given paths, hash-static extracts:
<script>...</script>blocks without asrcattribute →script-src-elem<style>...</style>blocks →style-src-elem- Every
style="..."attribute value (including empty strings) →style-src-attr - Every
on*="..."event handler attribute value (including empty strings) →script-src-attr
Empty-string values are included deliberately: browsers evaluate CSP against <div style=""> and require the empty-string SHA-256 (sha256-47DEQpj8...) to be listed for the attribute to apply.
When any hashes end up under style-src-attr or script-src-attr, 'unsafe-hashes' is added automatically — without it, the browser silently ignores attribute-context hashes (CSP3 §2.3.2).
Output
The generated directive map always includes a secure baseline:
default-src 'self'base-uri 'self'form-action 'self'font-src 'self'img-src 'self' data:object-src 'none'
Plus script-src-elem / style-src-elem / style-src-attr / script-src-attr populated from the scan, and any additional directives provided via --extra.
Examples
Generate and print the meta tag
csp-analyser hash-static dist/Inject into every HTML file in the build output
csp-analyser hash-static dist/ --injectAs a post-build step in package.json
{
"scripts": {
"build": "vitepress build docs && csp-analyser hash-static docs/.vitepress/dist --inject"
}
}Include runtime-injected hashes captured by a prior crawl
csp-analyser hash-static dist/ --inject \
--extra "style-src-elem='sha256-skqujXORqzxt1aE0NNXxujEanPTX6raoqSscTV/Ww/Y='" \
--extra "script-src-elem='sha256-someRuntimeInjectedScriptHash='"Add API endpoints, font CDNs, and frame sources
csp-analyser hash-static dist/ --inject \
--extra connect-src=https://api.example.com \
--extra connect-src=wss://ws.example.com \
--extra font-src=https://fonts.gstatic.com \
--extra frame-src=https://www.youtube.comMerge directives from a previous crawl export
# First, crawl the live site and export as JSON:
csp-analyser crawl https://example.com
csp-analyser export --format json > crawl-policy.json
# Then combine with static hashes:
csp-analyser hash-static dist/ --inject --merge-json crawl-policy.jsonThis merges runtime-discovered directives (like connect-src, frame-src) from the crawl with the static inline hashes — missing directives fall back to default-src 'self'.
Static React/Expo style attribute explosion
csp-analyser hash-static dist/ --inject \
--collapse-hash-threshold 10 \
--static-profile react-expoThis keeps script-src-elem, script-src-attr, and style-src-elem hash-based, but collapses an excessive style-src-attr hash set to style-src-attr 'unsafe-inline'. This is narrower than broad style-src 'unsafe-inline' and is intended for static React Native Web / Expo exports where dynamic nonces are unavailable. Attribute hashes require 'unsafe-hashes', but very large generated style-attribute sets are usually not maintainable; the profile uses the scoped style-src-attr fallback only for that case.
Add reporting and upgrade directives
csp-analyser hash-static dist/ --format header \
--policy-directive report-uri=/csp-report \
--policy-directive report-to=csp-endpoint \
--policy-directive upgrade-insecure-requestsExport as Cloudflare Pages _headers
csp-analyser hash-static dist/ --format cloudflare-pages > dist/_headersSupported directives
--extra and --merge-json accept the following CSP fetch and navigation directives:
default-src, script-src, style-src, img-src, font-src, connect-src, media-src, object-src, frame-src, worker-src, child-src, form-action, base-uri, manifest-src, script-src-elem, script-src-attr, style-src-elem, style-src-attr
--policy-directive accepts CSP document directives that don't take source lists:
report-uri, report-to, sandbox, upgrade-insecure-requests, require-trusted-types-for, trusted-types, plugin-types
These are passed verbatim into the policy. Note that report-uri and report-to are stripped when exporting to meta format (meta tags cannot include them).
Notes
- HTML parsing is regex-based, tuned for compliant machine-generated output (VitePress, Next.js, Astro, etc.). Hand-written HTML with unusual quoting may not parse cleanly.
- Runs without a database — it does not create a session and cannot be compared, scored, or diffed via the session-based commands.
- If you want to both hash static content and capture runtime-injected hashes automatically, run
crawlagainst a local preview instead.
When to use this command
Use hash-static as a post-build step for static sites (VitePress, Hugo, Jekyll, Astro, etc.) where you control the HTML output. It scans your built HTML files, computes SHA-256 hashes for every inline <script>, <style>, style="..." attribute, and on*="..." event handler, and emits a Content Security Policy that allows exactly those inline resources. No browser or network access required — it works entirely from the filesystem. Choose this over crawl when your site is static and you want deterministic, hash-based CSP generation.