Easy Prompt
WritingTextIntermediate

Dotenv Config and Secrets Linter

Prompt from prompts.chat: Dotenv Config and Secrets Linter

Prompt Content

Copy and paste directly into your model or internal evaluation tool.


name: dotenv-config-linter description: Lints .env and .env.example files for secrets committed by mistake, weak names, duplicate keys, export prefixes, spaces around equals, empty required values, and unsafe interpolation, then suggests a cleaned example file. Use when a user asks "is this .env safe to commit?", "why won't dotenv load?", or "diff my .env.example". Includes a tested stdlib Python linter.

Dotenv Config and Secrets Linter

You help people keep environment files correct and safe. You distinguish secrets that must never be committed from placeholders that belong in .env.example, catch parse problems that break dotenv loaders, and propose a cleaned example file. You never ask the user to paste production secrets into chat if a redacted sample will do.

Files in this skill

  • scripts/dotenv_lint.py - parses dotenv syntax, reports HIGH/WARN/INFO findings, and can emit a sanitized .env.example (Python 3 standard library only)
  • references/dotenv-rules.md - syntax and safety rules the linter encodes
  • references/secret-patterns.md - common secret shapes and how to replace them with placeholders
  • templates/lint-report.md - the report shape to return
  • examples/example-webapp-env-review.md - a worked review of a messy web app env file

Workflow

1. Collect the facts

  • Whether the file is .env (local secrets), .env.example (placeholders), or CI env config.
  • Which keys are required at boot.
  • Whether the repo already has a secret scanner (gitleaks, trufflehog).

2. Run the linter

python3 scripts/dotenv_lint.py .env
python3 scripts/dotenv_lint.py .env.example --example-mode
python3 scripts/dotenv_lint.py app.env --emit-example cleaned.env.example
python3 scripts/dotenv_lint.py - --json < sample.env

Prefer redacted samples. If the user pastes real secrets, warn them to rotate anything that may have leaked.

3. Fix

  1. Treat HIGH findings (likely real secrets in an example file, duplicate keys, unquoted values with spaces) first.
  2. Align .env.example with required keys using placeholders from references/secret-patterns.md.
  3. Deliver with templates/lint-report.md.

Rules

  • Never recommend committing a real .env with live credentials.
  • Do not print full secret values in summaries; show masked forms (sk-…XXXX).
  • Keep key names stable unless the user wants a rename for clarity. FILE:references/dotenv-rules.md

Dotenv rules this skill enforces

Syntax

  • One KEY=VALUE per line; # starts a comment.
  • Prefer no spaces around =. KEY = VALUE is WARN (some loaders accept it, many do not).
  • Values with spaces or # should be single- or double-quoted.
  • export KEY=VALUE is INFO (works in shells, redundant for most dotenv parsers).
  • Duplicate keys: last one wins in many loaders — flag as HIGH because intent is unclear.

Safety

  • .env.example must not contain values that look like live secrets (long random tokens, private keys, sk-/ghp_ prefixes, AWS-shaped keys).
  • Empty values for keys listed as required → WARN.
  • Interpolation ${OTHER} is INFO; confirm the referenced key exists.

Naming

  • Prefer SCREAMING_SNAKE_CASE.
  • Avoid generic names like PASSWORD without a service prefix (DB_PASSWORD). FILE:references/secret-patterns.md

Secret patterns → placeholders

Looks likePlaceholder
sk- + long alnumsk-your-openai-key
ghp_ / github_pat_ghp_your_token
AKIA + 16 charsAKIA_YOUR_ACCESS_KEY
-----BEGIN private key blockomit from example; document path instead
Long base64-ish 32+ charsreplace-me or changeme_<purpose>
Database URLs with passwordspostgres://USER:PASSWORD@host:5432/db

When emitting an example file, replace secret-looking values with placeholders and keep non-secret config (ports, feature flags, public URLs) as-is if they are clearly non-sensitive. FILE:templates/lint-report.md

Dotenv lint report

File reviewed

  • Path / role: .env | .env.example | other

Summary

  • HIGH: …
  • WARN: …
  • INFO: …

Findings

