next-secret-guard
v0.1.1
Published
CLI-first secret scanning and client-boundary guardrails for Next.js apps.
Maintainers
Readme
next-secret-guard
CLI-first guardrails for accidental secret exposure in Next.js apps.
next-secret-guard catches the kinds of mistakes that most often turn private values into client-reachable code in a Next.js app:
- environment variables used in the wrong place
NEXT_PUBLIC_values that look suspiciously sensitive- server-only modules imported into Client Component reachability paths
- shared utilities that accidentally drag secret-bearing code toward the browser bundle
It is designed for real-world Next.js apps, not as a generic env validator, not as a replacement for Next.js, ESLint, or a full security scanner, and not as a substitute for code review. It is a fast, CLI-first preflight check that helps teams catch risky patterns before they ship.
Why this exists
Secret leaks in Next.js rarely happen because someone deliberately published a credential. They usually happen because a developer:
- assumes an environment variable is server-only when it is not
- uses
NEXT_PUBLIC_for something that should stay private - imports a server helper into a shared module that later becomes reachable from
"use client" - moves code during a refactor and accidentally crosses the server/client boundary
- ships a feature quickly and does not notice that a secret ended up in a browser-accessible path
The cost of these mistakes can be high:
- API keys can be extracted from shipped JavaScript
- database credentials can be abused for unauthorized reads or writes
- third-party billing, identity, or AI APIs can be charged or manipulated
- private user data can be exposed through downstream access
- incident response becomes slower because the leak is embedded in deployed client code
This package exists to reduce that risk in the part of the workflow where it is easiest to prevent: before merge, before deploy, and before production traffic sees it.
What it does
next-secret-guard scans your codebase for practical secret-exposure risks and reports them with severity levels so teams can prioritize fixes:
- suspicious
process.envusage in Client Components - server-only module reachability from client-side code
- risky
NEXT_PUBLIC_*naming patterns - provider-specific secret patterns for Supabase, Stripe, Prisma, OpenAI, and Auth.js / Clerk-style setups
Install
npm install -D next-secret-guardOther package managers:
pnpm add -D next-secret-guard
yarn add -D next-secret-guard
bun add -d next-secret-guardTry it now
Run a scan from the root of your Next.js project:
npx next-secret-guard scanInteractive example:
$ npx next-secret-guard scan
next-secret-guard scan
Root: /Users/me/my-next-app
Files scanned: 42
Fail on: HIGH, MEDIUM
Issues found: 1
Issues
+---------+----------------------------------------------------------------------------------+
| Field | Value |
+---------+----------------------------------------------------------------------------------+
| # | 1 |
| Severity | HIGH |
| File | components/UserTable.tsx |
| Line | 4 |
| Title | Client component reaches server-only module |
| Message | Reachability path: components/UserTable.tsx -> src/lib/users.ts -> src/server/db.ts |
| Suggestion | Move server-only code behind a server action, route handler, or server component boundary. |
| Variable | src/server/db.ts |
| Path | components/UserTable.tsx -> src/lib/users.ts -> src/server/db.ts |
+---------+----------------------------------------------------------------------------------+
Summary
+----------+-------+---------------------------+
| Severity | Count | Meaning |
+----------+-------+---------------------------+
| HIGH | 1 | Blocks CI when configured |
| MEDIUM | 0 | Review before merge |
| LOW | 0 | Track and clean up |
| INFO | 0 | Informational only |
+----------+-------+---------------------------+
Exit code: 1 when an issue matches failOn, otherwise 0Switch to JSON for automation:
npx next-secret-guard scan --jsonUse CI mode to make the command fail when issues meet your configured threshold:
npx next-secret-guard scan --ciQuick start
If you want the shortest possible path:
npx next-secret-guard scan --root .If your repository uses a config file:
npx next-secret-guard scan --config next-secret-guard.config.tsCode preview: risky patterns detected
The examples below are intentionally unsafe and are shown only to demonstrate what next-secret-guard can detect.
1. Secret used inside a Client Component
"use client";
export function RiskyClientComponent() {
// Risky: Client Components run in the browser.
// Server-only secrets must not be referenced here.
console.log(process.env.SUPABASE_SERVICE_ROLE_KEY);
console.log(process.env.STRIPE_SECRET_KEY);
console.log(process.env.DATABASE_URL);
return <div>Risky client component</div>;
}This is risky because "use client" marks the component as client-side code. Server-only secrets should be moved to Route Handlers, Server Actions, Server Components, or backend services.
Expected scanner output:
✖ HIGH Secret used in Client Component
File: components/RiskyClientComponent.tsx
Variable: SUPABASE_SERVICE_ROLE_KEY
Suggestion: Move this logic to a server-only module, Route Handler, or Server Action.2. Dangerous NEXT_PUBLIC_* secret name
NEXT_PUBLIC_SUPABASE_URL=https://example.supabase.co
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_fake_public_key
# Risky: NEXT_PUBLIC_* variables are exposed to the browser bundle.
NEXT_PUBLIC_STRIPE_SECRET_KEY=sk_test_do_not_use_this
NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY=fake_service_role_key_do_not_useNEXT_PUBLIC_* should only be used for values that are safe to expose publicly. Publishable keys are okay. Secret keys, service role keys, private tokens, and database credentials are not okay.
Expected scanner output:
✖ HIGH Dangerous public environment variable
File: .env.example
Variable: NEXT_PUBLIC_STRIPE_SECRET_KEY
Suggestion: Remove NEXT_PUBLIC_ from secrets. Only expose publishable/public keys.3. Supabase service role key misuse
// Risky demo:
// Supabase service role keys bypass Row Level Security.
// Keep them in server-only modules, Route Handlers, or backend services.
export function createRiskySupabaseAdminClient() {
return {
url: process.env.NEXT_PUBLIC_SUPABASE_URL,
serviceRoleKey: process.env.SUPABASE_SERVICE_ROLE_KEY,
};
}Supabase service role keys must never be reachable from client-side code. Admin clients should live in files like src/server/supabase-admin.ts. Add import "server-only"; when using Next.js App Router.
Safe version:
import "server-only";
export function createSupabaseAdminClient() {
return {
url: process.env.NEXT_PUBLIC_SUPABASE_URL,
serviceRoleKey: process.env.SUPABASE_SERVICE_ROLE_KEY,
};
}4. Stripe secret key misuse
// Risky demo:
// Stripe secret keys must remain server-side.
export function createRiskyStripeClient() {
return {
secretKey: process.env.STRIPE_SECRET_KEY,
};
}Stripe secret keys can access sensitive payment operations. Use secret keys only in Route Handlers, Server Actions, or backend services. Client-side code should use publishable keys only.
Safe public variable example:
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_fake_public_key
STRIPE_SECRET_KEY=sk_test_fake_secret_key5. Prisma DATABASE_URL misuse
// Risky demo:
// DATABASE_URL gives direct access to your database.
// Prisma/database connections must stay server-side.
export function createRiskyDatabaseConnection() {
return {
databaseUrl: process.env.DATABASE_URL,
};
}DATABASE_URL should never be imported into Client Components. Prisma should only run on the server. Keep database clients inside server-only modules.
Safe structure example:
src/
server/
db.ts # Prisma client here
app/
api/
users/
route.ts # Uses server/db.ts safelyExample full scanner output
npx next-secret-guard scan✖ HIGH Secret used in Client Component
File: components/RiskyClientComponent.tsx
Variable: SUPABASE_SERVICE_ROLE_KEY
Suggestion: Move this logic to a server-only module, Route Handler, or Server Action.
✖ HIGH Dangerous public environment variable
File: .env.example
Variable: NEXT_PUBLIC_STRIPE_SECRET_KEY
Suggestion: Remove NEXT_PUBLIC_ from secrets. Only expose publishable/public keys.
✖ HIGH Supabase service role key misuse
File: lib/risky-supabase-client.ts
Variable: SUPABASE_SERVICE_ROLE_KEY
Suggestion: Keep service role usage in server-only files.
✖ HIGH Stripe secret key misuse
File: lib/risky-stripe-client.ts
Variable: STRIPE_SECRET_KEY
Suggestion: Use Stripe secret keys only in server-side code.
✖ HIGH Prisma DATABASE_URL misuse
File: lib/risky-prisma-client.ts
Variable: DATABASE_URL
Suggestion: Keep Prisma and database connections on the server.The exact output may differ depending on the installed version of next-secret-guard.
Safe usage summary
| Risky pattern | Safer approach |
|---|---|
| process.env.SECRET_KEY inside "use client" | Move logic to a Server Component, Server Action, Route Handler, or backend service |
| NEXT_PUBLIC_STRIPE_SECRET_KEY | Use STRIPE_SECRET_KEY only on the server |
| NEXT_PUBLIC_SUPABASE_SERVICE_ROLE_KEY | Use service role key only in a server-only admin client |
| DATABASE_URL imported by client code | Keep Prisma/database code under src/server |
| Shared lib file with secrets | Split into lib/public-* and server/* modules |
Safe public config example:
export const publicConfig = {
supabaseUrl: process.env.NEXT_PUBLIC_SUPABASE_URL,
stripePublishableKey: process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY,
};What can go wrong?
Here are a few common failure modes in Next.js applications:
- A secret is placed in
NEXT_PUBLIC_*so it can be used by the browser, but the value was never meant to be public. - A module that reads server-only environment variables is imported by a file that later becomes part of a Client Component subtree.
- A utility file is shared across server and client code, so a server-only dependency gets dragged into the browser bundle indirectly.
- A package that should only run on the server is accessed from code that is eventually bundled for the client.
The result is not always an obvious "leak" in the source code. Often the problem is that secret-bearing code becomes reachable from the browser bundle or from client-side execution paths.
What it detects
next-secret-guard is focused on the most common Next.js secret-exposure mistakes:
NEXT_PUBLIC_variables used for values that look sensitive- suspicious server-only environment variable usage in code that can reach the client bundle
- import chains that connect Client Components to server-only modules
- cross-boundary dependency paths that make server logic reachable from client code
- provider-specific risky patterns for common services such as Supabase, Stripe, Prisma, OpenAI, and Clerk/Auth.js style auth helpers
Severity is reported so teams can prioritize the riskiest issues first.
What it does not detect
This package is intentionally narrow.
It does not:
- replace Next.js build-time protections
- replace ESLint, TypeScript, or code review
- perform full static application security testing
- prove that a secret has been exfiltrated
- scan infrastructure, deployment logs, or runtime telemetry
- detect every possible security flaw in your application
- validate that all secrets are properly rotated or stored in a vault
Treat it as a guardrail, not a complete security audit.
CLI usage
Typical commands:
npx next-secret-guard scan
npx next-secret-guard scan --json
npx next-secret-guard scan --ci
npx next-secret-guard scan --preset supabase
npx next-secret-guard scan --preset supabase,stripe
npx next-secret-guard scan --config .next-secret-guard.jsonUseful flags:
--root <path>scan a different project root--ciexit with code 1 when findings match the configuredfailOnseverities--jsonprint a JSON report--preset <name>enable one or more provider presets--fail-on <severity>override the configured CI threshold for a specific run--config <path>load a custom config file
Example: override the CI threshold for a specific run:
npx next-secret-guard scan --ci --fail-on HIGHExample: run a full scan and output JSON for automation:
npx next-secret-guard scan --json > secret-guard-report.jsonExample: Supabase service role leak
Risky code
// lib/supabase-admin.ts
export const supabaseServiceRoleKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
export async function createAdminClient() {
return {
key: supabaseServiceRoleKey,
};
}// app/components/Settings.tsx
"use client";
import { createAdminClient } from "@/lib/supabase-admin";
export function Settings() {
void createAdminClient();
return <div>Settings</div>;
}Why this is risky:
- the service role key should never be reachable from client code
- importing
supabase-admin.tsinto a Client Component creates a dangerous boundary crossing
Fixed code
// lib/supabase-admin.ts
import "server-only";
export async function createAdminClient() {
return {
key: process.env.SUPABASE_SERVICE_ROLE_KEY,
};
}// app/components/Settings.tsx
"use client";
export function Settings() {
return <div>Settings</div>;
}Move the admin action behind a server action, route handler, or server component boundary instead of importing it into client code.
Example: server-only module imported by a Client Component
Risky code
// lib/server-secrets.ts
export const stripeSecretKey = process.env.STRIPE_SECRET_KEY;
export const openAIKey = process.env.OPENAI_API_KEY;// lib/index.ts
export * from "./server-secrets";// app/components/Billing.tsx
"use client";
import { stripeSecretKey } from "@/lib";
export function Billing() {
return <div>{stripeSecretKey ? "Ready" : "Not ready"}</div>;
}Fixed code
// lib/server-secrets.ts
import "server-only";
export const stripeSecretKey = process.env.STRIPE_SECRET_KEY;
export const openAIKey = process.env.OPENAI_API_KEY;// lib/index.ts
export * from "./public-helpers";// app/components/Billing.tsx
"use client";
export function Billing() {
return <div>Billing UI</div>;
}Keep server-only exports out of shared barrels that can be imported by client-side code.
Configuration
next-secret-guard loads config automatically from the project root in this order:
next-secret-guard.config.tsnext-secret-guard.config.mjsnext-secret-guard.config.js.next-secret-guard.json
You can also point the CLI at an explicit file with --config <path>.
Example config
export default {
include: ["app/**/*.{ts,tsx,js,jsx}", "src/**/*.{ts,tsx,js,jsx}"],
exclude: ["node_modules/**", ".next/**", "dist/**"],
secretPatterns: [
"SECRET",
"TOKEN",
"PRIVATE_KEY",
"SERVICE_ROLE",
"DATABASE_URL"
],
allowedPublicEnv: [
"NEXT_PUBLIC_SUPABASE_URL",
"NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY"
],
serverOnlyPaths: [
"src/server/**",
"server/**"
],
failOn: ["HIGH", "MEDIUM"]
}Field guide
includecontrols which project folders and glob patterns are scanned.excludefilters out build output, dependencies, tests, or other ignored paths.secretPatternsdefines the name fragments that make aNEXT_PUBLIC_variable suspicious.allowedPublicEnvwhitelists public env names that should never be flagged.serverOnlyPathsmarks path patterns that should be treated as server-only modules.failOncontrols which severities cause--cito exit with code 1.
Example overrides
export default {
exclude: ["src/legacy/**"],
allowedPublicEnv: ["NEXT_PUBLIC_API_BASE_URL"],
failOn: ["HIGH"]
}Severity levels
Findings are grouped so you can triage quickly.
HIGH: likely secret exposure or direct client reachability of sensitive materialMEDIUM: a risky import or boundary pattern that could expose secrets in a common refactor pathLOW: a pattern that is probably safe today but deserves attentionINFO: informational guidance or a non-blocking recommendation
Suggested policy:
- fail CI on
HIGH - review
MEDIUMbefore merge - surface
LOWandINFOin PR comments or local scans
Expected CLI output
Example text output:
next-secret-guard scan
Root: /path/to/project
Files scanned: 42
Fail on: HIGH, MEDIUM
Issues found: 4
Issues
... issue tables ...
Summary
... summary table with colored severity labels ...
Exit code: 1 when an issue matches failOn, otherwise 0Example JSON output:
{
"summary": {
"filesScanned": 42,
"high": 1,
"medium": 1,
"low": 1,
"info": 1
},
"findings": [
{
"severity": "HIGH",
"file": "app/components/Settings.tsx",
"message": "Client Component imports lib/supabase-admin.ts",
"ruleId": "client-boundary-import"
}
]
}CI usage
GitHub Actions example:
name: Secret Guard
on:
pull_request:
push:
branches:
- main
jobs:
scan:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
- name: Scan for secret exposure risks
run: npx next-secret-guard scan --ciIf you want PR-friendly JSON for annotation workflows, use:
- name: Scan for secret exposure risks
run: npx next-secret-guard scan --format jsonUse --fail-on only if you want to override the configured CI thresholds for a specific run.
Provider presets
The tool includes provider-aware checks for common Next.js stacks.
Supabase
What to watch for:
SUPABASE_SERVICE_ROLE_KEYin any code that can be reached from the client- server admin helpers imported into shared barrels
- client components indirectly importing server-only Supabase helpers
Risky example:
export const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
);Safer pattern:
import "server-only";
export const supabaseAdmin = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!
);Stripe
What to watch for:
STRIPE_SECRET_KEYimported by client-reachable code- payment helpers shared across server and client boundaries
Risky example:
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-06-20"
});If this file is imported into client-reachable code, the secret should be considered at risk.
Safer pattern:
import "server-only";
export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: "2024-06-20"
});Prisma
What to watch for:
DATABASE_URLorDIRECT_URLin code reachable from the browser bundle- Prisma client helpers exported from shared modules
Risky example:
export const dbUrl = process.env.DATABASE_URL;Safer pattern:
import "server-only";
export const prisma = new PrismaClient();OpenAI
What to watch for:
OPENAI_API_KEYin client-reachable helpers- AI utilities that are safe for server use but accidentally imported into the browser
Risky example:
export const openaiKey = process.env.OPENAI_API_KEY;Safer pattern:
import "server-only";
export const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY
});Clerk / Auth.js style secrets
What to watch for:
CLERK_SECRET_KEY,AUTH_SECRET,NEXTAUTH_SECRET, or similar secrets in client-reachable code- auth helper modules shared across both sides of the boundary
Risky example:
export const authSecret = process.env.AUTH_SECRET;Safer pattern:
import "server-only";
export const authOptions = {
secret: process.env.AUTH_SECRET
};How it works
At a high level, next-secret-guard checks for:
- sensitive environment variable patterns
- import graphs that cross from client code into server-only modules
- module reachability that could pull server-only dependencies into browser bundles
- provider-specific naming patterns that commonly correspond to secrets
The goal is practical detection, not perfect proof. It is meant to be fast enough to run locally and in CI.
Roadmap
Planned or likely future improvements:
- richer import graph analysis for larger Next.js monorepos
- better reporting for App Router and nested boundary cases
- SARIF output for code scanning platforms
- inline ignore comments for justified exceptions
- framework-aware presets for common Next.js ecosystems
- GitHub PR annotations
- support for custom rule packs
Contributing
Contributions are welcome.
Suggested workflow:
- Fork or branch from
main - Make a focused change
- Add or update tests if behavior changes
- Run the scanner or test suite locally
- Open a pull request with a clear explanation of the risk being addressed
When contributing, please keep the project scope tight:
- optimize for Next.js secret-exposure guardrails
- avoid turning the project into a generic security platform
- prefer clear, actionable findings over noisy warnings
Before publishing, run npm pack --dry-run and confirm the tarball only includes dist, README.md, LICENSE, SECURITY.md, and CHANGELOG.md alongside the required package.json.
For the full contributor workflow and release checklist, see CONTRIBUTING.md in the repository.
Security
If you discover a security issue in the package itself, please report it responsibly.
Recommended disclosure process:
- Do not open a public issue with exploit details
- Contact the maintainers privately with the affected file, version, and reproduction steps
- Allow time for review, fix, and release coordination
This project is intended to reduce accidental secret exposure, but it is not a complete security audit and it cannot guarantee that a secret has not already been leaked elsewhere.
License
MIT License.
See the LICENSE file if one is present in this repository.
