Claude Code hooks for Elixir and Phoenix

The deterministic layer: shell scripts that always run, across every Claude Code lifecycle event — blocking destructive mix commands, verifying Iron Laws, and injecting context models cannot read themselves.

View source
On this page
  1. Contents
  2. The five things hooks do
  3. What fires when
  4. Full hook list
  5. PreToolUse — can block
  6. PostToolUse — advises after the edit
  7. PostToolUseFailure — when mix fails
  8. UserPromptSubmit / SubagentStart — context injection
  9. SessionStart
  10. Compaction and exit
  11. Two rules that govern all of them
  12. Everything Elixir-specific is gated on mix.exs
  13. Hooks fail open, never closed
  14. Turning hooks off
  15. Deep dives

Why hooks instead of instructions?

Skills and agents are instructions a model may or may not follow. Hooks are shell scripts that always run. The plugin ships 23 of them across 10 Claude Code lifecycle events, and that split is deliberate: guidance that benefits from judgement stays in skills, and anything that must happen regardless of judgement moves here.

What made this necessary?

Session analysis across 400 sessions measured CLAUDE.md prose rules firing ~0% of the time. The rules were correct — they were simply never consulted mid-task. Intent routing and Iron Law injection moved into hooks because a rule that does not fire is not a rule.

Is it safe to install the plugin globally?

Yes. Every Elixir-specific hook checks for mix.exs before doing anything, so the plugin is inert in a Rust or Python repository — no directories created, no Phoenix rules injected, no banners. The one deliberate exception is the force-push block, which is dangerous in any repository.

What happens when a hook breaks?

Hooks fail open, never closed. No script uses set -e, and denials travel as JSON data rather than exit codes. A broken hook means the guard is off — never that the session is unusable. That rule was written after a corrupted safety script exited non-zero and blocked every Bash call in a session.

Hooks

The plugin ships 23 hooks across 10 lifecycle events. They are the deterministic layer: skills and agents are instructions a model may or may not follow, hooks are shell scripts that always run.

That distinction is the whole reason they exist. Session analysis across 400 sessions measured CLAUDE.md prose rules firing ~0% of the time — the rules were correct and simply never consulted. Anything that genuinely must happen lives here instead.

Every script is plain, auditable bash in plugins/elixir-phoenix/hooks/scripts/. No telemetry, no network calls except a localhost Tidewave probe and git fetch.

Contents

The five things hooks do

1. Stop destructive commands. mix ecto.reset, mix ecto.drop, MIX_ENV=prod mix …, and force pushes are denied before they execute, with a safer alternative attached so Claude does not just retry.

2. Verify code against the Iron Laws. After every edit to an Elixir file, the content you just wrote is scanned for seven of the non-negotiable rules — :float for money, String.to_atom on user input, raw/1 with a variable, implicit cross joins, unsupervised start_link, and more. A violation feeds the specific law and line back to Claude for correction.

3. Get context to models that cannot read it themselves. Subagents start with fresh context and cannot reliably read plugin reference files. A SubagentStart hook injects all 26 Iron Laws into every one. A UserPromptSubmit hook spots PR URLs and stack traces and suggests the right /phx: command.

4. Keep the plan workflow coherent. Writing a plan.md hard-stops the run so you can review before implementation. Compaction re-injects the current phase’s rules and your scratchpad’s dead ends. An API failure leaves a breadcrumb the next session reads.

5. Break debugging loops. Repeated mix failures escalate from generic hints to a structured critic that consolidates the error history and tells Claude to stop retrying — the Critic→Refiner pattern from AutoHarness (Lou et al., 2026).

What fires when

  session opens

      ├─ SessionStart ······ setup dirs · detect Tidewave · detect Ash
      │                      resume banner · scratchpad · branch freshness

  you type a prompt

      ├─ UserPromptSubmit ·· PR URL / stack trace → suggest a /phx: command

  Claude picks a tool

      ├─ PreToolUse ········ BLOCK destructive bash · deps gate · edit freeze

    tool runs ──────────────────┐
      │                         │ (failed)
      │ (ok)                    ▼
      │                    PostToolUseFailure ·· mix hints → loop critic

      ├─ PostToolUse ······· format · Iron Laws · debug stmts · security
      │                      plan STOP · edit metrics

      ├─ SubagentStart ····· inject Iron Laws into every spawned agent

  context fills up

      ├─ PreCompact ········ re-inject phase rules + scratchpad dead ends
      ├─ PostCompact ······· verify plan state survived

  turn ends

      ├─ Stop ·············· warn about background tasks / crons
      └─ StopFailure ······· log API failure for next session's resume

