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.
npx vercel buildnpx trufflehog filesystem .vercel/output --fail --only-verified --no-updatenpx 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-updateThe 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.
| Tool | What it finds | Verifies live credentials | Where to run it |
|---|---|---|---|
| TruffleHog | Secrets in git, filesystems, and Docker images (entropy + detectors) | Yes ( | CI on |
| Gitleaks | Known patterns in git history and diffs | No | Pre-commit hook and PR-scoped CI |
GitHub push protection | Partner patterns at | Partner validity checks | Every push. Free on public repos; Secret Protection on private repos |
| Trunk | Orchestrates Gitleaks, TruffleHog, Semgrep, Trivy with one config | Depends on the wrapped tool | Same rules locally and in CI |
| osv-scanner | Known CVEs in lockfiles and images, not secrets | Not applicable | CI 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
dangerouslySetInnerHTMLandeval. - Functional tests of security rules so a rate limit still returns
429after 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.