I automated my entire dev workflow with Claude Code hooks
<p>Most people use Claude Code as a smarter terminal assistant.<br> Type a request, read the response, approve the changes.<br> That's fine, but it leaves a lot of capability on the table.</p> <p>Claude Code has a hook system that wires the AI directly into your existing workflow:<br> your formatter, your test runner, your notification system.<br> We've been running hooks in production-style setups for a few months now<br> and the interaction model genuinely changes when the tool stops being conversational<br> and starts being ambient.</p> <p>Here's what we actually run and why.</p> <h2> What hooks are </h2> <p>Hooks are defined in <code>~/.claude/settings.json</code> under the <code>hooks</code> key.<br> Each hook fires at a lifecycle event and runs a shell command.</p> <p>The four events
Most people use Claude Code as a smarter terminal assistant. Type a request, read the response, approve the changes. That's fine, but it leaves a lot of capability on the table.
Claude Code has a hook system that wires the AI directly into your existing workflow: your formatter, your test runner, your notification system. We've been running hooks in production-style setups for a few months now and the interaction model genuinely changes when the tool stops being conversational and starts being ambient.
Here's what we actually run and why.
What hooks are
Hooks are defined in ~/.claude/settings.json under the hooks key. Each hook fires at a lifecycle event and runs a shell command.
The four events that matter:
-
PreToolUse -- fires before Claude runs a tool (file write, bash command, etc.)
-
PostToolUse -- fires after a tool completes
-
Notification -- fires when Claude sends status updates
-
Stop -- fires when Claude finishes a response
Hooks can be filtered by tool name, so you can target Bash separately from Write and Edit.
The basic structure:
{ "hooks": { "PostToolUse": [ { "matcher": "Write", "hooks": [ { "type": "command", "command": "your-command-here" } ] } ] } }{ "hooks": { "PostToolUse": [ { "matcher": "Write", "hooks": [ { "type": "command", "command": "your-command-here" } ] } ] } }Enter fullscreen mode
Exit fullscreen mode
Hook 1: Auto-format on file write
Every time Claude writes or edits a file, format it immediately. This keeps diffs clean and removes a whole category of review noise.
{ "hooks": { "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "bash -c 'FILE=$(echo $CLAUDE_TOOL_OUTPUT | python3 -c \"import sys,json; d=json.load(sys.stdin); print(d.get(\\\"filePath\\\",\\\"\\\"))\" 2>/dev/null); if [ -n \"$FILE\" ]; then case \"$FILE\" in *.rs) cargo fmt -- \"$FILE\" 2>/dev/null;; *.py) ruff format \"$FILE\" 2>/dev/null;; *.ts|*.tsx|*.js) npx prettier --write \"$FILE\" 2>/dev/null;; esac; fi'" } ] } ] } }{ "hooks": { "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "bash -c 'FILE=$(echo $CLAUDE_TOOL_OUTPUT | python3 -c \"import sys,json; d=json.load(sys.stdin); print(d.get(\\\"filePath\\\",\\\"\\\"))\" 2>/dev/null); if [ -n \"$FILE\" ]; then case \"$FILE\" in *.rs) cargo fmt -- \"$FILE\" 2>/dev/null;; *.py) ruff format \"$FILE\" 2>/dev/null;; *.ts|*.tsx|*.js) npx prettier --write \"$FILE\" 2>/dev/null;; esac; fi'" } ] } ] } }Enter fullscreen mode
Exit fullscreen mode
The hook reads the file path from the tool output, detects the extension, and runs the right formatter. Silent on error (2>/dev/null) so it doesn't interrupt the session if a formatter isn't installed.
For Rust specifically, we also add a cargo check after writes. This catches type errors while Claude is still in context and can fix them in the same pass:
{ "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "bash -c 'FILE=$(echo $CLAUDE_TOOL_OUTPUT | python3 -c \"import sys,json; d=json.load(sys.stdin); print(d.get(\\\"filePath\\\",\\\"\\\"))\" 2>/dev/null); if echo \"$FILE\" | grep -q \"\\.rs$\"; then cargo check 2>&1 | tail -5; fi'" } ] }{ "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "bash -c 'FILE=$(echo $CLAUDE_TOOL_OUTPUT | python3 -c \"import sys,json; d=json.load(sys.stdin); print(d.get(\\\"filePath\\\",\\\"\\\"))\" 2>/dev/null); if echo \"$FILE\" | grep -q \"\\.rs$\"; then cargo check 2>&1 | tail -5; fi'" } ] }Enter fullscreen mode
Exit fullscreen mode
Hook 2: Security scan before bash commands
Before Claude runs any shell command, scan for patterns worth flagging: piping to sh/bash from curl, rm -rf without bounds, writes to /etc/.
{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "bash -c 'CMD=$(echo $CLAUDE_TOOL_INPUT | python3 -c \"import sys,json; d=json.load(sys.stdin); print(d.get(\\\"command\\\",\\\"\\\"))\" 2>/dev/null); RISKY=0; echo \"$CMD\" | grep -qE \"curl.*\\|.*(bash|sh)\" && RISKY=1; echo \"$CMD\" | grep -qE \"rm -rf /[^t]\" && RISKY=1; echo \"$CMD\" | grep -q \"/etc/\" && RISKY=1; if [ $RISKY -eq 1 ]; then echo \"[hook] high-risk command flagged -- review before proceeding\"; fi'" } ] } ] } }{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [ { "type": "command", "command": "bash -c 'CMD=$(echo $CLAUDE_TOOL_INPUT | python3 -c \"import sys,json; d=json.load(sys.stdin); print(d.get(\\\"command\\\",\\\"\\\"))\" 2>/dev/null); RISKY=0; echo \"$CMD\" | grep -qE \"curl.*\\|.*(bash|sh)\" && RISKY=1; echo \"$CMD\" | grep -qE \"rm -rf /[^t]\" && RISKY=1; echo \"$CMD\" | grep -q \"/etc/\" && RISKY=1; if [ $RISKY -eq 1 ]; then echo \"[hook] high-risk command flagged -- review before proceeding\"; fi'" } ] } ] } }Enter fullscreen mode
Exit fullscreen mode
This doesn't block the command. It prints a visible warning in the output so you catch it when skimming. We've caught a few genuine mistakes this way -- not AI hallucinations, just cases where a reasonable command had an unexpected side effect in context.
Hook 3: Desktop notification when Claude finishes
Switch to another window during a long task and you lose track of when it's done. The Stop hook fires on response completion.
{ "hooks": { "Stop": [ { "hooks": [ { "type": "command", "command": "notify-send 'Claude Code' 'Done.' --icon=terminal --urgency=low 2>/dev/null || true" } ] } ] } }{ "hooks": { "Stop": [ { "hooks": [ { "type": "command", "command": "notify-send 'Claude Code' 'Done.' --icon=terminal --urgency=low 2>/dev/null || true" } ] } ] } }Enter fullscreen mode
Exit fullscreen mode
On Ubuntu this uses notify-send. On macOS, swap it for osascript -e 'display notification "Done." with title "Claude Code"'.
For remote machines, we send to a Discord webhook instead:
{ "type": "command", "command": "bash -c 'source ~/.secrets; curl -s -X POST \"$DISCORD_WEBHOOK_DEV\" -H \"Content-Type: application/json\" -d \"{\\\"content\\\": \\\"Claude finished a task\\\"}\" > /dev/null'" }{ "type": "command", "command": "bash -c 'source ~/.secrets; curl -s -X POST \"$DISCORD_WEBHOOK_DEV\" -H \"Content-Type: application/json\" -d \"{\\\"content\\\": \\\"Claude finished a task\\\"}\" > /dev/null'" }Enter fullscreen mode
Exit fullscreen mode
Hook 4: Custom status line with git context
The Notification hook fires when Claude sends progress updates (tool names, status messages). We use it to set the terminal title to show current branch and last action:
{ "hooks": { "Notification": [ { "hooks": [ { "type": "command", "command": "bash -c 'BRANCH=$(git branch --show-current 2>/dev/null || echo \"no-git\"); MSG=$(echo $CLAUDE_NOTIFICATION | python3 -c \"import sys,json; d=json.load(sys.stdin); print(d.get(\\\"message\\\",\\\"\\\")[:40])\" 2>/dev/null); printf \"\\033]0;claude [%s] %s\\007\" \"$BRANCH\" \"$MSG\"'" } ] } ] } }{ "hooks": { "Notification": [ { "hooks": [ { "type": "command", "command": "bash -c 'BRANCH=$(git branch --show-current 2>/dev/null || echo \"no-git\"); MSG=$(echo $CLAUDE_NOTIFICATION | python3 -c \"import sys,json; d=json.load(sys.stdin); print(d.get(\\\"message\\\",\\\"\\\")[:40])\" 2>/dev/null); printf \"\\033]0;claude [%s] %s\\007\" \"$BRANCH\" \"$MSG\"'" } ] } ] } }Enter fullscreen mode
Exit fullscreen mode
The terminal title becomes something like claude [main] Writing src/main.rs. When you have multiple Claude sessions across different projects, this is the only sane way to tell them apart in your taskbar.
Hook 5: Test runner on source file changes
For TDD-style work, tests should run automatically after Claude modifies source files. The tight feedback loop matters: Claude writes code, tests run, Claude sees the result in the same context window and can iterate without prompting.
{ "hooks": { "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "bash -c 'FILE=$(echo $CLAUDE_TOOL_OUTPUT | python3 -c \"import sys,json; d=json.load(sys.stdin); print(d.get(\\\"filePath\\\",\\\"\\\"))\" 2>/dev/null); if echo \"$FILE\" | grep -qE \"\\.(rs|py|ts)$\" && ! echo \"$FILE\" | grep -qE \"(test|spec)\"; then echo \"[hook] running tests...\"; if [ -f Cargo.toml ]; then cargo test 2>&1 | tail -10; elif [ -f pyproject.toml ]; then python -m pytest -x -q 2>&1 | tail -10; fi; fi'" } ] } ] } }{ "hooks": { "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "bash -c 'FILE=$(echo $CLAUDE_TOOL_OUTPUT | python3 -c \"import sys,json; d=json.load(sys.stdin); print(d.get(\\\"filePath\\\",\\\"\\\"))\" 2>/dev/null); if echo \"$FILE\" | grep -qE \"\\.(rs|py|ts)$\" && ! echo \"$FILE\" | grep -qE \"(test|spec)\"; then echo \"[hook] running tests...\"; if [ -f Cargo.toml ]; then cargo test 2>&1 | tail -10; elif [ -f pyproject.toml ]; then python -m pytest -x -q 2>&1 | tail -10; fi; fi'" } ] } ] } }Enter fullscreen mode
Exit fullscreen mode
Skips test files themselves to avoid infinite loops. Detects project type by manifest file. Tails 10 lines so the output stays readable.
Putting it together
The full hooks section in ~/.claude/settings.json:
{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [{ "type": "command", "command": "..." }] } ], "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "..." }, { "type": "command", "command": "..." } ] } ], "Notification": [ { "hooks": [{ "type": "command", "command": "..." }] } ], "Stop": [ { "hooks": [{ "type": "command", "command": "..." }] } ] } }{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "hooks": [{ "type": "command", "command": "..." }] } ], "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "..." }, { "type": "command", "command": "..." } ] } ], "Notification": [ { "hooks": [{ "type": "command", "command": "..." }] } ], "Stop": [ { "hooks": [{ "type": "command", "command": "..." }] } ] } }Enter fullscreen mode
Exit fullscreen mode
Multiple hooks can fire for the same event and run in sequence.
Caveats worth knowing
PreToolUse hooks run synchronously -- a slow pre-hook adds latency before every tool call. Keep them under 100ms or use them sparingly.
PostToolUse hooks run after the tool completes, so slow post-hooks don't block Claude. They just add noise to the output, which is usually fine.
If a hook exits non-zero, Claude sees the output but continues. By default hooks are advisory, not blocking. There's a blocking mode available for PreToolUse, but we haven't needed it -- the warning output is enough.
The hook system is underused because it's not obvious that it exists. Once you wire Claude into your actual toolchain, the conversational back-and-forth collapses into something closer to a fast pair programmer who runs your checks automatically.
The full config is in a public gist at github.com/noxcraftdev if you want a starting point.
Happy coding!*
DEV Community
https://dev.to/noxcraftdev/i-automated-my-entire-dev-workflow-with-claude-code-hooks-2e73Sign in to highlight and annotate this article