Full hook list

PreToolUse — can block

HookFires onDoes
block-dangerous-opsevery BashDenies mix ecto.reset/drop, force push, MIX_ENV=prod
deps-audit-gatemix deps.*Tiered supply-chain gate (cache hit <200 ms, fast rules <2 s)
freeze-gateEdit/Write/NotebookEditEnforces /phx:freeze edit-scope locks

PostToolUse — advises after the edit

HookFires onDoes
format-elixir*.ex, *.exsWarns if unformatted. Never rewrites — that would break Claude’s file state
iron-law-verifier*.ex, *.exsScans only the lines you just wrote for 7 Iron Laws
debug-statement-warning*.ex (not tests)Flags IO.inspect, dbg(), IO.puts
security-reminderauth-ish filenamesRe-states the 4 security Iron Laws
plan-stop-reminderWrite(*plan.md)Hard-stops before implementation
log-progressany Edit/WriteAppends edit metrics JSONL for /skill-monitor

PostToolUseFailure — when mix fails

HookDoes
elixir-failure-hintsCommand-specific hints for compile / test / credo / ecto
error-criticCounts repeats. Attempt 2 warns; attempt 3+ consolidates the error history and blocks the retry reflex

UserPromptSubmit / SubagentStart — context injection

HookDoes
route-intentPR URL → /phx:pr-review; stack trace or Tidewave page context → /phx:investigate. Once per category per session, never on an explicit slash command
inject-iron-lawsInjects all 26 Iron Laws into every spawned subagent

SessionStart

HookDoes
setup-dirsCreates .claude/{plans,reviews,solutions,audit,skill-metrics,research}
detect-tidewaveProbes localhost:4000 — only if tidewave is a dependency
detect-ashDetects Ash, prints codegen rules, checks usage_rules config
check-scratchpadSurfaces dead-end notes; seeds a scratchpad template
check-resume↻ Plan 'x' has N remaining tasks. Resume with: /phx:work …
check-branch-freshnessWarns when your branch is behind main

Compaction and exit

HookEventDoes
precompact-rulesPreCompactRe-injects the active phase’s rules plus scratchpad dead ends
postcompact-verifyPostCompactTells Claude to re-read the plan if tasks remain
stop-failure-logStopFailureWrites an API-failure note to the scratchpad
check-pending-plansStopWarns about running background tasks and scheduled crons

Two rules that govern all of them

Everything Elixir-specific is gated on mix.exs

Every hook checks for mix.exs before doing anything. Open a Rust or Python repo with the plugin installed globally and it is inert — no directories created, no Phoenix Iron Laws injected, no banners.

The single deliberate exception is the force-push block, which is dangerous in any repository and stays global.

This gating (added in v2.10.1, issue #55) is what makes it safe to enable the plugin globally rather than per-project.

Hooks fail open, never closed

No hook uses set -e. Denials are expressed as JSON data, never as an exit code, and the always-on Bash gate is registered as script.sh || exit 0.

This is not theoretical. The safety script once got corrupted by merge-conflict markers, and because bash exited non-zero, every Bash command in the session was blocked. A broken hook must degrade to “the guard is off”, never to “the session is unusable”.

Turning hooks off

The plugin does not ship a global switch, and most hooks are advisory — they print a message and get out of the way. For the ones that block:

HookEscape hatch
deps-audit-gatePHX_SKIP_DEPS_AUDIT=1 mix deps.get
freeze-gate/phx:freeze off (dormant unless you turned it on)
block-dangerous-opsBy design, none — run the command yourself in your terminal. In Claude Code, prefix with !

To disable a hook outright, remove its entry from plugins/elixir-phoenix/hooks/hooks.json in your installed copy, or uninstall the plugin. /reload-plugins picks up the change without a restart.

Deep dives

Each group has a reference page with the patterns, the failure modes they were built from, and the exact output you see:

PageCovers
Safety GatesThe three blocking hooks, the fail-open contract, whitespace-evasion anchoring
Code QualityThe 7 verified Iron Laws, blame-aware scanning, why formatting only warns
Failure RecoveryThe escalation ladder and the Critic→Refiner pattern
Context InjectionIntent routing, subagent Iron Laws, omitClaudeMd vs Iron Laws
Session LifecycleAll six SessionStart hooks and their gating
Workflow StatePlan STOP, compaction survival, the StopFailure breadcrumb

Contributors adding or changing a hook should start with the contributor guide, which carries the per-event output-channel table and the shared conventions.