Application & framework security

Why do NEXT_PUBLIC_ variables leak into the Next.js client bundle?

Any NEXT_PUBLIC_ value is public. Scan the files you ship (.vercel/output, .next, Docker layers), not only git history. TruffleHog, Gitleaks, GitHub push protection, Trunk, and osv-scanner are all current and cover different leak paths.

6 min read
In short: Any NEXT_PUBLIC_ value is public. Scan the files you ship (.vercel/output, .next, Docker layers), not only git history. TruffleHog, Gitleaks, GitHub push protection, Trunk, and osv-scanner are all current and cover different leak paths.

Why do NEXT_PUBLIC_ variables leak into the client bundle?

Next.js inlines every NEXT_PUBLIC_* environment variable into the JavaScript it sends to the browser. That is the documented contract, not a bug. A key named NEXT_PUBLIC_ARCJET_KEY or NEXT_PUBLIC_DATABASE_URL is readable in DevTools, in the built chunks under .next/static, and in the Vercel deployment artifact.

Server-only variables stay on the server unless you import them into a Client Component, log them, or copy them into a container layer. The NEXT_PUBLIC_ prefix is the one that leaks by design: Next.js inlines those values into the client bundle at build time. Treat any value with that prefix as public, forever, including values you later rename.

Do not put ARCJET_KEY, database URLs, or OAuth client secrets in NEXT_PUBLIC_ variables. Read secrets from the host's secret store at runtime. The Next.js security checklist lists environment variables as a first-class item; this page is the scanning layer around that item.

Two other leak paths show up in Next.js builds. A Server Component that passes a secret into a Client Component as a prop serializes it to the browser. A next.config env block that maps an unprefixed name into the client bundle does the same thing. Search the built chunks for ajkey_, sk_live_, and your cloud project id after every accidental rename. The scanner is there because people miss those searches.

How do I scan Next.js build artifacts for secrets?

Scan the files you are about to ship, not only git log. A secret can enter the bundle from the build environment even when it never appeared in a commit.

On Vercel the upload is the Build Output API directory, .vercel/output. Vercel's default Git integration does not let you insert a scanner between next build and the upload. Run your own CI, build with the Vercel CLI, scan, then deploy.

Terminal window
npx vercel build
npx trufflehog filesystem .vercel/output --fail --only-verified --no-update
npx vercel deploy --prebuilt

--only-verified asks TruffleHog to check the candidate against the provider (an AWS key that authenticates, a Slack token that returns 200). Without it you will fail builds on example strings and high-entropy hashes. Current TruffleHog also accepts --results=verified for the same idea.

If you self-host, scan .next after next build the same way. Pay attention to .next/static (client) and any server chunks that might have inlined process.env at build time.

How do I scan Docker images after a Next.js build?

A COPY . . that includes .env or a build-arg that became an ENV persists in an image layer after you delete the file in a later layer. Scan the saved image, not the build context.

- name: Build
run: docker build -t my-next-app:latest .
- name: Save
run: docker save -o /tmp/my-next-app.tar my-next-app:latest
- name: TruffleHog
run: >
docker run --rm -v /tmp:/tmp trufflesecurity/trufflehog:latest
docker --image file:///tmp/my-next-app.tar --fail --only-verified --no-update

The official trufflesecurity/trufflehog GitHub Action runs in git mode. Image scans need the CLI as above. The same image rules (non-root, Distroless, read-only) are in secure container deployments.

Which secret scanner should I use?

Use more than one. They fail on different inputs. All of the following are current as of 2026.

ToolWhat it findsVerifies live credentialsWhere to run it
TruffleHog

Secrets in git, filesystems, and Docker images (entropy + detectors)

Yes (--only-verified)

CI on .vercel/output, .next, and images

GitleaksKnown patterns in git history and diffsNoPre-commit hook and PR-scoped CI

GitHub push protection

Partner patterns at git push

Partner validity checks

Every push. Free on public repos; Secret Protection on private repos

TrunkOrchestrates Gitleaks, TruffleHog, Semgrep, Trivy with one configDepends on the wrapped toolSame rules locally and in CI
osv-scannerKnown CVEs in lockfiles and images, not secretsNot applicableCI next to the secret scan

GitHub push protection is the only control that can stop the secret from landing in history. Turn it on. It will not catch a generic NEXT_PUBLIC_ company token or a value that only appears in .next/static after the build. That is why TruffleHog on the artifact stays in the pipeline.

Gitleaks is the fast PR gate. Point it at origin/main..HEAD so you are not rescanning the whole history on every push. Run a full-history TruffleHog job on a schedule and after any suspected leak.

Trunk is optional glue. If you already run its linters, enable the secret plugins so the editor, the pre-commit hook, and CI share one allowlist. Do not treat Trunk as a substitute for push protection.

osv-scanner does not find secrets. It belongs in the same job because a leaked ghp_ token and a critical lockfile CVE are both "do not deploy" conditions.

What else belongs next to the secret scan?

Secret scanning is one row on a Next.js release gate:

  • No NEXT_PUBLIC_ on anything that can mint a session, talk to a database, or call Arcjet.
  • import "server-only" on modules that read secrets, so a Client Component import fails the build.
  • Semgrep (or equivalent) on dangerouslySetInnerHTML and eval.
  • Functional tests of security rules so a rate limit still returns 429 after the refactor that "only touched env files."

If a scanner reports a verified ajkey_ or a cloud credential, rotate it before you rewrite history. Removing the file from git does not invalidate the key.

Frequently asked questions

Why do `NEXT_PUBLIC_` variables leak into the client bundle?

Next.js inlines them into browser JavaScript on purpose. A key named NEXT_PUBLIC_ARCJET_KEY is readable in DevTools and in .next/static. Unprefixed server secrets stay server-side unless you pass them into a Client Component or bake them into an image layer.

Does docs.arcjet.com/checklists/nextjs still exist?

Yes. Link that checklist for the broader Next.js gate. This article is the scanning layer around environment variables.

Which scanner should I run?

GitHub push protection on every push, Gitleaks on the PR diff, TruffleHog --only-verified on .vercel/output or the saved image, and osv-scanner on the lockfile. Trunk can run the same plugins locally and in CI.

Can I scan inside Vercel's default Git integration?

Not cleanly. Build with the Vercel CLI in your own CI, scan .vercel/output, then vercel deploy --prebuilt.

Does removing a secret from git revoke it?

No. Rotate the credential first. History and old image layers still hold the old value.

Application security in your code

Protect your application with Arcjet

Get rate limits, bot detection, and attack blocking in your request handlers.