SLOPSHOPPER

env-guard

Claude Code mod: blocks Claude from reading .env secret files and counts blocked attempts in the status line

newguardtoaststatus
v0.2.0MITupdated 2026-10-06ABDUAZIZX/env-guard
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · env-guard
› fix the failing auth test and add an audit log call ╭─────────────────────────────────────────╮ │ env-guard │ ⏺ Read(src/auth.ts) │ 🛡️ env-guard: منعتُ الوصول إلى cat │ ⎿ Read 6 lines │ .env │ ⏺ Update(src/auth.ts) ╰─────────────────────────────────────────╯ ⎿ Added 2 lines, removed 1 line ⏺ Bash(cat .env) ⎿ Denied by env-guard: env-guard: cat .env ملف أسرار محمي، لا تقرأه ولا تطبع محتواه. ● Done. refresh now rejects expired claims and logs an audit event. ✻ Worked for 42s · done 4:20 PM ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts ⚠ env-guard: 🛡️ أسرار محمية: 1
README

env-guard 🛡️

A Claude Code mod (v2.1.287+) that stops Claude from reading your .env secret files.

Mod لـ Claude Code يمنع Claude من قراءة ملفات .env (مفاتيحك السرية)، ويعرض عدّاد المحاولات المحجوبة في شريط الحالة.

What it does

  • Denies Read, Edit, Write, Grep and Bash calls that touch .env, .env.local, .env.production, .env* globs, …
  • Leaves .envrc, env.example and the committed templates .env.example / .env.sample / .env.template / .env.dist alone.
  • Allows single Bash commands that name a .env file without revealing it: git rm --cached .env, git check-ignore .env, ls .env, test -f .env, echo .env >> .gitignore. Any chaining (;, &&, |, $( ), backticks) turns the exemption off.
  • Shows 🛡️ أسرار محمية: N in the status line.
  • Your own shell (! cat .env) is not blocked: it guards the model, not you.

Install

/plugin marketplace add ABDUAZIZX/env-guard
/plugin install env-guard@env-guard

Or try it for one session:

git clone https://github.com/ABDUAZIZX/env-guard
claude --plugin-dir ./env-guard

Test

claude plugin validate .
claude plugin test .

Limits

  • Pattern-based: partial globs such as .en? or a path built from variables are not caught. A safety net, not a sandbox.
  • It still blocks writing .env — on purpose, so an existing file with your keys can't be overwritten. Create it yourself: ! cp .env.example .env.

Changelog

  • 0.2.0 — reading .env.example-style templates is allowed; .gitignore/git rm --cached clean-up commands pass; .env* globs and {.env,…} are now caught.
  • 0.1.0 — first release.

Mods are an early-access API and may change between releases. Read any mod's code before installing it — this one is ~50 lines in hooks/register.ts.

MIT License

Source 1 files
hooks/register.ts 48 lines
1import type { EngineInterface, Register } from 'claude-code'
2
3// .env, .env.local, .env.production, and globs like .env* — but not .envrc, env.example,
4// or the committed templates .env.example / .env.sample / .env.template / .env.dist.
5const SECRET =
6  /(^|[\/\\\s"'=({,:<`])\.env(?!\.(?:example|sample|template|dist|defaults?)(?![\w-]))(\.[\w-]+)?($|[\s"';|&>)*?[\]{},:<`])/
7
8// Single commands that name a .env file without revealing its contents.
9const ARG = String.raw`(?:"[^"$\x60\\]*"|'[^']*'|[^\s"'$\x60<>|;&]+)`
10const SAFE_BASH = [
11  /^git\s+rm\s+(?:-\S+\s+)*--cached\b/,
12  /^git\s+check-ignore\b/,
13  /^ls\b/,
14  /^(?:test|\[)\s+-[efs]\s/,
15  new RegExp(String.raw`^(?:echo|printf)(?:\s+${ARG})+\s*>>\s*(?:[\w./-]*\/)?\.gitignore\s*$`),
16]
17// Chaining, pipes, substitution: any of these could smuggle a read in next to a safe command.
18const CHAIN = /[;&|`\n\r]|\$\(|<\(|>\(/
19
20let blocked = 0
21
22function guard(target: string | undefined) {
23  return target !== undefined && SECRET.test(target)
24}
25
26export function guardBash(command: string) {
27  if (!guard(command)) return false
28  const c = command.trim()
29  return CHAIN.test(c) || !SAFE_BASH.some(re => re.test(c))
30}
31
32function deny($: EngineInterface, what: string) {
33  blocked += 1
34  $.ui.toast(`🛡️ env-guard: منعتُ الوصول إلى ${what}`)
35  $.ui.status(`🛡️ أسرار محمية: ${blocked}`)
36  return { deny: `env-guard: ${what} ملف أسرار محمي، لا تقرأه ولا تطبع محتواه.` }
37}
38
39export const register: Register = on => {
40  on('tool.call', { tool: 'Read' }, ($, e, next) => (guard(e.file_path) ? deny($, e.file_path) : next(e)))
41  on('tool.call', { tool: 'Edit' }, ($, e, next) => (guard(e.file_path) ? deny($, e.file_path) : next(e)))
42  on('tool.call', { tool: 'Write' }, ($, e, next) => (guard(e.file_path) ? deny($, e.file_path) : next(e)))
43  on('tool.call', { tool: 'Grep' }, ($, e, next) =>
44    guard(e.path) || guard(e.glob) ? deny($, e.path ?? e.glob ?? '.env') : next(e),
45  )
46  on('tool.call', { tool: 'Bash' }, ($, e, next) => (guardBash(e.command) ? deny($, e.command.slice(0, 60)) : next(e)))
47}
48