File Deletion Protection
Blocks rm -rf on critical paths and enforces
a TRASH directory pattern. Requires approval
for other rm commands.
The safety-hooks plugin provides a comprehensive
set of hooks that enhance Claude Code’s behavior and
prevent dangerous operations. These hooks intercept
commands before execution and either block them
outright, require user approval, or suggest safer
alternatives.
Install the plugin via the Claude Code marketplace:
claude plugin install safety-hooks@cctools-pluginsFile Deletion Protection
Blocks rm -rf on critical paths and enforces
a TRASH directory pattern. Requires approval
for other rm commands.
Git Commit Protection
Requires explicit user approval before any
git commit, using Claude Code’s built-in
permission prompt UI.
Git Add Protection
Hard-blocks bulk staging patterns like
git add . and git add -A, while staging
of specific paths goes through untouched.
Environment Security
Blocks all .env file operations (read, write,
edit) and suggests the
env-safe command for
safe inspection.
Context Management
Blocks reading files longer than 500 lines to prevent context bloat in Claude Code sessions.
Command Enhancement
Enforces rg (ripgrep) over grep for better
search performance and output.
Safety hooks intercept commands at different stages:
Each hook returns one of three results:
| Hook File | What It Protects |
|---|---|
bash_hook.py | Main orchestrator for all bash command checks |
git_commit_block_hook.py | User permission prompt for git commit |
git_add_block_hook.py | Blocks bulk staging patterns (git add -A, git add ., wildcards) |
env_file_protection_hook.py | Blocks all .env file operations (read/write/edit) |
file_size_conditional_hook.py | Prevents reading files over 500 lines |
grep_block_hook.py | Enforces ripgrep (rg) over grep |
notification_hook.sh | Sends ntfy.sh notifications on events |
The git add hook blocks bulk staging outright:
git add .git add ../git add *git add -A / git add --all./, :/, :(top), an
empty pathspec, or an absolute path that
contains the working directoryThese stay blocked behind command prefixes too,
so env -C <dir> git add -A and
GIT_DIR=... git add . are blocked as well.
Staging specific paths — new files, modified
files, a named directory — is never gated and
never prompts. The one exception is
--pathspec-from-file, which stages an
unreadable list of paths and so still asks.
git commit asks for approval by default. Set
CCTOOLS_ALLOW_GIT=1 in your environment to allow
commits everywhere without prompting — useful for
unattended workflows, where a prompt stalls the run:
{ "env": { "CCTOOLS_ALLOW_GIT": "1" }}The >allow-git trigger is the per-session
override. Type it directly at the prompt:
| Trigger | Effect |
|---|---|
>allow-git | Allow commits in this session |
>allow-git off | Restore approval prompts in this session |
>allow-git status | Show current state and why |
>allow-git off wins over CCTOOLS_ALLOW_GIT, so a
single session can opt back into prompts. Both
triggers are session-scoped: other concurrent
sessions are unaffected.
All operations on .env files are blocked:
cat .env, Read tool)The hook directs users to the env-safe CLI for safe inspection of environment variables.
The guard also inspects filename globs passed to
find, grep, and rg, and blocks a command only
when its glob targets a dotenv name. A glob counts
as targeting a dotenv when its literal characters or
its non-* wildcards (?, bracket classes) help
spell the .env name:
find . -name '.env',
find . -path '*/.env',
grep -r --include='*.env*', rg -g '*env',
find . -name '.?nv', rg -g '*e?v'find . -name '*.ts',
find . -path '*/dist/*',
grep -rn TODO --include='*.py' .,
rg TODO -g '*.rs'A bare * on its own is never treated as spelling
.env, even though it could match one at runtime —
otherwise every ordinary include-glob would be
denied. This is consistent with the guard allowing
grep -r SECRET . (no glob at all), which reads the
same files. The known trade-off: a glob like
--include='*.local' can match .env.local but is
allowed, because it reaches a dotenv only by *
expanding over the entire .env core.
Negated predicates and globs are exclusions, not
selections, so they never trigger the guard:
find . ! -path '*/.env',
find . -not -name '.env', and
rg -g '!.env*' are all allowed. Double negation
(find . ! ! -name '.env') selects again and is
still blocked, and negation is ignored when the
find expression contains a disjunction
(-o/-or/,), since an -o branch can re-select
the excluded file. Parenthesised \( ... \)
expressions are not analysed as a unit; the guard
inspects predicates token by token.
Reading files longer than 500 lines is blocked to prevent context window bloat. When a large file is detected, the hook suggests reading specific line ranges or using search tools instead.
When Claude Code attempts to use grep, the hook
blocks the command and suggests using rg (ripgrep)
instead. Ripgrep provides better performance,
respects .gitignore, and produces cleaner output.