Evaluates the Content-Security-Policy you paste into the query string. Nothing is fetched: not your origin, not the resource, not a report endpoint, not the specification. It is pure string and set logic over the values you supply, so it runs in CI, offline, and against a server that does not exist yet.
/check is the route to reach for first. It answers "would this policy allow this URL, and which directive decided?" — the two questions a blocked console message never answers precisely. It resolves the effective directive for the request's destination (CSP3 §6.8.1), walks the §6.8.3 fallback chain, and reports which single directive executed: §6.8.4 lets only the first declared name in the chain run, and every other directive defers. So with both default-src and script-src present, script-src executes and default-src reports executed: false — not "both applied". Then it runs the executing directive's own pre-request check and returns the expression that matched, or the exact production each expression failed at.
The fallback chains are the facts most worth knowing, and they are transcribed from §6.8.3 rather than guessed: frame-src → child-src → default-src; worker-src → child-src → script-src → default-src; script-src-elem → script-src → default-src. And the ones that do not exist: base-uri, form-action, frame-ancestors, sandbox, webrtc and the reporting directives never fall back to default-src. §6.1.3's own equivalence example omits all of them, and §6.4.2 states outright that default-src 'none' still allows the resource to be embedded by anyone. /check?context=form-action proves it in one request.
'strict-dynamic' is the other answer people come looking for, and §6.7.1.1's step order is normative and non-obvious: nonce, then integrity, then 'strict-dynamic', then the source list. A parser-inserted <script src> with a matching nonce is therefore allowed even alongside 'strict-dynamic'; without a nonce it is blocked even when its host is allowlisted. Flip parserInserted — which defaults to true, modelling a script tag written in the HTML — and watch the answer change. The keyword is also inert outside the script check: in img-src it does nothing, and in default-src it only bites when no script directive is declared. Even a worker is subtle, because worker-src's own check is a plain source-list match while script-src's is the script check. §6.7.1.1 gates its whole nonce/integrity/'strict-dynamic' block on the request's destination being script-like, and Fetch's script-like list has six entries — audioworklet, paintworklet, script, serviceworker, sharedworker, worker — while asking algorithms that use it to consider xslt as well, because that too can execute script. This API takes Fetch up on that, so context=xslt is treated as script-like and the nonce, integrity and 'strict-dynamic' steps apply to an XSLT stylesheet. A literal six-name reading would give it plain source-list matching instead; that is the one place this API goes beyond the letter of the two specs, and it is the safer of the two readings.
/parse runs the §2.2.1 parse algorithm and shows you its results: directive names lowercased, a repeated name kept once and every later occurrence discarded, empty and non-ASCII tokens skipped, and every source expression classified against the §2.3.1 grammar with its scheme, host, port, path, hash algorithm or base64 value pulled apart. /evaluate returns a coverage table — for each of the fifteen protection surfaces, which directive governs it after the fallback chain and whether inline content is allowed — plus ranked findings, each citing the clause it rests on.
There is deliberately no grade, no score and no pass/fail badge. Not "not yet" — never. Findings carry a severity that is this API's own documented ranking of how much protection the condition removes; it is not a CVSS score and not a vulnerability rating, and a policy with zero findings is not an audit. CSP is a defence-in-depth mitigation against injection, not an authorization mechanism.
Asymmetries that look like bugs and are not. Scheme matching upgrades only upwards: http: matches https:, ws: matches wss:, http: and https:, and nothing matches downward. *.example.com matches www.example.com and a.b.example.com and not the apex example.com. example.com. with a trailing dot is compared literally. 'none' is honoured only while it is the sole expression, so script-src 'none' https://example.com really does match https://example.com/, while a valueless script-src matches nothing at all. A path-part is dropped entirely once redirectCount is not zero — a deliberate compromise against cross-origin path leaks (§7.6). And a host-source never matches a URL with no host, which is why img-src 'self' blocks a data: image and img-src data: allows it.
Where the pinned draft contradicts itself, this API follows the normative algorithm, says which one it followed, and reports both readings in ambiguities — it never silently picks a side and never claims what a browser does. There are exactly three such places, each with two citable conflicting passages: whether a host-source can match an IP-literal host, whether http://example.com:80 matches https://example.com/, and what 'unsafe-allow-redirects' means. Growing that list needs a new citation, not a hunch.
Scope, stated plainly rather than discovered later. No network, ever. No inline-content checking and no hash computation: matching an inline script or style against a hash-source needs SHA-256/384/512 over the source, which on this runtime means the asynchronous crypto.subtle API, and these handlers are synchronous. /evaluate still answers the §6.7.3.2 question "does this source list allow all inline behaviour?", because that needs no hashing, and /check accepts URLs only. Resource-hint requests (prefetch, prerender) use a different union-of-all-source-lists rule and are out of scope, so they are not context values. There is no separate post-request route: model a redirect chain by passing the final URL with redirectCount set. Opaque origins and §7.8 policy inheritance into blob:, data: and srcdoc documents are not modelled, self=null is a 400, and 'self' compares the URL's own scheme/host/port tuple, so it will not match a blob: URL whose inner origin is yours. sandbox tokens are echoed, not validated — they come from HTML's iframe attribute, not from CSP. There is no referrer row, because no current W3C Recommendation or draft defines it, and no embedded-enforcement directives. A source expression containing a comma cannot be expressed, exactly as on the wire.
We model the pinned W3C draft, not any browser. Console wording, vendor quirks, Reporting-Endpoints plumbing and CSP Level 2-only behaviour are out of scope, and browsers differ from the draft and from each other. The directive table is pinned to CSP Level 3 Working Draft 2026-07-29 by SHA-256 and cross-checked against the IANA registry, and scripts/refresh-csp-directives.mjs --check re-derives the mechanical columns from the live documents and fails on drift. Facts verified 2026-07-30.