The security header an agent widened to make the feature work
A strict Content-Security-Policy is usually the thing standing between an agent and the feature it was asked to add. Widening it is the path of least resistance, it is never mentioned in the summary, and nothing about the site looks different afterwards.
An AI coding agent is asked to add an analytics snippet. It adds the script tag. The
console fills with Refused to load the script ... violates the following Content Security Policy directive. The error names the exact directive that
refused, and the agent resolves it the shortest way available: it adds
'unsafe-inline' to script-src.
The feature now works. Nothing in the test suite looks at response headers, so the suite is green. The pull request says "add analytics".
It does not say "disabled the site's protection against cross-site scripting", because from where the agent was standing that is not what happened. What happened is that a configuration was blocking a legitimate resource and the configuration was corrected.
Why CSP specifically
Every category of security configuration carries this risk, and a strict CORS
policy gets it too. CSP takes the most damage for a structural reason:
Strict-Transport-Security does not stop a feature, and
X-Content-Type-Options does not stop a feature, but a tightly scoped CSP stops
features constantly, by design. It is an allowlist covering scripts, styles,
images, fonts, frames and connections, so almost anything new is denied until
someone names it — and the browser prints the exact directive to change.
That makes it the header that generates an error message during development, and error messages are what an agent optimises against. The loop is: make the thing work, observe the failure, remove the cause of the failure. A CSP directive is a cause of failure, and it is the cheapest cause in sight to remove.
There is a correct fix — add the specific origin, or a hash, or a nonce. There
is a fast fix — 'unsafe-inline', or *, or delete the header while debugging
and forget to restore it. Both produce a working page and only one of them shows
up as different in any test you are likely to have.
What each loosening actually exposes
Vagueness is not useful here, so:
script-src 'unsafe-inline' means any injected <script> in your HTML
executes. This is the difference between a stored XSS being a rendering bug and
being a full account takeover: the injected script runs with your origin's
privileges, can read the DOM, and can act as the logged-in user. A strict CSP is
one of the few mitigations that works even when the injection itself was not
prevented. Removing it removes the second line of defence, which is the one that
matters precisely because the first one failed.
script-src * or a wildcard CDN means the set of parties who can run code
on your origin is now the set of parties who can get a file onto that host. This
is the supply-chain case: you are trusting every package that ever ships through
that CDN, forever, on your users' sessions.
frame-ancestors removed re-enables clickjacking. Your authenticated app
can be loaded invisibly inside somebody else's page with a button positioned
underneath their cursor.
A cookie without HttpOnly is readable by JavaScript, which turns any XSS
into session theft even without CSP involvement. Without Secure, the
cookie is sent over plain HTTP, so a network observer on any request that
somehow goes to http:// collects it. Without SameSite — and the defaults
here vary by browser and version, which is why relying on them is a bad idea —
the cookie rides along with cross-site requests, which is the whole basis of
CSRF.
Mixed content is an HTTPS page loading an HTTP subresource. Browsers block active mixed content — script, iframes, XHR — and log it to the console, which means the symptom is a feature that does not work and nothing at all in your server logs to explain why. Passive mixed content — an image — may be upgraded or displayed with a downgraded indicator. Either way, the page made a plaintext request to somewhere, and a network attacker can see it and, for active content, replace it.
Access-Control-Allow-Origin: * on an authenticated API is the one people
underestimate most. On its own, with no credentials, it exposes whatever the API
returns to anonymous callers. Combined with
Access-Control-Allow-Credentials: true it is a serious hole — browsers
disallow that exact combination, so the common workaround is reflecting the
request's Origin header back, which is the same thing with extra steps and is
functionally an invitation for any site your user visits to read their data with
their cookies attached.
Why review does not catch it
The diff is small. It is often one line inside a long string in an nginx config or a middleware, and the surrounding line is already three hundred characters of directives. Adding a token to a CSP is visually indistinguishable from removing one.
The change also has no behavioural symptom. Nothing renders differently. No test fails, because tests exercise the application, and the application works better after this change than before. The end-to-end suite is in fact more likely to pass.
And the reason for the change is genuinely legitimate. Someone did ask for analytics. The header was in the way. Every step in the chain is defensible; the outcome is a site with weaker protection than it had, and no record anywhere that a security decision was made.
The structural difference with an agent is not care, it is context. Whoever wrote that CSP had a reason for each narrow directive, and that reason was not written into the file — CSP has no comments in a header. A person who was there may remember it. A session that opened the file for the first time cannot, so the directive is just a value that is in the way, and the loop is optimising to remove the error.
Watch the response, not the config file
The check that survives all of this is the same one that works for certificates: look at what a client actually receives, rather than at the file that was supposed to produce it. Headers get rewritten by proxies, CDNs, and framework middleware, so the config in the repository is a hypothesis about your production responses, not a description of them.
HarpyWatch splits this across five checks because they fail independently:
| Check | What it observes |
|---|---|
security-headers |
The standard set is present and sane — HSTS, X-Content-Type-Options, frame-ancestors or X-Frame-Options, referrer policy |
csp-policy |
Content-Security-Policy specifically: which directives exist, and whether the dangerous tokens have appeared |
cookie-flags |
Secure, HttpOnly and SameSite on the cookies actually set |
mixed-content |
HTTPS pages requesting HTTP subresources |
cors-policy |
What your API tells a browser about who may call it |
CSP gets its own check rather than living inside security-headers for the
reason this whole piece is about: presence is not the interesting property. A
CSP can be present, syntactically valid, and permissive enough to be decorative.
The question is which tokens are in it, and that question needs asking after
every deploy rather than in an audit once a year.
The limit
None of this prevents the change. It is external observation, so it tells you the header on the response is now weaker than it was — after the deploy, not before it.
If you want the before, run the checks against a staging URL before you point DNS at it. That is the cheapest moment to find it. The prerequisite is a staging URL your monitoring can reach from outside, which is a small piece of work and the only one this asks of you.
Cite this
The HarpyWatch team. “The security header an agent widened to make the feature work”. The Nest, HarpyWatch, 8 October 2026 UTC. https://harpywatch.com/blog/the-csp-an-agent-widened
More from The Nest
Set it up in an afternoon and then stop
Adopting monitoring for a small site is five decisions, not a project. Here is what each one actually involves, and why finishing a rough version beats designing a perfect one.
Staging configuration travels, and agents help it
A canonical tag pointing at staging, a noindex that shipped, a test API key in production. Each is invisible on a developer's machine and expensive live, and copying config between environments is the fastest way to make something work.
Monitor the site nobody is looking at yet
The instinct is to add monitoring once a site has traffic worth protecting. That gets it backwards: an empty site is the one failure mode nobody reports, because your first hundred visitors leave instead.