# `next dev` Renders but Nothing Works: Your CSP Is Missing `unsafe-eval`

> Source: <https://dev.to/ai_changewatch/next-dev-renders-but-nothing-works-your-csp-is-missing-unsafe-eval-26pl>
> Published: 2026-08-24 12:00:00+00:00

I run [ AI Change Watch](https://aichangewatch.com/?src=devto), a small independent project that

At some point I added a Content-Security-Policy. It was correct. It shipped. Production was fine.

And then, locally, every interactive thing on the site stopped working.

`next dev`

starts. The page loads. It looks **exactly right** — the layout, the data, the styles, all

of it. Then:

`onClick`

anywhere firesNo error page. No red overlay. No failed request in the Network tab. The server rendered the HTML and

sent it, so the page you are looking at is real — it is just **completely inert**. Nothing hydrated.

If you have not hit this before, the natural first guess is your own component. That is where I went,

and it is the wrong place, because every component is fine.

The console has it, but you have to be looking:

```
Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an
allowed source of script in the following Content Security Policy directive:
"script-src 'self' 'unsafe-inline' …"
```

And the reason it is easy to miss is that it is not a JavaScript error. It does not have a stack. It

does not point at your file. It appears once, near the top, above whatever else the page logged, and it

names a directive rather than a component.

Next's development server compiles modules and hands them to the browser wrapped in `eval`

— that is

how the dev `devtool`

setting works, and it is what React Refresh needs to swap a component without

reloading the page. Fast Refresh is built on it.

A production build does not do that. `next build`

emits static chunks. There is no string being

evaluated at runtime, so there is nothing for `'unsafe-eval'`

to permit.

Which produces the trap:

The CSP is correct for production and fatal in development — and development is where you spend all

your time.

You will not catch it in CI, because CI builds. You will not catch it in preview, because preview

builds. You catch it the moment you try to click something locally, and by then you are three commits

into a feature and looking for the bug in your own diff.

Because it is in `next.config`

, and that file has no idea which mode it is running in unless you tell

it:

```
// next.config.mjs
async headers() {
  return [{ source: '/:path*', headers: securityHeaders }];
},
```

`source: '/:path*'`

means every path. There is no dev/prod branch, so `next dev`

serves the same header

`next start`

does. That is a reasonable default — you generally *want* to develop against the headers

you ship — it just happens to be wrong for this one directive.

**Option A — widen the policy in development only.**

``` js
const isDev = process.env.NODE_ENV === 'development';

const csp = [
  "default-src 'self'",
  // 'unsafe-eval' is DEV-ONLY: the dev server evaluates compiled modules as strings (that is what
  // React Refresh is built on), and a production build never does. Shipping it would be a real
  // widening of the policy for zero benefit.
  `script-src 'self' 'unsafe-inline'${isDev ? " 'unsafe-eval'" : ''}`,
  // The HMR socket, same reasoning. In production nothing connects back to the dev server.
  `connect-src 'self'${isDev ? ' ws: wss:' : ''}`,
  // …the rest
].join('; ');
```

The comment is not decoration. A conditional in a security header is exactly the kind of line that gets

"cleaned up" six months later by someone who reads it as an inconsistency, so the reason it is

conditional has to sit next to it.

**Option B — stop testing client behaviour in next dev.**

```
next build && next start
```

This is what I actually do, for a reason that has nothing to do with CSP: this project deploys to

Cloudflare Workers through OpenNext, and `next dev`

is not the runtime it ships on. Behaviour I verify

in dev is behaviour I verified somewhere the code will never run. So for anything client-side I build

and serve the real thing.

The cost is real — you lose Fast Refresh, and a rebuild per change is slow enough to change how you

work. If your production runtime *is* Node, Option A is the better trade. If it isn't, Option B was

going to be necessary anyway and this just makes it obvious sooner.

The thing worth taking away isn't the directive. It's this:

A security header set in`next.config`

applies to the dev server, and the dev server has different

requirements than the thing you deploy.

`unsafe-eval`

is the one that produces a *silent* failure, which is why it costs the most time. But the

same category catches you elsewhere:

| directive | what dev needs that prod doesn't |
|---|---|
`script-src` |
`'unsafe-eval'` for the module runtime / React Refresh |
`connect-src` |
`ws:` / `wss:` for the HMR socket |
`style-src` |
`'unsafe-inline'` if your prod build extracts CSS but dev injects it |

If you are about to add a CSP to a Next app, the fastest check is not a code review. It is:

`next dev`

Thirty seconds, and it is the only test that distinguishes "rendered" from "working". Everything else

about a dead page looks identical to a live one.

*The tracker this came out of is at aichangewatch.com — it watches AI
vendor docs for changes. Its CSP still has no 'unsafe-eval' in production, which is the point.*
