H Heygents Docs Open App

Updated July 31, 2026 · Security · High severity

Secrets committed to git history and world-readable env files

how do I check if secrets are committed to my git history Yes, run a history scan - `git log -p` plus a secret-scanning tool catches keys committed in any past commit, even if deleted later, because git never removes old blobs. Rotate every exposed credential first, then fix file modes and .gitignore, then rewrite history with git filter-repo or BFG and force-push.

Seen in 25 of 450 scanned projects (6%). Based on 450 Deep Scan runs across production Node and Postgres projects.

Common stacks: git env node postgres ci

How to tell you have it

  • .env.production is readable by any local user on the server (mode 644 or 664)
  • grep for a known API key prefix finds a hit in an old commit that no longer exists on disk
  • stray .env.bak or .env.old files sit next to the real .env
  • .gitignore is missing or does not list .env variants
  • the app falls back to a hardcoded default key when env loading fails at startup

Why it matters

Git is append-only by design. Deleting a secret in a new commit only removes it from the latest tree, the blob with the plaintext key still lives in every earlier commit and in the packed history that gets cloned along with the repo. Anyone with read access to the repository, including a contractor from a year ago or a leaked backup, can run `git log -p` and pull the value straight out.

A world-readable .env.production (mode 644) turns a single compromised shell account, a misconfigured backup job, or a shared hosting box into a full credential leak. A service-role key in that file bypasses row-level security entirely, so an attacker with read access to the file gets unrestricted database access, not just the access your app's normal API grants.

Stray .env.bak, .env.old, or editor swap files are the most common accidental leak vector. They are created by hand during debugging, forgotten, and picked up by the next `git add .` because there is no .gitignore rule blocking them. Once committed, they carry the same history-persistence problem as the primary .env.

A hardcoded fallback credential (used when env loading throws or a variable is unset) is worse than a missing variable, because the app keeps running with a known, guessable value instead of failing loudly. That fallback often ends up in the git history too, and it does not rotate when the real credential does, so it stays valid as a silent backdoor indefinitely.

How Heygents detects it

Deep Scan runs git log --all -p and git ls-files against the working tree to look for .env variants and key-shaped strings in current and historical commits, checks file modes with ls -la on .env and .env.production, and confirms .gitignore coverage for env files.

How to fix it

  1. Rotate every exposed credential before touching git History rewriting does not un-leak a secret that was already pushed to a remote, cloned, or cached by a CI runner. Assume anything that ever appeared in a commit is compromised and rotate it in the provider dashboard first. Only after rotation is the old value safe to leave in history while you clean up.
  2. Fix file permissions on env files Restrict .env and .env.production to the owning user only. World or group readable env files are a common finding on shared hosts and on servers where a deploy script runs as a different user than the app.
  3. Add or fix .gitignore and remove stray backup files Cover every env variant so a future 'quick backup' copy cannot slip into a commit. Delete stray .env.bak/.env.old files from disk, they serve no purpose and only add exposure.
  4. Scan the full history for secrets, not just the working tree A working-tree grep misses anything already deleted. Search the full commit history including all branches, or use a dedicated scanner for known key formats.
  5. Rewrite history to remove the file, then force-push Deleting a file in a new commit leaves every prior commit untouched, so the secret is still fetchable by anyone who clones or fetches the repo. Only a history rewrite followed by a force-push actually removes the blob from the remote. Every collaborator must then re-clone, since their local history no longer matches.
  6. Remove hardcoded fallback credentials and fail loudly instead Replace any default value used when an env var is missing with a startup check that exits immediately. A crashed app on a misconfigured server is far cheaper than a silently running one on a known key.

Rotate every exposed credential before touching git

# Supabase example: rotate service role key from the dashboard,
# then update the running app
ssh user@host "pm2 set <app>:service_role_key <new_key> && pm2 restart <app>"

Fix file permissions on env files

chmod 600 /var/www/<app>/.env.production
chown appuser:appuser /var/www/<app>/.env.production
ls -la /var/www/<app>/.env.production

Add or fix .gitignore and remove stray backup files

.env
.env.*
!.env.example
*.bak
*.old

Scan the full history for secrets, not just the working tree

git log --all -p -- '*.env*' | grep -iE 'key|secret|password|token'
# or, with a scanner installed:
# gitleaks detect --source . --log-opts=--all

Rewrite history to remove the file, then force-push

git filter-repo --path .env.production --invert-paths
git push origin --force --all
git push origin --force --tags

Remove hardcoded fallback credentials and fail loudly instead

if (!process.env.SERVICE_ROLE_KEY) {
  throw new Error('SERVICE_ROLE_KEY is not set, refusing to start');
}

Find this in your own projects, automatically

Heygents runs a read-only Deep Scan across every project you own, finds issues like this one, and hands you a ready-to-run fix an AI agent can execute and verify. A solo developer gets the audit, the backlog and the fix loop in one place.

Open Heygents →

Frequently asked questions

If I delete the .env file in a new commit, is the secret gone?

No. Git keeps every past commit's content in the repository's object store, so the secret is still present in the old blob and reachable with git log -p or by checking out an earlier commit. Deleting the file only stops it from appearing in the current working tree. You must rewrite history and force-push to actually remove it, and you should rotate the credential regardless.

Do I need to rotate the key if I am going to rewrite history anyway?

Yes, always rotate first. Anyone who already cloned the repo, forked it, or has it cached in a CI artifact keeps the old history with the secret intact, even after you force-push a rewritten version to the main remote. Rotation is the only step that actually invalidates the leaked value; history rewriting just limits future exposure.

What file mode should .env and .env.production have on a production server?

Use 600, readable and writable only by the user the application runs as. Mode 644 or 664 lets any local account on the box read the file, which matters on shared hosts, multi-tenant servers, or boxes where deploy scripts and cron jobs run under different users than the app itself.

Why does a hardcoded fallback credential matter if the real .env is set correctly?

A fallback only fires when env loading fails, which can happen after a deploy misconfiguration, a renamed variable, or a broken .env parse. The app keeps running instead of crashing, silently using a known value that is often visible in the source code itself. It also does not get rotated alongside the real credential, so it can remain valid as an unmonitored access path indefinitely.