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
// Avertit si un fichier .env créé n'est pas couvert par .gitignore (PreToolUse Write|Edit)
import { readFileSync, existsSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, basename, join } from 'path';
// .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