Hooks that stop Claude Code reading your secrets
The job is keeping Claude Code away from .env files, SSH keys and cloud credentials. A PreToolUse hook sees every tool call before it runs, and exit code 2 blocks it. On 5 and 6 November 2026 I tested two public guards against my own two (one for secret reads, one for dangerous git commands) by feeding all of them the same fake tool calls.
How I tested
No real secrets, no real home folder. Each hook ran on a scratch copy with a fake HOME, one JSON event per line on stdin, the way Claude Code calls it. I recorded the exit code.
printf '{"tool_name":"Read","tool_input":{"file_path":"/work/app/.env"}}' \
| HOME=FAKE_HOME bash COPY_OF_HOOK.sh
24 fake calls went to the guard in disler/claude-code-hooks-mastery and to mine. 33 went to emaarco/agento-patronum, once against its old plugin hook and once against the current one. The author has since folded the repo into emaarco/hogwarts, so the old URL no longer works. Both hooks gave identical results.
hooks-mastery's guard against mine
rm -rf /,rm -Rf ~,sudo rm --recursive --force /: theirs blocked. Mine left these to Claude Code's own permission rules.rm -rf node_modules: theirs blocked, mine let it through.find ~ -deleteand Pythonshutil.rmtree: both let through.git push --force: theirs let it through, mine blocked.cat .env, Read.env, Read.env.local: both blocked.grep -n KEY .env: both let through (mine on purpose).- Read
.env.exampleand Readsrc/.environment.ts: theirs blocked, mine let through. cat ~/.zshrc,cat ~/.aws/credentials,cat ~/.ssh/id_ed25519: theirs let through, mine blocked.
Neither guard covers everything. Theirs is stronger on deletes, mine on reading credentials through the shell.
agento-patronum: strong on files, thin on the shell
It ships 12 rules: .env, .env.*, *.pem, *.key, ~/.ssh/*, ~/.aws/credentials, ~/.aws/config, ~/.docker/config.json, ~/.kube/config, ~/.npmrc, ~/.pypirc and the command printenv. An update on 10 November added five more: ~/.config/gh/hosts.yml, ~/.config/glab-cli/config.yml, ~/.git-credentials, ~/.netrc and ~/.claude/.credentials.json.
On 14 November 2026 I re-ran fake calls against that updated hook, inside a macOS sandbox with no network, writes allowed only in two scratch folders, a throwaway HOME, an empty environment and a 10-second CPU limit. I read all 163 lines of the hook first. My original list of 33 calls was not kept, so I rebuilt it from my notes (32 calls plus 3 I had noted separately, which is not the identical set). Every result below held, except where it says otherwise.
- File tools are blocked well. Read, Write, Edit and MultiEdit on
.env,.env.production.bak,server.pem,~/.ssh/id_rsa, a nested~/.ssh/keys/deployand even~/.ssh/../.ssh/id_rsaall exited 2. - The shell is open.
cat ~/.ssh/id_rsa,cat .env,grep KEY .envandcat ~/.aws/credentialsall passed. The one shell rule checks whether a command starts withprintenv, soenv,sudo printenvandcd /tmp && printenvpassed too. The author's docs say so: "it won't catch commands buried in pipes or subshells". - Two tools are never checked. The hook only listens to Read, Write, Edit, MultiEdit and Bash, so Grep on a
.envpath and Glob on~/.ssh/*went straight through. - A fixed list misses things and over-blocks others.
.envrc, and a private key copied outside~/.sshall pass. The GitHub CLI's token file passed in my first run and is blocked since the 10 November update. The*pattern crosses folders, so.env.example,i18n/menu.keyand~/.ssh/known_hostswere blocked. - It fails closed, except on bad input. A broken rules file or an unset
HOMEblocks every call. That is the right default. But input that is not valid JSON, such as plain text or a truncated event, makesjqfail and the hook exit 5, which Claude Code treats as a hook error, not a block. An empty input exits 0. The truncated event I sent named a.envfile, and it still got through. - The log is multi-line. Each block writes a six-line indented JSON object, because
jqruns without-c. All 21 blocked calls in the re-run wrote six lines each.
What works for the shell, and what it costs
Match the whole command text, not its first word. My git guard runs a regex over the full command, so it catches cd x && git push, which a prefix rule misses. The cost is false alarms. During these tests my own guards blocked a command that only had the words "git push" inside a quoted string, and another that named .env in a label. hooks-mastery's guard has the same kind of false alarm.
If you write one
- Put Grep and Glob in the matcher, not only Read, Edit and Bash.
- For Bash, match the whole command text, and accept a few false alarms.
- Fail closed when the rules file is missing or broken, and when the input is not valid JSON.
- Give every block a reason a person can read, and log each one on a single line.
- Test it with fake calls like the ones above, and check the exit code.
A hook doesn't replace deny rules in your settings. patronum's own newer README notes that Read deny rules also feed Claude Code's sandbox. Use both.
Tiếng Việt