Guard Scanning¶
Use envdrift guard as a last line of defense against plaintext secrets.
What guard checks¶
- Unencrypted env files missing dotenvx or SOPS markers
- Common secret patterns in source and config files
- High-entropy strings (env files by default; all files with
--entropy, off with--no-entropy/check_entropy = false) - Git history for previously committed secrets (optional)
Which files count as env files¶
The native scanner applies the unencrypted-file policy — and, unless entropy
detection is disabled (check_entropy = false / --no-entropy), the entropy
scan — to every file whose name matches an env-file shape:
.envand.env.<environment>— including.env.localand.env.test, the canonical real-secrets files for Next.js/Vite/CRA local development<name>.env(e.g.production.env,database.env) — thedocker --env-file, direnv, and CI convention- Custom env files declared in
vault.syncmappings
Only the template names .env.example, .env.sample, and .env.template are skipped
by default, since they hold placeholder values by convention.
Env files are scanned regardless of text encoding instead of being misclassified as binary and skipped: UTF-8, and UTF-16 (the Windows Notepad and PowerShell "Unicode" default) whether or not it carries a BOM, are decoded before pattern matching. A BOM-prefixed UTF-32 file is also decoded; BOM-less UTF-32 (which has no reliable byte signature to detect) is the one encoding still treated as binary.
Quick start¶
Run a default scan in the current directory:
Run without external tools:
Run in CI with a strict threshold:
Choosing scanners¶
By default, guard runs the native scanner and gitleaks. You can enable additional scanners with CLI flags:
For maximum detection including password hashes:
For entropy-based and encoded content detection:
For comprehensive multi-target security scanning:
For 140+ secret types with git history support:
You can also enable scanners in envdrift.toml:
[guard]
scanners = ["native", "gitleaks", "trufflehog", "detect-secrets", "kingfisher", "talisman", "trivy", "infisical"]
Scanner comparison¶
| Scanner | Strengths |
|---|---|
| native | Fast, zero dependencies, unencrypted .env detection |
| gitleaks | Great pattern coverage, fast |
| trufflehog | Service-specific tokens (GitHub, Slack, AWS) |
| detect-secrets | 27+ plugin detectors, keyword scanning |
| kingfisher | 700+ rules, password hashes, secret validation |
| talisman | Entropy detection, encoded content, file analysis |
| trivy | Comprehensive multi-target scanning, severity filtering |
| infisical | 140+ secret types, git history, staged changes |
Reporting and CI¶
Generate SARIF output for code scanning systems:
See the CI/CD Integration guide for upload examples.
Configuration¶
Guard configuration lives under [guard]:
[guard]
auto_install = true
include_history = false
check_entropy = true # true = all files, false = off everywhere, unset = env files only
entropy_threshold = 4.5
fail_on_severity = "high"
ignore_paths = ["tests/**", "*.test.py"]
Handling False Positives¶
No secret scanner is 100% accurate. Envdrift provides a centralized ignore system that works uniformly across ALL scanners (native, gitleaks, trufflehog, detect-secrets, kingfisher, git-secrets, talisman, trivy, and infisical). This ensures you configure ignores once and they apply everywhere.
Inline Ignore Comments (Recommended)¶
Add ignore comments directly in your code. These travel with your code, are visible in pull requests, and are the most maintainable approach:
# Ignore all rules on this line
password = ref(false) # envdrift:ignore
# Ignore a specific rule
SECRET_KEY = "test-key-for-unit-tests" # envdrift:ignore:django-secret-key
# Ignore with a reason (best practice)
API_KEY = "test_fixture" # envdrift:ignore reason="test data, not a real key"
Supported comment styles:
| Language | Syntax |
|---|---|
| Python, Shell, YAML | # envdrift:ignore |
| JavaScript, TypeScript, Go, C | // envdrift:ignore |
| CSS, C-style | /* envdrift:ignore */ |
| JSON | Use TOML config (no comments allowed) |
TOML Configuration for Bulk Ignores¶
For patterns that appear across many files (like translation files or test fixtures),
use the ignore_rules setting:
[guard]
# Global path ignores - skip these paths entirely
ignore_paths = [
"**/tests/**",
"**/fixtures/**",
"**/locales/**",
"**/__mocks__/**",
]
# Rule-specific path ignores - ignore specific rules in specific paths
[guard.ignore_rules]
# FTP password pattern matches "Mot de passe" in French translations
"ftp-password" = ["**/*.json", "**/locales/**"]
# Connection string pattern matches Helm value templates
"connection-string-password" = ["**/helm/**", "**/charts/**"]
# Django secret key in test settings is intentional
"django-secret-key" = ["**/test_settings.py", "**/conftest.py"]
Ignore Priority¶
The ignore system applies in this order:
- Inline comments - Checked first, most specific
- Rule+path ignores - From
[guard.ignore_rules]in TOML - Built-in noisy-rule defaults - Keyword/entropy rules only, in config/lock files
- Global path ignores - From
ignore_pathsin TOML
Built-in default ignores¶
Config and lock files (pyproject.toml, envdrift.toml, mkdocs.yml, *.lock,
package-lock.json, *-lock.json, *.sum, ...) routinely contain "secret"/"token"
keywords and high-entropy integrity hashes that false-positive the keyword- and
entropy-driven rules. Guard suppresses only those noisy rules (rule ids containing
generic, entropy, or keyword — across every scanner) in these files.
High-confidence, distinctive-prefix detections are never suppressed by the
defaults: a GitHub PAT (ghp_...), AWS access key (AKIA...), or PyPI token committed
inside pyproject.toml or a lock file is still reported. To skip such a file entirely,
add it to ignore_paths yourself — explicit user ignores always suppress everything.
Directory-scoped defaults (bin/**, dist/**, vendor/**, node_modules/**, ...)
apply consistently however the scan path is spelled: envdrift guard, envdrift
guard ., and a symlinked path all produce the same result set.
Finding Rule IDs¶
To see which rule triggered a finding, run with --verbose:
Or use JSON output:
Common rule IDs:
| Rule ID | Description |
|---|---|
aws-access-key-id |
AWS access key pattern (AKIA...) |
aws-secret-access-key |
AWS secret key |
github-pat |
GitHub Personal Access Token (gitleaks findings are prefixed, e.g. gitleaks-github-pat) |
django-secret-key |
Django SECRET_KEY setting |
connection-string-password |
Database connection string passwords |
ftp-password |
FTP/SFTP password in JSON/config |
high-entropy-string |
High entropy value (entropy scan) |
unencrypted-env-file |
.env file without encryption markers |
unencrypted-secret-file |
partial-encryption .secret file left plaintext |
committed-private-key |
dotenvx .env.keys private-key file tracked or staged in git |
unencrypted-secret-file(CRITICAL). A partial-encryption.secretfile holds the sensitive half of an environment and must be dotenvx-encrypted before commit. A plaintext one is flagged CRITICAL (not the generic HIGHunencrypted-env-file), soguard --stagedblocks the commit. Fix it withenvdrift push, which encrypts the.secret. The installed pre-commit hook enforces the same rule — it refuses a plaintext.secretbut leaves the plaintext.clearhalf (committed by design) and encrypted.secretfiles alone.
committed-private-key(CRITICAL). dotenvx writes decryption keys to.env.keys. A local, gitignored/untracked.env.keysis the expected state and is not flagged. Once git tracks or stages it, guard flags it ascommitted-private-key— anyone with repo access could decrypt every secret it protects. The fix is not to encrypt the key file; remove it from git (git rm --cached .env.keys), rotate the exposed key(s), and add.env.keysto.gitignore(envdrift push/pull-partialdo this automatically).
Skipping Clear Files¶
By default, .clear files ARE scanned. This ensures all configuration files
are checked for accidentally included secrets. To skip them:
Note:
skip_clear_filestakes precedence over thepartial_encryption.clear_fileallowlist. When enabled, ALL.clearfiles are skipped entirely--even those declared inpartial_encryption.environments. The allowlist only affects behavior whenskip_clear_files=false, exempting specified files from the "unencrypted-env-file" check while still scanning them for secret patterns.
Option 1: Skip all .clear files globally¶
Or via CLI: envdrift guard --skip-clear
Option 2: Skip specific .clear files using ignore rules¶
Option 3: Use inline ignore comments¶
In your .clear file:
Allowed Clear Files (Partial Encryption)¶
For partial encryption setups, files listed in partial_encryption.environments
with clear_file are automatically excluded from the "unencrypted env file" check
(but still scanned for secret patterns):
[[partial_encryption.environments]]
name = "production"
clear_file = "app/.env.production.clear"
secret_file = "app/.env.production.secret"
combined_file = "app/.env.production"
Precedence Example¶
[guard]
skip_clear_files = true # Takes precedence
[[partial_encryption.environments]]
name = "production"
clear_file = "app/.env.production.clear" # Will NOT be scanned when skip_clear_files=true
secret_file = "app/.env.production.secret"
combined_file = "app/.env.production"
With skip_clear_files = true: app/.env.production.clear is completely skipped from scanning.
With skip_clear_files = false (default): app/.env.production.clear is scanned for patterns but
exempt from the "unencrypted-env-file" check.
Tips¶
--historyrequires a git repository plus an active history-capable scanner (gitleaks, trufflehog, kingfisher, git-secrets, talisman, or infisical) — guard errors out rather than silently skipping history — and can be slower on large histories.skip_clear_filesskips.clearfiles entirely (default: false, they ARE scanned).ignore_pathsapplies globally to all scanners.ignore_rulesprovides fine-grained control per rule per path pattern.- Use inline comments for one-off ignores; use TOML for bulk patterns.
- Always provide a
reasonin inline comments for future maintainers. - External scanners can auto-install; disable with
--no-auto-install. Downloads are SHA256-verified against upstream checksums and fail closed when the checksum is missing or mismatched.