Skip to content
M.Infath
cd ~/blog
~/blog/netlify-secrets-scanning-turbopack-cache.mdx
21 Aug 2026 2 min readnextjsturbopacknetlifyci-cd

Netlify blocked my deploy for leaking secrets (it was Turbopack's cache)

A while back my portfolio stopped deploying. Netlify failed the build with a secrets scanning error and said it found the value of my RESEND_API_KEY in the build output.

That confused me, because I never print that key anywhere. It only gets used inside one API route, on the server.

Where the key actually was#

The scanner pointed at files like this:

.netlify/.next/cache/turbopack/xxxx.sst

I didn't even know what an .sst file was. Turns out it belongs to Turbopack's persistent build cache. Since Next.js 16.3, Turbopack saves build state between runs so rebuilds are faster, and part of that state is the environment variables your build read. With their values.

So the key was never in my actual site. It was in a cache folder the build keeps for itself, and that folder never gets deployed. But Netlify scans everything the build produced, saw a real secret sitting in a file, and blocked the deploy. Which is fair from their side. I just lost some time figuring out why.

The fix#

Two lines in netlify.toml:

netlify.toml
[build.environment]
  SECRETS_SCAN_OMIT_PATHS = ".netlify/.next/cache/**,.next/cache/**"

This tells the scanner to skip the cache folders and nothing else. The rest of the build output and the repo still get scanned, so if a secret ever ends up somewhere that actually ships, the deploy still fails.

My first instinct was to set SECRETS_SCAN_ENABLED = "false" and move on. I'm glad I didn't. That turns off the whole alarm because it rang once for a half-valid reason.

One more thing I changed#

While I was in there I also changed how the code reads env variables. If you write process.env.RESEND_API_KEY as a static expression, the bundler is allowed to inline the actual value into the output. That is how secrets leak for real, not the cache thing above.

So now the keys are read with computed access and validated with zod on first use, not at import time:

src/lib/env.ts
const ENV_KEYS = ['RESEND_API_KEY', 'CONTACT_FROM_EMAIL', 'CONTACT_TO_EMAIL'] as const;
 
export const getEnv = (): Env => {
  if (!cached) {
    const source = Object.fromEntries(ENV_KEYS.map((key) => [key, process.env[key]]));
    cached = envSchema.parse(source);
  }
  return cached;
};

Two nice side effects: next build runs without needing the real secrets at all, and there is no static expression for the bundler to inline.

Summary#

  • The Turbopack cache stores env values, not just names. Treat .next/cache and .netlify/.next/cache as sensitive.
  • Scope the scanner exclusion to the cache paths only. Don't disable scanning.
  • Read server secrets with computed access so they can't be inlined at build time.

If you got here because your Netlify build failed with a secrets error pointing at .sst files, it's the cache. The two lines above should fix it.