Skip to content

Instantly share code, notes, and snippets.

@ustun
Last active March 25, 2026 10:45
Show Gist options
  • Select an option

  • Save ustun/ece8a17ab61028786c1366127cad1569 to your computer and use it in GitHub Desktop.

Select an option

Save ustun/ece8a17ab61028786c1366127cad1569 to your computer and use it in GitHub Desktop.
Find Plaintext Secrets Hiding in Your `.env` Files: https://dev.to/ustun/find-plaintext-secrets-hiding-in-your-env-files-5dpl
#!/usr/bin/env bash
set -euo pipefail
ROOT="${HOME}"
PATTERN="token|api|key"
usage() {
cat <<'EOF'
Scan for env files under a root directory that contain sensitive-looking patterns.
Usage:
scripts/find-env-secret-patterns.sh [options]
Options:
--root DIR Root directory to scan (default: $HOME)
--pattern REGEX Case-insensitive regex to search for (default: token|api|key)
-h, --help Show this help
Scanned filenames:
.env
.env.production
.env.local
.env.local.secrets
Ignored directories:
node_modules, .git, .hg, .svn, .next, .turbo, dist, build, coverage,
.cache, .npm, .pnpm-store, .yarn, Library, Trash
Examples:
scripts/find-env-secret-patterns.sh
scripts/find-env-secret-patterns.sh --root ~/code
scripts/find-env-secret-patterns.sh --pattern 'token|secret|api|key'
Notes:
Matching lines containing 'encrypted:', 'DOTENV_PUBLIC_KEY', or
'public-key encryption for .env files', or
'USE_KEYCHAIN_FOR_DOTX' are ignored.
`.env.local.secrets` reports only env-style `key=value` lines that do
not contain `encrypted:`.
Reported matches redact everything after the first '='.
EOF
}
is_env_local_secrets_file() {
local file_path="$1"
[[ "${file_path##*/}" == ".env.local.secrets" ]]
}
should_skip_match_line() {
local line_content_lower="${1,,}"
local skip_pattern
for skip_pattern in \
"encrypted:" \
"dotenv_public_key" \
"public-key encryption for .env files" \
"use_keychain_for_dotx"; do
if [[ "$line_content_lower" == *"$skip_pattern"* ]]; then
return 0
fi
done
return 1
}
is_env_assignment_line() {
local line_content="$1"
[[ "$line_content" =~ ^[[:space:]#]*((export[[:space:]]+)?[A-Za-z_][A-Za-z0-9_]*[[:space:]]*=) ]]
}
redact_match_line() {
local line_content="$1"
if [[ "$line_content" == *"="* ]]; then
printf '%s[REDACTED]' "${line_content%%=*}="
return
fi
printf '[REDACTED]'
}
while [[ $# -gt 0 ]]; do
case "$1" in
--root)
ROOT="${2:-}"
shift 2
;;
--pattern)
PATTERN="${2:-}"
shift 2
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1" >&2
usage >&2
exit 1
;;
esac
done
if [[ -z "$ROOT" ]]; then
echo "Error: --root cannot be empty." >&2
exit 1
fi
if [[ -z "$PATTERN" ]]; then
echo "Error: --pattern cannot be empty." >&2
exit 1
fi
if [[ ! -d "$ROOT" ]]; then
echo "Error: root directory not found: $ROOT" >&2
exit 1
fi
MATCHING_FILES="$(mktemp)"
trap 'rm -f "$MATCHING_FILES"' EXIT
find "$ROOT" \
\( \
-name node_modules -o \
-name .git -o \
-name .hg -o \
-name .svn -o \
-name .next -o \
-name .turbo -o \
-name dist -o \
-name build -o \
-name coverage -o \
-name .cache -o \
-name .npm -o \
-name .pnpm-store -o \
-name .yarn -o \
-name Library -o \
-name .Trash -o \
-name Trash \
\) -prune \
-o -type f \
\( \
-name .env -o \
-name .env.production -o \
-name .env.local -o \
-name .env.local.secrets \
\) -print0 >"$MATCHING_FILES"
if [[ ! -s "$MATCHING_FILES" ]]; then
echo "No matching env files found under $ROOT."
exit 0
fi
FOUND_MATCH=0
HAD_ERROR=0
while IFS= read -r -d '' file_path; do
set +e
RG_OUTPUT="$(
rg \
--ignore-case \
--line-number \
--color never \
-- "$PATTERN" "$file_path" 2>&1
)"
RG_STATUS=$?
set -e
case "$RG_STATUS" in
0)
while IFS= read -r rg_line; do
[[ -z "$rg_line" ]] && continue
line_number="${rg_line%%:*}"
line_content="${rg_line#*:}"
if should_skip_match_line "$line_content"; then
continue
fi
if is_env_local_secrets_file "$file_path" && ! is_env_assignment_line "$line_content"; then
continue
fi
printf '%s:%s:%s\n' "$file_path" "$line_number" "$(redact_match_line "$line_content")"
FOUND_MATCH=1
done <<<"$RG_OUTPUT"
;;
1)
;;
*)
printf 'Error scanning %s\n%s\n' "$file_path" "$RG_OUTPUT" >&2
HAD_ERROR=1
;;
esac
done <"$MATCHING_FILES"
if [[ "$HAD_ERROR" -ne 0 ]]; then
exit 1
fi
if [[ "$FOUND_MATCH" -eq 1 ]]; then
exit 0
fi
echo "No matching lines found for pattern '$PATTERN' under $ROOT."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment