OPENAIAPIKEY=sk-replace-me-with-a-real-key-at-runtime
Root cause: gitleaks protect --staged only diffs the staging area. A gitignored, repo-local .env is never staged, so it is invisible to the guard — yet it sits on disk, readable by the app, and leaks via any full-tree scan. Two fixes are needed: stop putting live keys in repo-local files, and verify exposure with a full-tree scan (the guard alone is not a sufficient gate).
1. Never keep live keys in a repo-local .env — delete it, keep only a placeholder template:
# .env.example (placeholder only — never a real key)
OPENAI_API_KEY=sk-replace-me-with-a-real-key-at-runtime
2. Inject secrets via the environment, not files:
# scripts/run.sh — refuses to start without an injected secret
#!/usr/bin/env bash
set -euo pipefail
if [[ -z "${OPENAI_API_KEY:-}" ]]; then
echo "FATAL: OPENAI_API_KEY is not set. Inject via export / CI secret / secret manager." >&2
exit 1
fi
exec python3 app.py
# app.py — reads ONLY from os.environ, fails closed
import os
PLACEHOLDERS = {"", "sk-replace-me-with-a-real-key-at-runtime"}
def main():
key = os.environ.get("OPENAI_API_KEY", "")
if key in PLACEHOLDERS or key.startswith("sk-replace"):
raise SystemExit("FATAL: OPENAI_API_KEY missing or placeholder — inject a real secret via the environment")
print(f"OK: key loaded (masked={key[:8]}...)")
3. Verify exposure with a full-tree scan — this is the gap-closer; run it in CI and pre-push, alongside the staged guard:
# scripts/full-scan.sh — scans EVERY file on disk, including gitignored .env
#!/usr/bin/env bash
set -euo pipefail
gitleaks detect --source . --no-git \
--report-format json --report-path gitleaks-report.json -v
# .github/workflows/secrets.yml (conceptual)
on: [push, pull_request]
jobs:
secrets:
steps:
- uses: actions/checkout@v4
- run: gitleaks protect --staged --source . # fast, staged diff
- run: gitleaks detect --source . --no-git -v # full tree, catches .env
Local dev/CI injection: export OPENAI_API_KEY=$(op read "op://MyVault/OpenAI/credential") or a provider secret store — the key never touches the repo.
Reproduced live in `/tmp/gitreins-demo` (gitleaks 8.24.3): | # | Test | Result | |---|------|--------| | 1 | **Gap:** live `sk-proj-…` key in gitignored `.env`; `protect --staged` | exit **0** — "no leaks found" (guard passes; leak invisible) | | 2 | **Gap:** `detect --source . --no-git` on same tree | exit **1** — finding `generic-api-key`, File: `.env` | | 3 | Fixed tree: `protect --staged` | exit **0** | | 4 | Fixed tree: full-tree scan | exit **0**, clean JSON report `[]` | | 5 | `git grep -n 'sk-'` on tracked files | only the `.env.example`/`app.py` placeholder strings | | 6 | App with `OPENAI_API_KEY` injected | runs: "OK: key loaded (masked=sk-proj-…)" | | 7 | App with `env -u OPENAI_API_KEY` (unset) | exit **1**, fail-closed message | | 8 | App with placeholder value | exit **1**, placeholder rejected | | 9 | **Edge:** real key staged in a tracked file (`leak_test.txt`) → `protect --staged` | exit **1**, finding `leak_test.txt` (guard works within its scope; it's the *scope* that's insufficient) | | 10 | Placeholder `.env.example` not flagged by either scan | pass | | 11 | Full-scan JSON report artifact generated | pass | Note: the sandbox shell inherits pi's own `OPENAI_API_KEY` (the agent's LLM key) — a good real-world reminder that env vars can be inherited; the fail-closed tests explicitly unset it (`env -u`) to isolate behavior.
{"model": "deepseek-v4-flash", "problem_class": "python-gitleaks-staged-vs-fulltree-gap", "result": "passed", "tests": 11}