tasks/find-security-issues.md has produced a "add HSTS + X-Frame-Options to next.config.*" PR twice on the Sanity/Next fleet — jotul-frontend#675 (merged, sat 26 days, reverted) and merkur-frontend#566 (blocked by the protected-paths gate, now inside bundle #576). Both were wrong for the same reason, and both cost review time.
The interesting part: security headers are not in the task's pattern list. Step 2 enumerates auth bypass, injection, XSS, exposed secrets, CSRF, rate limiting, and deserialization/SSRF/open redirects. Missing response headers appear nowhere — so the run went outside its own stated scope, twice, and step 3's "pick one real, non-speculative issue" didn't hold it in.
Needed from whoever owns the task prompts: an explicit negative in step 2. The positive list demonstrably didn't constrain this, so listing categories isn't enough — "missing security header" is the kind of finding a model reaches for when the real list turns up nothing that night.
Why both PRs were wrong
Verified 2026-08-26 with curl -sSI against production apexes:
- Vercel already serves
strict-transport-security: max-age=63072000 on every attached custom domain — confirmed on merkur-programmet.no, distriktssenteret.no and www.jotul.com, none of which set it in app code. So an app-level HSTS header can only add includeSubDomains / preload (effectively irreversible) or silently weaken the platform value. merkur#566's stated premise — "the custom domain only gets it if we set it" — is false.
X-Frame-Options is the wrong tool on this stack, not merely redundant. All seven synced clones set Content-Security-Policy: frame-ancestors including cross-origin Sanity Studio hosts for Presentation. Modern browsers ignore XFO when frame-ancestors is present, and legacy XFO can't express an allowlist, so SAMEORIGIN would break the Studio embed exactly where it takes effect.
jotul already worked this out and wrote it down — next.config.mjs:56 carries the full rationale as a comment. That reasoning never reached the boilerplate template, which is filed as sanity-nextjs-boilerplate#438.
Suggested wording for step 2
A short "not in scope" line, e.g.:
Not in scope: missing or weak response headers (HSTS, X-Frame-Options, CSP hardening) on platform-hosted apps. The host may already set them, and the irreversible directives (includeSubDomains, preload) are an ops decision, not an overnight fix. Report as an issue if you believe one is genuinely missing — do not open a PR.
The "report, don't PR" half matters: if a header genuinely is missing, that's still worth surfacing. It's the unattended code change that's wrong.
Adjacent
Step 1 dedupes against open night-shift security PRs only, so a proposal that was merged-and-reverted (jotul#675) or closed is invisible to the next run — which is how the same idea returned seven weeks later. That's #38's territory rather than this issue's, noting it because it's the mechanism that let the repeat through.
The path-based baseline exclude in bundles/_multi-runner.md is not the right lever here — it excludes directories, not finding types.
tasks/find-security-issues.mdhas produced a "add HSTS + X-Frame-Options tonext.config.*" PR twice on the Sanity/Next fleet — jotul-frontend#675 (merged, sat 26 days, reverted) and merkur-frontend#566 (blocked by the protected-paths gate, now inside bundle #576). Both were wrong for the same reason, and both cost review time.The interesting part: security headers are not in the task's pattern list. Step 2 enumerates auth bypass, injection, XSS, exposed secrets, CSRF, rate limiting, and deserialization/SSRF/open redirects. Missing response headers appear nowhere — so the run went outside its own stated scope, twice, and step 3's "pick one real, non-speculative issue" didn't hold it in.
Needed from whoever owns the task prompts: an explicit negative in step 2. The positive list demonstrably didn't constrain this, so listing categories isn't enough — "missing security header" is the kind of finding a model reaches for when the real list turns up nothing that night.
Why both PRs were wrong
Verified 2026-08-26 with
curl -sSIagainst production apexes:strict-transport-security: max-age=63072000on every attached custom domain — confirmed onmerkur-programmet.no,distriktssenteret.noandwww.jotul.com, none of which set it in app code. So an app-level HSTS header can only addincludeSubDomains/preload(effectively irreversible) or silently weaken the platform value. merkur#566's stated premise — "the custom domain only gets it if we set it" — is false.X-Frame-Optionsis the wrong tool on this stack, not merely redundant. All seven synced clones setContent-Security-Policy: frame-ancestorsincluding cross-origin Sanity Studio hosts for Presentation. Modern browsers ignore XFO whenframe-ancestorsis present, and legacy XFO can't express an allowlist, soSAMEORIGINwould break the Studio embed exactly where it takes effect.jotul already worked this out and wrote it down —
next.config.mjs:56carries the full rationale as a comment. That reasoning never reached the boilerplate template, which is filed as sanity-nextjs-boilerplate#438.Suggested wording for step 2
A short "not in scope" line, e.g.:
The "report, don't PR" half matters: if a header genuinely is missing, that's still worth surfacing. It's the unattended code change that's wrong.
Adjacent
Step 1 dedupes against open night-shift security PRs only, so a proposal that was merged-and-reverted (jotul#675) or closed is invisible to the next run — which is how the same idea returned seven weeks later. That's #38's territory rather than this issue's, noting it because it's the mechanism that let the repeat through.
The path-based baseline exclude in
bundles/_multi-runner.mdis not the right lever here — it excludes directories, not finding types.