Hook, the rule you enforce
CLAUDE.md guides Claude, but it does not guarantee anything. A hook is a command that Claude Code itself runs. See how to create one that protects the .env file and how to prove that it works.
- Claude Code
- PreToolUse hook
- example to copy
Published on
Read first: the limit of this hook
This hook applies to the tools that edit and create files (matcher Edit|Write). A terminal command that writes to .env, such as echo oi >> .env, gets past it.
The documentation points to two ways to cover that: a Stop hook, which checks the folder once per turn, or also matching the terminal, with the matcher Edit|Write|Bash|PowerShell plus a script that lists the modified files (git status --porcelain). This guide does not include that second script. Until you create it, the protection is partial.
What each thing is
- CLAUDE.md: text that Claude reads as context. According to the documentation, it guides Claude, but it is not a rule enforced by the system, and there is no guarantee that it will be followed to the letter.
- Hook: a command (script) that Claude Code runs at fixed points of its operation, such as before using a tool. According to the documentation, it runs regardless of what Claude decides.
- PreToolUse: the event that fires before the tool runs and can block the action.
- Matcher: the filter that says which tools the hook applies to.
Edit|Writecovers editing and creating files; it does not cover terminal commands.
Prepare a test folder
Prerequisites: Claude Code installed and the jq tool, which reads the JSON that Claude Code sends to the hook. On a Mac, check with which jq.
Use a test folder, not a real project. Create a .env file with sample content and the hooks folder:
echo "CHAVE=EXEMPLO" > .env
mkdir -p .claude/hooksCreate the hook script
Save it as .claude/hooks/protect-env.sh. It reads the path of the file Claude wants to change and, if the name starts with .env, exits with code two, which is the code that blocks.
#!/bin/bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
FILE_PATH="${FILE_PATH//\\//}"
NAME="${FILE_PATH##*/}"
if [[ "$NAME" == .env* ]]; then
echo "Bloqueado: $FILE_PATH é um arquivo .env protegido por hook" >&2
exit 2
fi
exit 0The script compares only the file name (what comes after the last slash). That way it blocks .env, .env.local and .env.production, and does not block a folder that merely has ".env" in its name, such as .environments. .env.example is also blocked; adjust the script if you need to allow it. The message in the script is in Portuguese ("Bloqueado" means "Blocked"); you can translate the text inside echo without changing how it works.
Make the script executable:
chmod +x .claude/hooks/protect-env.shRegister the hook in settings.json
Create .claude/settings.json with this content. If the file already exists, merge the hooks block into what is already there.
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-env.sh"
}
]
}
]
}
}Open Claude Code in the test folder and type /hooks. The hook should appear in the list, under PreToolUse.
Test
Make these three requests to Claude, one at a time:
Add a comment to the .env file- Expected: the edit is blocked before it happens and Claude receives the message "Bloqueado: ...".
Create a file teste.txt with the word oi- Expected: it works, because the script exits with code zero for other files.
Run in the terminal: echo oi >> .env- Expected: it gets through. The
Edit|Writematcher does not see terminal commands. This is the limit mentioned at the start.
If the hook does not appear or does not fire, start with claude --debug-file /tmp/claude.log and follow it with tail -f /tmp/claude.log, or use /debug inside the session.
What to check
- Only exit code two blocks. Exit code one without valid JSON is a non-blocking error: the action goes ahead.
- The script must be executable and
jqmust be installed. Without that, the hook fails and, in general, the action goes ahead. - Exit code zero in PreToolUse does not approve the action: it follows the normal permission flow.
- Command hooks are fixed rules. Prompt and agent hooks use a model to decide; they are for judgment, not for fixed rules.
- Where to keep it:
.claude/settings.jsonapplies to the project and can go into Git;.claude/settings.local.jsonapplies only to you;~/.claude/settings.jsonapplies to all your projects. - To forbid an action broadly, permission rules (
permissions.deny) are another layer the documentation points to.
Rule of thumb
- Hook or permission: a rule that must always apply, such as protecting a file or blocking a command.
- CLAUDE.md: style preferences and project context.
Before you take it to your project
- The script was tested on November 9, 2026 in the terminal, on Linux and on a Mac (macOS 27), with the PreToolUse JSON the documentation describes: it blocks .env, .env.local, .env.production, sub/.env and .env.example; it allows app.py and files inside .environments; it does not see terminal commands.
- Test it inside a real Claude Code session, in the test folder, before using it in a real project.
- Event names and the settings.json format may change. Check the documentation on the date you use it.
Sources
Official Claude Code documentation, checked on November 9, 2026: Memory (CLAUDE.md), Hooks guide and Hooks reference. The script is based on the documentation’s "Block edits to protected files" example, with the file name comparison and the message adapted.
Guide wrap-up
You now know how to create a hook that blocks edits to .env, prove that it works and what it does not cover.
Follow @drasdiegotechNext guide
Coming soon to the site.
Bookmark this guide in your browser and save the reel on Instagram so you can come back when you need it.
This site counts visits with Vercel Web Analytics, anonymously and in aggregate, without third-party cookies. Privacy notice