cleancontext.blog
npm, runs locallyLer em português

wardenv

I wrote this guard after noticing the obvious too late: .gitignore stops the commit, not the read. Your coding agent never sees the value, and still writes the right code.

install
npm install -g wardenv
wardenv install

Restart your agent. The installer backs up your settings.json first, is idempotent, and leaves every other hook untouched. Zero dependencies, MIT, 101 tests.

The four doors

Most setups lock the front door and leave three open.

01

Direct read

Read(.env)

The door everyone locks, and the only one permissions.deny covers well.

02

Shell

cat .env, Get-Content .env, git show HEAD:.env

The target is still in the command, but there are so many ways to write the same thing that a permission rule becomes a pattern list you maintain forever.

03

Ricochet

printenv, docker compose config, vercel env pull, kubectl get secret

The command is innocent and names no file: the leak is in the output. No permission rule catches this, because there is nothing suspicious to match on.

04

Exfiltration

a live key written into config.ts, or curl -F f=@.env

This one runs the other way. Once the secret is in context, the agent inlines the value trying to help, or ships the whole file over the network.

Block the value, not the knowledge

A guard that just says “no” makes the agent guess, work around you, and get uninstalled within a week. So the block hands back the structure:

🔒 wardenv: ".env" is a secret file — read blocked.

File structure (names only, values withheld):
  DATABASE_URL=<set, 48 chars>
  STRIPE_SECRET=<set, 31 chars>

The agent learns the key exists, learns its shape, and writes process.env.DATABASE_URL correctly without ever seeing the password.

Why a hook, and not permissions.deny

permissions.deny is skipped under --dangerously-skip-permissions. A PreToolUse hook is policy enforcement, not a permission prompt: it still runs.

The hooks fire inside subagents too, where the blast radius is largest and where almost no guardrail is ever wired in.

What is never blocked

False positives are treated as a first-class security failure, with their own test suite. A guard that cries wolf gets switched off, and a guard that is off protects nothing.

  • .env.example, .env.sample and .env.template are documentation, never blocked.
  • Values under 12 characters are never redacted, otherwise NODE_ENV=production would erase the word “production” from every log you read.
  • Writing into .env is legitimate. The block runs the other way: a secret leaving for a file that is not a vault.
  • Fail open. A malformed payload, an unreadable file or a bug in wardenv itself lets the call through: a security tool that breaks your session gets uninstalled.

Limits, honestly

Redaction is not hermetic: it catches known values from your .env files and known secret shapes (Anthropic and OpenAI key prefixes, GitHub tokens, AWS access key IDs, JWTs, PEM blocks, connection strings carrying a password). A secret in an exotic format that never passed through a .env can slip. It reduces the surface drastically; it does not zero it.

There are adapters for Claude Code, Codex CLI, Gemini CLI, Cursor and GitHub Copilot CLI, but only Claude Code is verified end to end in a real session. Codex is close: a live session blocked every scenario tested, but the unlock flow and subagents haven’t been exercised yet. Gemini, Cursor and Copilot were checked against each agent’s source and docs, not a live session, and the installer warns you when it runs. Cursor has no output hook, so door 3 can’t close there. Shell parsing is heuristic: the threat model is a helpful agent taking the obvious path, not a deliberate adversary.

unlike the other tools here, this one does not run in the browser: it is an npm package that installs hooks into your agent, and everything happens on your machine. keeping the secret there is the entire point.