This is a companion to my earlier post, A Small Hardening Trick for .env.local: dotenvx + OS Keychain, where I described a pattern for encrypting local secrets with dotenvx and storing the decryption key in the OS keychain.
That post was about fixing the problem. This one is about finding it first.
The problem with "just gitignore it"
If you have been developing locally for a while, you probably have .env files
scattered across dozens of project directories. Some are recent, some are from
projects you have not touched in months. Some contain harmless config. Some
contain database URLs, API keys, and auth secrets in plaintext.
The trouble is that you do not always know which is which, especially once you
have 10 or 20 repos checked out under ~/code. And if a supply chain attack or
a compromised tool scans your filesystem, it does not care whether you remember
what is in those files.
I wanted a quick way to answer one question: where on my machine are plaintext
secrets sitting in .env files right now?
The script
I wrote a small bash script that walks a directory tree, finds .env files, and
reports lines that look like they contain secrets. It is deliberately simple,
opinionated, and safe to run.
The full script is at the end of this post. Here is what it does.
What it scans:
It looks for files named .env, .env.production, .env.local, and
.env.local.secrets. It skips node_modules, .git, build output, caches,
and other directories you would never want to crawl.
How it matches:
It uses rg (ripgrep) to find lines matching a pattern. The default pattern is
token|api|key, which catches most secret-looking variable names without
generating too much noise. You can override it:
bash find-env-secret-patterns.sh --pattern 'token|secret|api|key|password'
What it skips:
Lines containing encrypted: (already handled by dotenvx), DOTENV_PUBLIC_KEY
(not a secret), USE_KEYCHAIN_FOR_DOTX (a config flag, not a credential), and
the dotenvx public-key header comment are all ignored. For .env.local.secrets
files specifically, it only reports lines that look like env-style assignments
(KEY=value), since the rest of an encrypted file is ciphertext noise.
What it outputs:
Every match is printed as filepath:line_number:KEY=[REDACTED]. The script
redacts everything after the = sign, so you can share the output or paste it
into a ticket without leaking actual values.
Usage
# scan your entire home directory (the default)
bash find-env-secret-patterns.sh
# scan only your code directory
bash find-env-secret-patterns.sh --root ~/code
# use a broader pattern
bash find-env-secret-patterns.sh --pattern 'token|secret|api|key|password|credential'
Example output:
/Users/you/code/project-a/.env.local:3:GOOGLE_CLIENT_SECRET=[REDACTED]
/Users/you/code/project-a/.env.local:5:POSTGRES_URL=[REDACTED]
/Users/you/code/project-b/.env:2:STRIPE_API_KEY=[REDACTED]
/Users/you/code/old-project/.env.production:8:AUTH_TOKEN=[REDACTED]
Each of those lines is a plaintext secret sitting on your disk.
What to do with the results
The output tells you where your exposure is. From there you have a few options
depending on how much effort you want to put in:
For active projects, this is a good prompt to adopt the dotenvx + OS keychain
pattern from the first post: Split secrets into .env.local.secrets, encrypt them, move the key into your keychain, and those files will stop showing up in future scans.
For old projects you are not actively working on, consider whether those .env
files need to exist at all. If you have not touched a project in six months, the
credentials in its .env.local might be stale, but they might also still be
valid. Deleting or encrypting them reduces your surface.
For production env files (.env.production), those should probably not be on
your machine in plaintext at all. If they are showing up in the scan, that is
worth a closer look at how your deployment pipeline distributes config.
Running it periodically
You could add this to a cron job or a weekly reminder. The script exits with
code 0 on success (whether or not it found matches) and code 1 on errors, so
it plays well with automation. If you want to fail a CI check when plaintext
secrets are found, you could adjust the exit code for the FOUND_MATCH case.
Design decisions
A few things I was deliberate about:
Redaction by default. The script never prints secret values. It shows you the
variable name and the file location, which is enough to act on. This means you
can pipe the output into a log or share it with a teammate without worrying.
rg over grep. ripgrep is faster on large directory trees and handles
null-delimited input cleanly. If you do not have rg installed, brew install or your package manager equivalent.
ripgrep
Skipping dotenvx artifacts. If you have already encrypted a file with
dotenvx, the script should not flag it. The skip patterns for encrypted: and
DOTENV_PUBLIC_KEY handle this. The .env.local.secrets special casing is there
because an encrypted file still has key-looking variable names in its ciphertext
lines, but those are not plaintext secrets.
find with -print0 and null-delimited reads. Filenames with spaces or
special characters will not break the script. This matters less for .env files
specifically, but it is a good habit.
The full script
The complete script is in this gist:
find-env-secret-patterns.sh
Closing thought
Encrypting secrets is the fix. But knowing where unencrypted secrets are sitting
is the prerequisite. You cannot harden what you have not inventoried.
Run this once across your home directory and see what comes back. Then do the hardening steps in the previous post.
Top comments (0)