[BUG] Security remediation generates vercel.json with mutually-exclusive routes + headers — security headers silently not applied
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report
What's Wrong?
When Claude Code performs a security audit and remediation on a Vercel-hosted static site, it may generate a vercel.json combining the legacy routes array with the modern headers array. These two top-level fields are mutually exclusive in Vercel: the presence of routes silently disables headers processing, so all security headers never reach the client.
Steps to Reproduce
- Have a static Vercel site (HTML + no build step)
- Ask Claude Code to run a security audit and apply fixes
- Claude Code generates
vercel.jsonwithroutes(for path-blocking) ANDheaders(for security headers) - Deploy succeeds with no errors
- Run
curl -sI https://your-domain.com— security headers are absent
Generated (broken) vercel.json:
{
"routes": [
{ "src": "/.env", "status": 404 },
{ "src": "/.git", "status": 404 }
],
"headers": [
{
"source": "/(.*)",
"headers": [
{ "key": "Content-Security-Policy", "value": "default-src 'self'..." },
{ "key": "X-Frame-Options", "value": "DENY" },
{ "key": "X-Content-Type-Options", "value": "nosniff" }
]
}
]
}
What Should Happen?
Claude Code should generate a vercel.json where security headers actually reach the browser. When path-blocking is also needed, it should use compatible alternatives:
middleware.jsfor path blocking (compatible withheaders)redirects/rewrites(modern syntax, coexists withheaders)- NOT
routeswhenheadersare also needed
Working approach using middleware:
// middleware.js
import { NextResponse } from 'next/server'
export function middleware(request) {
const url = new URL(request.url)
const blocked = ['/.env', '/.git', '/wp-admin']
if (blocked.some(p => url.pathname.startsWith(p))) {
return new Response('Not Found', { status: 404 })
}
}
Root Cause
Vercel has two routing systems:
- Legacy (
routes) — overrides ALL other config processing when present - Modern (
redirects,rewrites,headers,cleanUrls) — composable, coexist fine
When routes is present, Vercel silently ignores the headers block. This is documented in Vercel's config reference but not prominently surfaced. Claude Code correctly knows both systems exist but generates them together, producing a configuration that looks correct but silently fails.
Impact
- Silent security failure — no error in build logs, no warning in Vercel dashboard
- Invisible to users — headers appear correct in
vercel.jsonbut never reach the browser - Real-world incident — after remediating 20/22 security audit findings (commit
e043f73), onlyStrict-Transport-Security(set by Vercel automatically) was present in responses; CSP, X-Frame-Options, Referrer-Policy, Permissions-Policy, COOP, X-Content-Type-Options were all silently absent - Verification required — discovery only via
curl -sI https://domain.com/?bust=$(date +%s%N)(random query to bypass Edge cache)
Workaround
Replace routes with middleware.js for path blocking. Keep headers in vercel.json as-is — it applies correctly once routes is removed.
Confirmed working in production after migration (nuvaus.com, Vercel Pro, static HTML+Tailwind+Alpine.js).
Suggested Fix
When generating vercel.json with security headers:
- Detect if
routesis present alongsideheadersand warn/refuse to generate - Auto-generate
middleware.jsinstead ofroutesentries when path-blocking is needed - Or add a post-generation verification step:
curl -sI <deployed-url>and compare actual headers againstvercel.json
Environment
- OS: Windows 11 Pro 10.0.26200
- Claude Code: latest (2.1.119+)
- Vercel: Pro plan
- Site type: static (HTML + Tailwind CDN + Alpine.js v3, no build step)
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