Env file gitignore guard
A new .env can never sneak into a commit unignored
When the agent creates a .env-style file, it checks the nearest .gitignore for a covering rule and warns if the file would be tracked — before any secret lands in it. Skips shared templates (.env.example/.sample). Non-blocking advisory, so it pairs cleanly with hard secret-write guards.
What does the Env file gitignore guard hook do?
Env file gitignore guard is a Claude Code PreToolUse hook matching Write|Edit. It fires automatically at that lifecycle event — outside the model, so it can't be skipped or forgotten. A new .env can never sneak into a commit unignored.
As a PreToolUse hook it runs before the action completes, so it can block or adjust what Claude is about to do. Because it is a deterministic Node.js script, it executes on every matching event without relying on the model to remember — the guarantee that makes agentic workflows safe to automate.
Use cases
- Secret leak prevention
- Onboarding safety
- Compliance
Tags
settings.json fragment
{
"hooks": {
"PreToolUse": [
{
"hooks": [
{
"command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/env-gitignore-guard.mjs",
"type": "command"
}
],
"matcher": "Write|Edit"
}
]
}
}Script · .claude/hooks/env-gitignore-guard.mjs
#!/usr/bin/env node
// @hookstack pre-write-env-gitignore-guard
// Avertit si un fichier .env créé n'est pas couvert par .gitignore (PreToolUse Write|Edit)
import { existsSync, readFileSync } from "node:fs";
import { basename, dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
// .env, .env.local, .env.production… mais PAS les modèles partagés (.env.example/.sample/.template).
const ENV_FILE = /^\.env(?:\.[A-Za-z0-9_-]+)?$/;
const TEMPLATE = /\.(?:example|sample|template|dist)$/i;
// Une ligne de .gitignore qui couvre les fichiers .env.
const COVERS_ENV = /^\s*\.env(?:\*|\.\*)?\s*$|^\s*\*\.env\s*$/m;
function findGitignore(dir, fileExists, depth = 0) {
if (depth > 6 || !dir) return null;
const candidate = join(dir, ".gitignore");
if (fileExists(candidate)) return candidate;
const parent = dirname(dir);
return parent === dir ? null : findGitignore(parent, fileExists, depth + 1);
}
export function run(
input,
{ readFile = readFileSync, fileExists = existsSync } = {},
) {
const filePath = input.tool_input?.file_path ?? "";
const base = basename(filePath);
if (!ENV_FILE.test(base) || TEMPLATE.test(base)) return null;
const gitignore = findGitignore(dirname(filePath), fileExists);
let covered = false;
if (gitignore) {
try {
covered = COVERS_ENV.test(readFile(gitignore, "utf8"));
} catch {
covered = false;
}
}
if (covered) return null;
return {
message:
`[env-gitignore] ${base} n'est pas couvert par .gitignore — un secret pourrait être commité. ` +
"Ajoutez une ligne `.env*` à votre .gitignore avant d'y mettre des valeurs.\n",
};
}
/* v8 ignore next 5 */
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const input = JSON.parse(readFileSync(0, "utf8"));
const result = run(input);
if (result?.message) process.stderr.write(result.message);
}
Learn more
Related hooks
- Secret detection before Bash executionCatch a leaked API key before it ever runs
- Destructive command blockingStops a disk-wiping shell command before it runs
- Sensitive file write protectionYour .env and keys stay untouched by the agent
- Lock file write protectionLock files stay intact - package manager only