Conversation starters
Daily AI Digest
Get the top 5 AI stories delivered to your inbox every morning.
More about
claudemodelavailableKnowledge Map
Connected Articles — Knowledge Graph
This article is connected to other articles through shared AI topics and tags.
More in Products
Security flaw in OpenAI s Atlas browser is a warning for all AI agents
A simple parsing error allows crafted URLs to become powerful, malicious commands that dupe AI browsers such as OpenAI Atlas. The post Security flaw in OpenAI’s Atlas browser is a warning for all AI agents first appeared on TechTalks .

Scientists Build Living Robots With Nervous Systems
Engineers have long tried to mimic life. They’ve built machine-learning algorithms modeled after the human brain , designed machines that walk like dogs or fly like insects , and taught robots to adapt, however clumsily , to the world around them. Now they are skipping imitation altogether. Instead of taking inspiration from biology, they are building robots out of it: fashioning tiny, free-swimming assemblages of living cells that organize into self-directed systems, complete with neurons that wire themselves into functional circuits. The result, reported last month in Advanced Science , is what the researchers call a “neurobot.” These living machines could help scientists better understand how simple neural networks give rise to complex behaviors, a foundational step toward building cybo
The generative AI loop: Why more use leads to better decision-making
Business AI is central to executive decision-making, with strong feedback loops enhancing its value. Companies prioritizing data quality and intuitive tools yield significant productivity gains. The post The generative AI loop: Why more use leads to better decision-making first appeared on TechTalks .
When machines start predicting tomorrow: How AI is rewriting the rhythm of global operations
Artificial intelligence is revolutionizing operations by predicting needs and preventing disruptions. Industries leverage predictive systems to enhance efficiency, sustainability, and readiness for future challenges. The post When machines start predicting tomorrow: How AI is rewriting the rhythm of global operations first appeared on TechTalks .


Discussion
Sign in to join the discussion
No comments yet — be the first to share your thoughts!