A Flutter app used to showcase Claude Code hooks.
Hooks are shell commands that Claude Code runs automatically at specific points in its lifecycle (before/after a tool runs, when a session starts, etc.). They let you enforce policy and automate chores deterministically — without relying on the model to remember to do them. This repo wires up three small Python hooks so you can watch them fire during a normal Claude Code session.
The app itself is just the default Flutter counter template; the interesting part is
the .claude/ directory.
Hooks are registered in .claude/settings.json, which maps a Claude Code event + tool matcher to a script:
| Event | Matcher | Script | What it does |
|---|---|---|---|
PreToolUse |
Bash |
block_rm_rf.py | Blocks dangerous rm -rf commands before they run. |
PostToolUse |
Edit|Write|MultiEdit |
format_dart.py | Runs dart format on a .dart file after Claude edits it. |
Both scripts share hook_logger.py, which appends a
timestamped line (and the full payload) to .claude/hooks/hooks.log every time a hook
fires — handy for seeing exactly what Claude Code passed in.
Claude Code pipes a JSON payload describing the tool call to the script's stdin. The script inspects it and communicates back via stdout/exit code:
- Allow (the default) — print nothing and exit
0. - Block / deny — print a JSON decision on stdout. For
PreToolUse, apermissionDecision: "deny"stops the tool from running and shows the reason to Claude.
Inspects every Bash command before it executes. If the command is a recursive and
force rm (e.g. rm -rf, rm -fr, rm --recursive --force), the hook returns a
deny decision so Claude Code refuses to run it. Anything else is allowed through
untouched. This is a deterministic guardrail — the model can't talk its way past it.
Runs after any Edit, Write, or MultiEdit. If the touched file ends in .dart,
it shells out to dart format <file> so edits are always consistently formatted. Non-Dart
files are skipped, and if dart isn't on PATH the hook silently no-ops — it never
blocks or fails the turn.
Not a hook itself; a small module imported by the other two. It exposes:
log(script, **props)— one timestampedkey=valueline per call.log_payload(script, payload)— the full, untruncated stdin payload.
Output goes to .claude/hooks/hooks.log. Logging failures are caught so they can never
break a safety or formatting hook.
The hooks are Python scripts with no third-party dependencies, so there's nothing to install beyond the basics:
- Python 3 on your
PATH(the scripts run viapython3). - Dart/Flutter SDK so
dart formatis available for the formatter hook (optional — the hook no-ops without it). - Open this project in Claude Code. Project hooks in
.claude/settings.jsonare picked up automatically. Run/hooksinside Claude Code to confirm they're registered.
Security note: Hooks run arbitrary shell commands with your user permissions whenever their event fires. Review hook scripts before trusting a project's
.claude/settings.json.
-
Formatter: ask Claude to edit any
.dartfile (e.g.lib/main.dart) and watchdart formatrun afterward. -
Safety guard: ask Claude to run
rm -rfon something — the command is blocked before it executes. -
Logs: tail the shared log to see each hook fire:
tail -f .claude/hooks/hooks.log