Skip to content

Safety Hooks

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:

Terminal window
claude plugin install safety-hooks@cctools-plugins

File 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:

  • PreToolUse hooks run before a tool executes, allowing the hook to block, modify, or require approval for the operation.
  • PostToolUse hooks run after a tool executes, enabling follow-up actions like notifications.

Each hook returns one of three results:

  • Allow — the command proceeds normally
  • Block — the command is prevented, with an error message explaining why
  • Approval required — Claude Code shows a permission prompt so you can approve or deny
Hook FileWhat It Protects
bash_hook.pyMain orchestrator for all bash command checks
git_commit_block_hook.pyUser permission prompt for git commit
git_add_block_hook.pyBlocks bulk staging patterns (git add -A, git add ., wildcards)
env_file_protection_hook.pyBlocks all .env file operations (read/write/edit)
file_size_conditional_hook.pyPrevents reading files over 500 lines
grep_block_hook.pyEnforces ripgrep (rg) over grep
notification_hook.shSends 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
  • pathspecs that name the same thing by another route: ./, :/, :(top), an empty pathspec, or an absolute path that contains the working directory

These 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:

~/.claude/settings.json
{
"env": {
"CCTOOLS_ALLOW_GIT": "1"
}
}

The >allow-git trigger is the per-session override. Type it directly at the prompt:

TriggerEffect
>allow-gitAllow commits in this session
>allow-git offRestore approval prompts in this session
>allow-git statusShow 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:

  • Reading (e.g., cat .env, Read tool)
  • Writing (e.g., Write tool)
  • Editing (e.g., Edit 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:

  • Blocked: find . -name '.env', find . -path '*/.env', grep -r --include='*.env*', rg -g '*env', find . -name '.?nv', rg -g '*e?v'
  • Allowed: 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.