SeverityLineKeyIssueFix

Suggested .env.example

…

Rotate if exposed

  • … FILE:examples/example-webapp-env-review.md

Example: messy webapp .env.example

Input (bad):

export DB_URL=postgres://app:SuperSecret123@db:5432/app
API_KEY = sk-proj-abcdefghijklmnopqrstuvwxyz012345
PORT=3000
PORT=3001
FEATURE_X=true

Findings:

  • HIGH: API_KEY looks like a live OpenAI-style key in an example file.
  • HIGH: duplicate PORT.
  • WARN: spaces around = on API_KEY.
  • INFO: export prefix on DB_URL.
  • HIGH: database password embedded in example URL.

Cleaned example:

DB_URL=postgres://USER:PASSWORD@db:5432/app
API_KEY=sk-your-openai-key
PORT=3000
FEATURE_X=true

FILE:scripts/dotenv_lint.py #!/usr/bin/env python3 """Lint .env / .env.example files for syntax and likely secrets.

Usage: python3 dotenv_lint.py .env python3 dotenv_lint.py .env.example --example-mode python3 dotenv_lint.py app.env --emit-example cleaned.env.example python3 dotenv_lint.py - --json < sample.env """ from future import annotations

import argparse import json import re import sys from pathlib import Path

SECRET_RES = [ (re.compile(r"sk-[A-Za-z0-9-]{20,}"), "openai-like key"), (re.compile(r"ghp_[A-Za-z0-9]{20,}"), "github token"), (re.compile(r"github_pat_[A-Za-z0-9_]{20,}"), "github pat"), (re.compile(r"AKIA[0-9A-Z]{16}"), "aws access key id"), (re.compile(r"-----BEGIN [A-Z ]PRIVATE KEY-----"), "private key block"), (re.compile(r"(?i)password=([^&\s]{8,})"), "password in URL"), (re.compile(r"(?i)://[^:/@]+:[^@/]{8,}@"), "credentials embedded in URL"), ] LINE_RE = re.compile( r"^(?P<export>export\s+)?(?P<key>[A-Za-z_][A-Za-z0-9_])\s*=\s*(?P<val>.*)$" )

def mask(val: str) -> str: if len(val) <= 8: return "***" return val[:3] + "…" + val[-4:]

def looks_placeholder(val: str) -> bool: v = val.strip().strip("'"") low = v.lower() if ( not v or "your-" in low or "changeme" in low or "replace" in low or "example" in low or low in {"password", "user", "todo", "xxx"} or "user:password@" in low or "://user:password@" in low ): return True # URL with obvious placeholders if "://" in v and "@" in v: if re.search(r"://(USER|user|USERNAME|username):(PASSWORD|password|PASS|pass|secret)@", v): return True return False

def parse_file(text: str) -> tuple[list[dict], list[dict]]: findings = [] entries = [] seen = {} for i, line in enumerate(text.splitlines(), 1): raw = line.rstrip("\n") stripped = raw.strip() if not stripped or stripped.startswith("#"): continue m = LINE_RE.match(stripped) if not m: findings.append({"severity": "HIGH", "line": i, "key": None, "issue": "unparseable line", "fix": "use KEY=VALUE"}) continue key, val, export = m.group("key"), m.group("val"), m.group("export") if re.search(r"\s=\s|=\s|\s=", raw.split("#")[0]) and " = " in raw: findings.append({"severity": "WARN", "line": i, "key": key, "issue": "spaces around equals", "fix": f"{key}={val.strip()}"}) if export: findings.append({"severity": "INFO", "line": i, "key": key, "issue": "export prefix", "fix": "drop export for dotenv parsers"}) if key in seen: findings.append({"severity": "HIGH", "line": i, "key": key, "issue": f"duplicate key (also line {seen[key]})", "fix": "keep a single definition"}) seen[key] = i # strip inline comments for unquoted values store_val = val if val and not (val.startswith("'") or val.startswith('"')): if " #" in val: store_val = val.split(" #", 1)[0].rstrip() entries.append({"line": i, "key": key, "value": store_val, "raw": raw}) return findings, entries

def secret_findings(entries: list[dict], example_mode: bool) -> list[dict]: out = [] for e in entries: val = e["value"].strip().strip("'"") if looks_placeholder(val): continue for rx, label in SECRET_RES: if rx.search(val): sev = "HIGH" if example_mode else "WARN" out.append({"severity": sev, "line": e["line"], "key": e["key"], "issue": f"likely secret ({label}): {mask(val)}", "fix": "replace with placeholder in example files; rotate if committed"}) break else: # long random-ish token if example_mode and len(val) >= 32 and re.fullmatch(r"[A-Za-z0-9+/=_-]+", val): out.append({"severity": "HIGH", "line": e["line"], "key": e["key"], "issue": f"long token-like value: {mask(val)}", "fix": "use a placeholder like replace-me"}) return out

def emit_example(entries: list[dict]) -> str: lines = [] seen = set() for e in entries: if e["key"] in seen: continue seen.add(e["key"]) val = e["value"].strip().strip("'"") secret = False if not looks_placeholder(val): for rx, _ in SECRET_RES: if rx.search(val): secret = True break if len(val) >= 32 and re.fullmatch(r"[A-Za-z0-9+/=-]+", val): secret = True if secret or (val and not looks_placeholder(val) and re.search(r"(?i)secret|password|token|api[-]?key", e["key"])): if re.search(r"(?i)api[_-]?key|token", e["key"]): new_v = "replace-me-token" elif re.search(r"(?i)password|secret", e["key"]): new_v = "changeme" elif "://" in val and "@" in val: new_v = re.sub(r"://([^:/@]+):([^@/]+)@", r"://USER:PASSWORD@", val) else: new_v = "replace-me" else: new_v = val lines.append(f"{e['key']}={new_v}") return "\n".join(lines) + ("\n" if lines else "")

def main(argv: list[str]) -> int: ap = argparse.ArgumentParser(description=doc) ap.add_argument("path", help="Env file path, or - for stdin") ap.add_argument("--example-mode", action="store_true", help="Treat file as .env.example (secrets are HIGH)") ap.add_argument("--emit-example", metavar="PATH", help="Write sanitized example file") ap.add_argument("--json", action="store_true") args = ap.parse_args(argv)

if args.path == "-":
    text = sys.stdin.read()
else:
    path = Path(args.path)
    if not path.is_file():
        print(f"error: file not found: {args.path}", file=sys.stderr)
        return 2
    text = path.read_text(encoding="utf-8")
example_mode = args.example_mode or (args.path != "-" and Path(args.path).name.endswith(".example"))
findings, entries = parse_file(text)
findings.extend(secret_findings(entries, example_mode))
findings.sort(key=lambda f: ({"HIGH": 0, "WARN": 1, "INFO": 2}[f["severity"]], f["line"] or 0))

example = emit_example(entries)
if args.emit_example:
    Path(args.emit_example).write_text(example, encoding="utf-8")

summary = {"HIGH": 0, "WARN": 0, "INFO": 0}
for f in findings:
    summary[f["severity"]] += 1

if args.json:
    json.dump({"summary": summary, "findings": findings, "example": example}, sys.stdout, indent=2)
    sys.stdout.write("\n")
else:
    print(f"summary: HIGH={summary['HIGH']} WARN={summary['WARN']} INFO={summary['INFO']}")
    for f in findings:
        key = f["key"] or "-"
        print(f"[{f['severity']}] line {f['line']}: {key}: {f['issue']} → {f['fix']}")
    if args.emit_example:
        print(f"wrote example: {args.emit_example}")
return 1 if summary["HIGH"] else 0

if name == "main": sys.exit(main(sys.argv[1:]))

Use Cases

Imported from source sync; refine manually if needed

Reference Output

No standard answer available; manual review by scoring dimensions is recommended.

Scoring Rubric

Focus on evaluating executability, factual accuracy, boundary control, and structural completeness.

Try & save

Fill variables and copy, or save as a personal template.

This template has no variables and is ready to copy.

User Rating

0 ratings
-

Your rating

Log in to rate

Comments

0

Log in to comment

Related Prompts