API Key Exposed in Code — What to Do Right Now
Quick answer
💡If you have committed an API key to a git repository, treat it as compromised immediately — even if the commit was private and you deleted it seconds later. Rotate the key at the provider first, then remove it from the codebase and git history. Do not reverse the order: rotating first ensures the leaked key is unusable before you spend time cleaning up history.
Error symptoms
- ✕
A secret or API key appears in a git commit, pull request diff, or CI log - ✕
GitHub sends a Secret Scanning alert email about a detected credential - ✕
An API provider sends a key compromise notification after detecting unusual usage - ✕
A code review or automated scan flags a hardcoded credential in source code - ✕
Unexpected API usage or billing charges that indicate an exposed key is being used - ✕
A .env file was accidentally committed and pushed to a public or private repository
Common causes
- •Hardcoding a key directly in source code during development and forgetting to move it to env vars
- •Committing a .env file because it was not in .gitignore before the first commit
- •Logging request headers or API responses that include Authorization tokens
- •Pasting a key into a code sharing site, chat tool, or public forum while debugging
- •Using NEXT_PUBLIC_ prefix in Next.js for a secret that should stay server-side only
- •Storing keys in Docker image layers, CI artifact archives, or build output files
When it happens
- •During early project setup before .gitignore and environment variable patterns are established
- •When a developer copies example code from documentation that includes placeholder keys they replace with real ones
- •After a git rebase or history rewrite where deleted secrets reappear in rebased commits
- •When CI/CD debug logging is enabled and secrets from environment variables appear in build output
Examples and fixes
Hardcoded keys in source files are exposed to anyone with repository access and are captured permanently in git history.
Moving a hardcoded key to an environment variable
❌ Wrong
// src/lib/stripe.ts — key hardcoded in source
import Stripe from 'stripe';
const stripe = new Stripe('sk_live_51AbcDef...XYZ', {
apiVersion: '2024-06-20',
});
export async function createCheckoutSession(priceId: string) {
return stripe.checkout.sessions.create({
mode: 'subscription',
line_items: [{ price: priceId, quantity: 1 }],
success_url: 'https://app.example.com/success',
});
}✅ Fixed
// .env.local (add to .gitignore — never commit this file)
// STRIPE_SECRET_KEY=sk_live_51AbcDef...XYZ
// src/lib/stripe.ts — key from environment
import Stripe from 'stripe';
if (!process.env.STRIPE_SECRET_KEY) {
throw new Error('STRIPE_SECRET_KEY is not set');
}
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
apiVersion: '2024-06-20',
});
export async function createCheckoutSession(priceId: string) {
return stripe.checkout.sessions.create({
mode: 'subscription',
line_items: [{ price: priceId, quantity: 1 }],
success_url: 'https://app.example.com/success',
});
}The environment variable approach keeps the secret value completely out of your codebase. The .env.local file stores the actual value locally and must be listed in .gitignore before the repository's first commit — adding it to .gitignore after the file has already been committed does not remove it from git history. The process.env guard at startup catches missing configuration early rather than failing at runtime when a payment is attempted. Commit a .env.example file with placeholder values so other developers know which variables to configure.
A .env file committed once is in git history permanently, even after deletion.
Preventing .env from being committed
❌ Wrong
# .gitignore — missing .env rules when project started
node_modules/
.next/
dist/
# .env.local was committed with:
STRIPE_SECRET_KEY=sk_live_51AbcDef...
OPENAI_API_KEY=sk-proj-AbcXyz...
DATABASE_URL=postgres://user:password@host/db✅ Fixed
# .gitignore — env files excluded before first commit
node_modules/
.next/
dist/
# Environment variable files — never commit these
.env
.env.local
.env.*.local
.env.development
.env.production
.env.test
# Safe to commit — contains only placeholder values
# .env.example is committed with: STRIPE_SECRET_KEY=your_key_hereThe critical timing issue is that .gitignore only prevents files from being added to future commits — it does not remove files that have already been committed. If .env was committed even once, the secrets in it are permanently in git history and accessible to anyone who can clone the repository. The fix for an already-committed .env file is git filter-repo to rewrite history (a destructive operation requiring coordination with your team), or simply rotating all the exposed keys. Both actions are necessary.
Why Exposed Keys in Git Are Permanent
Git stores every version of every file you have ever committed. When you commit a file containing a secret, that commit object is stored in the repository's object database. Even if you immediately create a follow-up commit that deletes the file or removes the key, the original commit continues to exist. Anyone with access to the repository can check out that commit, view the file, and read the secret.
This permanence is especially dangerous for public repositories. GitHub, GitLab, and Bitbucket provide public repositories that can be cloned by anyone on the internet. If a secret is pushed to a public repository for even a few seconds before being deleted, automated bots that continuously monitor public repositories for high-value credentials may already have captured it. This is not a hypothetical — AWS, Stripe, GitHub, and other providers all operate secret scanning systems that monitor public repositories in real time and notify both the affected user and the service provider.
For private repositories, the risk is lower but not zero. Every team member with read access to the repository can see the full git history. If the repository is ever made public, forked by a security researcher, or its access controls are misconfigured, the exposed key becomes public. Past employees who had repository access retain the git objects they cloned before losing access.
Environment variables are the standard solution. In Node.js, process.env.MY_KEY reads a value injected by the operating system's process environment rather than sourced from a file in the repository. In production environments like Vercel, Heroku, and AWS, secrets are configured through the platform's secrets management interface and injected as environment variables at runtime. The source code never contains the secret value — only the variable name.
Auditing Your Repository for Exposed Secrets
Start by searching git history for patterns that look like secrets. The command git log -p --all -- followed by a file path shows the full diff history for that file. For a broader search, git log --all --full-history -- .env shows all commits that touched any .env file. Use git show <commit-hash>:.env to view the file content at a specific historical commit.
For automated scanning, use tools like gitleaks, truffleHog, or git-secrets. Gitleaks scans the full git history of a repository for common secret patterns: API keys, tokens, private keys, and connection strings. Run it as gitleaks detect --source . to scan the current repository. It outputs a report of every detected secret with the commit hash and line number. Install it as a pre-commit hook to prevent future commits with detected secrets.
GitHub provides built-in Secret Scanning for all repositories (free and paid). Navigate to your repository's Settings tab, then Security and Analysis, and ensure Secret Scanning is enabled. GitHub scans every push for known API key formats from major providers including AWS, Stripe, Google, GitHub, Slack, and hundreds of others. When a secret is detected, GitHub notifies the repository owner and, for public repositories, notifies the service provider so they can revoke the key.
Check your CI/CD logs as well. Pipeline logs often capture environment variables in debug output, especially when a step runs with verbose logging or when an error causes a full environment dump. Review recent build logs for any that contain long alphanumeric strings resembling API keys. Most CI platforms (GitHub Actions, CircleCI, GitLab CI) automatically redact known secret formats in log output, but custom secrets or unusual formats may slip through.
Immediate Response Steps When a Key Is Exposed
Step one: rotate the key immediately. Do not wait to clean up the code or git history first. Go to the API provider's dashboard and revoke the exposed key, then generate a new one. This ensures the exposed credential is useless even if someone has already captured it from your repository. For AWS, use the IAM console to deactivate the access key. For Stripe, revoke the key in the Developers section. For OpenAI, delete the key in the API keys management panel.
Step two: update your environment configuration. Add the new key to your local .env file and to your production environment (Vercel, AWS Secrets Manager, Heroku Config Vars, etc.). Test that the application works with the new key before proceeding to history cleanup. Confirm the old key is fully revoked by checking that any application still configured with it fails to authenticate.
Step three: remove the secret from git history. The recommended tool for this is git filter-repo (the modern replacement for git filter-branch). Run: git filter-repo --path .env --invert-paths to remove the .env file from all commits. For removing a secret from a specific file without removing the file itself: git filter-repo --textconv-path script that replaces the secret with a placeholder. After rewriting history, force-push to the remote with git push --force-with-lease and notify all team members to re-clone the repository, as their local clones now have diverged history.
Step four: add preventative measures so this does not happen again. Install gitleaks as a pre-commit hook: gitleaks protect --staged will scan staged files before each commit. Add .env* to .gitignore and commit a .env.example file with placeholder values. For Next.js projects, review every environment variable that has the NEXT_PUBLIC_ prefix — those values are embedded in the client-side JavaScript bundle and visible to end users, so only non-sensitive values should carry that prefix.
Tricky Situations: Forks, Archives, and Build Artifacts
Public repository forks present a problem that git history rewriting cannot solve. If your repository was public and another user forked it before you removed the secret, their fork still contains the original git objects including the commit with the exposed key. GitHub does not automatically propagate history rewrites to forks. Revoking the exposed key is the only effective remediation — the historical data in forks cannot be removed.
GitHub allows you to contact support to quarantine a repository that has had sensitive data exposed, which can help prevent further forking while you remediate. However, this is a time-sensitive process and any forks created before quarantine retain the data. The lesson is that rotating the key is not optional — it is the one action guaranteed to make the exposed credential useless regardless of how many copies exist.
Docker images are a frequently overlooked vector. If a secret is added to a Docker image as an environment variable at build time using ENV MY_KEY=secret, it is baked into the image layer and visible to anyone with access to pull the image. Even if the layer is not the top layer, docker history and docker save can extract it. Secrets should never be provided to Docker builds with ARG or ENV — they should be injected at container runtime via the orchestrator (Kubernetes secrets, Docker Swarm secrets, or the platform's environment variable injection).
CI/CD artifact storage is another risk. Build artifacts (zip files, compiled binaries, log archives) stored in CI systems sometimes capture environment variable values in build logs or configuration files. Review what your CI system stores as artifacts and for how long. Many CI platforms allow you to configure log redaction patterns to mask secrets that are not automatically detected.
Common Mistakes After Discovering an Exposed Key
The most dangerous mistake is deleting the file or commit without rotating the key first. Deleting the .env file from the repository makes the secret harder to find in git history, but anyone who already cloned the repository has the key in their local object store. More importantly, automated scrapers may have already captured the key. Rotation must come first.
Another common mistake is using git revert instead of git filter-repo to remove a secret. git revert creates a new commit that undoes the changes, but it does not remove the original commit from history — it simply adds a new commit on top. The secret is still visible in the reverted commit. Only history rewriting with git filter-repo or equivalent actually removes the secret from the repository's object database.
Adding .gitignore entries for .env after the file has already been committed is ineffective. Git ignores files that have not yet been tracked, but once a file is in git's index, .gitignore has no effect on it. To stop tracking a previously committed file, you must run git rm --cached .env to remove it from the index, then commit the removal. After that, .gitignore will prevent future accidental additions.
Trusting that a private repository is safe because only trusted team members have access is a false sense of security. Access controls can change — employees leave, repository visibility can be accidentally changed to public, and GitHub/GitLab permissions can be misconfigured in complex organizations. Treat every secret that enters a repository as if that repository could become public at any time, and design your secret management accordingly.
Preventing Secret Exposure From the Start
Establish your .gitignore and environment variable patterns before the first commit. The GitHub gitignore templates for Node.js, Python, and other languages already include .env and .env.local patterns. Use the appropriate template when creating a repository. Add a .env.example file that lists all required environment variables with placeholder values — this documents the configuration interface without revealing any actual secrets.
Use a secrets manager for production deployments rather than storing secrets in environment variable configuration files on servers. AWS Secrets Manager, HashiCorp Vault, Doppler, and 1Password Secrets Automation all provide APIs for retrieving secrets at runtime. Your application authenticates to the secrets manager using a role or service account credential (which itself should never be a static API key), and retrieves the actual secrets at startup. This approach means rotating a secret requires only updating the secrets manager entry, not re-deploying the application.
Install pre-commit hooks that scan staged files for secrets before every commit. Gitleaks and git-secrets both support pre-commit mode. With Husky or lefthook, add the scanner to your project's git hooks so every developer on the team runs the check automatically on every commit. Configure CI to run the same scan on every pull request as an additional check.
For Next.js projects specifically, understand the NEXT_PUBLIC_ naming convention deeply. Any environment variable prefixed with NEXT_PUBLIC_ is intentionally embedded in the client-side JavaScript bundle and visible to anyone who inspects the page source or JavaScript files. It is appropriate for public API endpoints, analytics IDs, and public configuration values. It is never appropriate for secrets, private API keys, database credentials, or signing secrets. All sensitive values must use server-side environment variables without the NEXT_PUBLIC_ prefix.
Exposed API key response checklist
- ✓Rotate the exposed key at the API provider FIRST, before any other action
- ✓Update the new key in all environments: local .env, Vercel, AWS, or your secrets manager
- ✓Verify the old key is fully revoked by testing a request with it — it should fail
- ✓Add .env and all .env variants to .gitignore if not already present
- ✓Run git filter-repo to remove the secret from all commits in git history
- ✓Force-push the rewritten history and notify all team members to re-clone
- ✓Install gitleaks or git-secrets as a pre-commit hook to prevent future exposures
- ✓Commit a .env.example file with placeholder values to document required configuration
Related guides
Frequently asked questions
I deleted the commit with the API key. Is the key still exposed?
Yes. Deleting a commit through git revert, git reset, or removing the file in a new commit does not erase the original commit from git history. Anyone who cloned the repository before the deletion still has the original commit objects locally. For public repositories, bots may have already captured the key. Treat the key as fully compromised and rotate it at the provider immediately — deletion alone does not protect you.
What is the safest way to remove a secret from git history?
Use git filter-repo, the officially recommended replacement for git filter-branch. It rewrites the entire repository history to remove the specified file or text pattern from all commits. After rewriting, force-push to all remotes. All existing clones become invalid and must be re-cloned. For GitHub, you can also contact support to help with cleanup on their end. This is a significant operation that requires team coordination — document it and notify all contributors before doing it.
Can GitHub Secret Scanning remove an exposed key for me?
No. GitHub Secret Scanning detects exposed credentials and notifies you and the affected service provider — it does not remove the secret from your repository or revoke it. When GitHub detects a key, it may alert the service provider (Stripe, AWS, etc.), who may auto-revoke the key for known patterns. Do not rely on this automation. Treat every detected secret as requiring immediate manual rotation on your end, followed by history cleanup.
What is the difference between .env and .env.local in Next.js?
.env is a generic environment file loaded in all environments. .env.local is a local override file that should never be committed and is loaded on top of .env. Next.js also supports .env.development and .env.production for environment-specific values. All of these can contain sensitive values and must be in .gitignore. Only .env.example — containing placeholder documentation without real values — should be committed to the repository.
How do I share secrets with my team without committing them?
Use a password manager with sharing capabilities (1Password Teams, Bitwarden Organizations) or a dedicated secrets management tool (Doppler, Infisical, HashiCorp Vault). Each developer retrieves the secrets from the shared manager and stores them in their local .env file. For production deployments, inject secrets directly from the secrets manager into the runtime environment — never through the repository. Never share secrets via Slack, email, or code review comments.
Is it safe to log environment variables in CI for debugging?
No. CI logs are often accessible to all repository collaborators, and in public repositories they can be publicly visible. Most CI platforms (GitHub Actions, CircleCI, GitLab CI) automatically redact secrets that are registered as protected variables or secret context values. However, if you echo or print environment variables directly in a script step, the platform may not detect the redaction pattern and the value may appear in plain text. Never log the contents of environment variables — log only the variable name and whether it is set.
What should I do if GitHub sends me a Secret Scanning alert?
Rotate the key immediately at the API provider — do not wait. Once the new key is configured in your application, click Dismiss or Revoke in the GitHub Security tab for the alert. If the repository is public, assume the key was captured by automated scanners and treat it as compromised even if you rotated it within minutes. Review git history to confirm no other secrets are exposed in the same or related commits, and add pre-commit scanning to prevent future occurrences.
How do I prevent secrets in Docker image builds?
Never use ENV or ARG instructions to pass secrets into a Docker build — values set with ENV are stored in the image layer and visible via docker inspect. For build-time secrets (e.g., a token to pull a private npm package), use docker buildx with the --secret flag, which mounts secrets in a temporary filesystem not written to any layer. For runtime secrets, inject them via the container orchestrator (Kubernetes Secrets, Docker Swarm secrets) as environment variables at container startup, not at image build time.
All tools run in your browser. Your data never leaves your device. Last updated: 2026-05-06.