enforce-hooks: Turn CLAUDE.md Rules into Actual Enforcement
CLAUDE.md is a suggestion. Claude reads it, tries to follow it, sometimes doesn’t. There’s a cluster of issues about this in the Claude Code repo: rules get skipped during long sessions, forgotten after compaction, or just quietly ignored. The fix isn’t better prompting. It’s code.
Claude Code has a hook system. A PreToolUse hook is a script that runs before every tool invocation. It reads the tool call as JSON on stdin. If it prints {"decision": "block", "reason": "..."}, the tool call gets rejected. Otherwise it passes through.
So the question becomes: can you read a CLAUDE.md file and automatically generate the right hooks?
What enforce-hooks does
enforce-hooks.py reads your CLAUDE.md, identifies directives that can be turned into hooks, and generates standalone shell scripts for each one. It handles four pattern types:
File-guard: rules like “never modify .env” or “do not edit files in secrets/” become hooks that block Write/Edit/MultiEdit on matching paths.
Bash-guard: rules like “never run rm -rf” or “no force pushes” become hooks that pattern-match against Bash commands.
Branch-guard: rules like “never commit to main” become hooks that check the git branch before allowing commits.
Require-prior-tool: rules like “run tests before committing” become hooks that check session history for a required prior action.
Concrete examples
Say your CLAUDE.md contains:
## Rules @enforced
- Never modify .env, secrets/, or *.pem files
- Never use git push --force
Running python3 enforce-hooks.py --scan shows which rules are enforceable. Running --install generates two hook scripts and registers them in .claude/settings.json.
The generated file-guard hook:
#!/usr/bin/env bash
INPUT=$(cat)
TOOL=$(echo "$INPUT" | jq -r '.tool_name')
case "$TOOL" in Write|Edit|MultiEdit) ;; *) exit 0 ;; esac
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
[ -z "$FILE" ] && exit 0
for pat in ".env" "secrets/" ".pem"; do
[[ "$FILE" == *"$pat"* ]] && \
echo '{"decision":"block","reason":"Protected file: '"$FILE"'"}' && exit 0
done
exit 0
The generated bash-guard hook:
#!/usr/bin/env bash
INPUT=$(cat)
[ "$(echo "$INPUT" | jq -r '.tool_name')" != "Bash" ] && exit 0
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
for pat in "push --force" "push -f"; do
[[ "$CMD" == *"$pat"* ]] && \
echo '{"decision":"block","reason":"Force push blocked."}' && exit 0
done
exit 0
Each script is self-contained. No dependencies beyond bash and jq.
Two approaches
Plugin mode (recommended)
curl -fsSL https://raw.githubusercontent.com/Bande-a-Bonnot/Boucle-framework/main/tools/enforce/enforce-hooks.py -o /tmp/enforce-hooks.py
python3 /tmp/enforce-hooks.py --install-plugin
This installs one hook that reads your CLAUDE.md at runtime. Change a rule and enforcement updates immediately; no re-running, no re-generating. The engine caches parsed directives using file mtime, so it only re-parses when CLAUDE.md actually changes.
Under the hood, --install-plugin copies the Python engine plus a thin bash wrapper into .claude/hooks/PreToolUse/ and registers it in settings.json. Two files, one settings entry.
Static mode
--install generates standalone bash scripts, one per rule type. Each script is self-contained (bash + jq, no Python at runtime). Useful if you want to inspect or customize individual hooks, or if you prefer not to have Python in your hook chain.
--scan shows what the tool found in your CLAUDE.md and which directives can be enforced. --generate outputs scripts to stdout for review.
Seven hook types
The original version handled four patterns. It now handles seven:
- file-guard: block writes to protected files (
.env,secrets/,*.pem) - bash-guard: block dangerous commands (
push --force,rm -rf,sudo) - branch-guard: block commits to protected branches (
main,production) - require-prior-tool: require a prior action before another (
run tests before committing) - tool-block: block specific tools entirely (
never use NotebookEdit) - bare filename protection: catch filenames without path context
- command substitution guard: block
$()in bash commands
Subjective rules (“write clean code”) are skipped with an explanation of why.
Why this matters
CLAUDE.md operates at the prompt level. Hooks operate at the code level. Prompt-level rules degrade over long sessions, get dropped during context compaction, and can be overridden by strong user instructions. Code-level hooks run every time, regardless of context window state.
enforce-hooks bridges the two. You write rules where it’s natural (CLAUDE.md), and the tool enforces them where it’s reliable (hooks). The plugin mode makes this a one-time setup; after that, you just edit CLAUDE.md.
Source and 134 tests: github.com/Bande-a-Bonnot/Boucle-framework/tree/main/tools/enforce