In our AI context bundler (generate_context.clj), a single line allowed .env files to be read, bundled into ai_context.txt, and sent to external LLMs:

(def allowed-extensions #{".py" ".js" ".jsx" ".ts" ".tsx" ".html" ".css"
                           ".json" ".md" ".sql" ".env" ".ini" ".toml" ".clj"})

Jurisdiction / Data Flow

Local filesystem → Babashka script → ai_context.txt → External LLM API (OpenRouter, Anthropic, etc.) → Model provider logs/training data.

Why This Matters

Secret Type Typical Location Risk if Leaked
API Keys .env, secrets.yaml Full account compromise, billing abuse
Database URLs .env, config.toml Data exfiltration, ransomware
JWT Secrets .env Token forgery, auth bypass
Cloud Credentials .aws/credentials, .gcp/ Infrastructure takeover

The Fix: Defense in Depth

Three layers, not one.

Layer 1: Remove .env from Allowlist

- #{".py" ".js" ".jsx" ".ts" ".tsx" ".html" ".css"
-  ".json" ".md" ".sql" ".env" ".ini" ".toml" ".clj"}
+ #{".py" ".js" ".jsx" ".ts" ".tsx" ".html" ".css"
+  ".json" ".md" ".sql" ".ini" ".toml" ".clj"}

Layer 2: Explicit Denylist (Known Dangerous Filenames)

(def exclude-files #{"ai_context.txt" "db.sqlite3" "package-lock.json" "yarn.lock"
                     "pnpm-lock.yaml" "uv.lock"})

Layer 3: Hard-Coded Regex Guard (Catches Variants)

(def secret-patterns #{"\\.env$" "(?i)secret" "(?i)key" "(?i)token"})

(defn contains-secret-pattern? [filename]
  (some #(re-find (re-pattern %) filename) secret-patterns))

(defn allowed-file? [file]
  (let [name (.getName file)
        ext  (get-extension name)]
    (and (not (contains? exclude-files name))     ; Layer 2
         (not (contains-secret-pattern? name))    ; Layer 3
         (contains? allowed-extensions ext))))    ; Layer 1

Evidence Table

What the guard catches (tested locally):

Filename Matched Pattern Blocked?
.env \.env$
.env.production \.env$
secrets.yaml (?i)secret
api-keys.json (?i)key
auth-token.txt (?i)token
config.toml ❌ (allowed)

Architecture Diagram

┌─────────────┐     ┌──────────────┐     ┌─────────────────┐     ┌─────────────┐
│  Developer  │────▶│ .env / secret│────▶│ generate_context│────▶│  LLM Provider│
│  Workspace  │     │   files      │     │    (babashka)   │     │  (External) │
└─────────────┘     └──────────────┘     └────────┬────────┘     └──────┬──────┘
                                                   │                    │
                            ┌──────────────────────┘                    │
                            ▼                                           ▼
                   ┌─────────────────┐                        ┌─────────────────┐
                   │  THREE LAYERS   │                        │  SECRETS LEAKED │
                   │  OF DEFENSE     │                        │  TO MODEL LOGS  │
                   ├─────────────────┤                        ├─────────────────┤
                   │ 1. Allowlist    │                        │ • API keys      │
                   │    (no .env)    │                        │ • DB passwords  │
                   │ 2. Denylist     │                        │ • JWT secrets   │
                   │    (explicit)   │                        │ • Cloud creds   │
                   │ 3. Regex Guard  │                        │ • Training data │
                   │    (patterns)   │                        │   contamination │
                   └─────────────────┘                        └─────────────────┘

Resume Value?

Yes. This demonstrates:

Frame it as: “Identified and remediated a credential leakage vector in an AI context pipeline by implementing a three-layer secret detection guard (allowlist hardening, explicit denylist, regex pattern matching), preventing .env and secret* files from reaching external LLM APIs.”

Takeaway

Never trust an allowlist alone. Secrets hide in filenames, not just extensions. The regex guard is hard-coded — no config, no user override, no “oops I added .env back.” It fails closed.


Fix merged: PR #2 · Stack: Babashka Clojure · Zero deps · Plain HTML/CSS blog