Three PreToolUse Hooks I Stopped Using (and What Replaced Them)
I have rolled out a lot of PreToolUse hooks over the last six months. Most of them were a mistake. Here are the three I removed and what I actually use now.
1. "Don't edit files outside the project" hooks
The hook would inspect file_path in the Edit tool's arguments, check that it started with the project root, and short-circuit if it didn't. The intent was good — Claude Code does occasionally wander into ~/.zshrc if you let it — but the implementation was wrong.
The cost was high: every single Edit, Read, Write, and Bash call paid the hook's regex + path check, even on the 99% of cases that were fine. The benefit was rare, and when it did fire, the model would just retry with a different path. The hook trained the model to be sneaky about file paths instead of being honest about wanting to touch them.
What replaced it: a one-line rule in CLAUDE.md that says "ask before editing anything outside this project's tree" and a SessionStart hook that prints the project root. The model has the rule, the session knows the boundary, and the per-call cost is zero.
2. "Strip secrets from tool output" hooks
This one looked important on paper. The PostToolUse hook would grep the tool result for sk-, AKIA, and other secret patterns and redact them. The model would then see [REDACTED] and behave accordingly.
The cost was even worse than the file-scope hook. A PostToolUse hook runs on every Bash, Read, and Edit result. The secret scanner did a regex over potentially huge outputs, and most of the work was wasted because no secret was actually there. When a real secret was in a log dump, the redaction usually happened on the wrong line — the secret was a few characters past where the regex stopped.
What replaced it: a .gitignore that includes .env* plus a SessionStart hook that does a one-time git log --diff-filter=A --name-only to make sure no .env ever got committed in the first place. The model never sees the secret because it never tries to read it. The per-call cost is zero, and the redaction is correct because there is no redaction to do.
3. "Inject project context on every Read" hooks
The intent was to give the model a per-file "this file is part of the auth module, treat it carefully" comment on every Read call. The hook would do a quick git log and file-glob lookup and prepend a small context block to the tool output.
The cost was the highest of the three. Reads are the most frequent tool call in a long session, and the git log would itself trigger a Bash call on some setups, leading to recursive hook runs that I had to debug more than once. The benefit was marginal because the model rarely uses injected context when it appears in the middle of a file read — it focuses on the file content and ignores the prefix.
What replaced it: a project-level .claude/docs/ directory of short canonical notes, and a SessionStart hook that lists the available notes. The model reads the relevant note on demand, and the cost is one bash call per session, not one per Read.
What I still use
I kept two kinds of hooks:
- SessionStart hooks for environment checks (preflight, arch lint, dependency availability). They run once per session, the cost is amortized, and the failure mode is "session starts broken" instead of "model writes a bad line of code".
-
PreToolUse hooks for destructive Bash commands — a tiny allowlist check for
rm -rf,git push --force,kubectl delete. The cost is one regex per Bash, the benefit is a hard stop on a class of mistakes that no prompt can reliably prevent.
Everything else I tried was a hook for a problem that was better solved with a CLAUDE.md rule, a SessionStart script, or just letting the model do the work and trusting it.
Tags: claudecode, codex, hooks, devtools, ai
Top comments (0)