How I Automate My Dev Workflow with Claude Code Hooks

Dev.to / 4/28/2026

💬 OpinionDeveloper Stack & InfrastructureTools & Practical Usage

Key Points

  • Claude Code hooks are shell commands configured in settings.json that run automatically in response to specific agent events, enabling lightweight automation without plugins or special runtimes.
  • The article explains practical hook events—PreToolUse, PostToolUse, and Stop—and how they can be used to block risky commands, trigger side-effect tasks like linting/builds, and send notifications on agent shutdown.
  • The author reports using hooks in production for months to implement workflow automation patterns such as Telegram alerts when an agent stops and automated build scripts after file edits.
  • A key theme is that hooks cover automation needs that subagents and memory cannot fully replace, and the post documents the patterns that work reliably.

Claude Code hooks are the feature most developers skip past. They sit in settings.json under a hooks key, they run shell commands in response to agent events, and they unlock a class of automation that subagents and memory cannot replace. I have been running hooks in production for a few months. I use them to notify Telegram when an agent stops, block bash commands that match risky patterns, and fire build scripts after file edits. This post documents the patterns that actually work.

What Claude Code Hooks Are

Hooks are shell commands that run automatically when Claude Code does something. That's it. No plugins, no extensions, no special runtime. You write a command, attach it to an event, and Claude executes it as part of its normal operation.

They live in ~/.claude/settings.json (global) or .claude/settings.json (project-level) under the hooks key. A hook entry looks like this:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "npm run lint --silent"
          }
        ]
      }
    ]
  }
}

The three hook events you'll actually use:

  • PreToolUse — fires before Claude runs a tool. Return a non-zero exit code to block the action entirely.
  • PostToolUse — fires after a tool completes. Use for side effects: linting, building, logging.
  • Stop — fires when Claude's agent session ends. Good for notifications and cleanup.

There's also SubagentStop for when a spawned subagent finishes, and PreCompact for before context compaction runs.

The matcher field is a regex matched against the tool name. Write|Edit catches both file write and edit operations. Bash catches every shell command Claude runs. Leave matcher empty to catch everything.

Pattern 1: Telegram Notification on Agent Stop

The most useful hook I run. When Claude finishes a long task, I get a Telegram message instead of watching the terminal.

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "curl -s -X POST 'https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage' -d chat_id=${TELEGRAM_CHAT_ID} -d text='Claude+stopped'"
          }
        ]
      }
    ]
  }
}

I use a wrapper script that sends the full session summary, not just a ping. The hook passes context via environment variables — CLAUDE_STOP_HOOK_ACTIVE, tool name, tool input — so you can make the message more specific.

For longer sessions I pipe the stop reason into the message body. Claude includes a stop_reason in the hook payload. A session that stopped because it hit a blocker gets a different message than one that completed normally.

The practical result: I kick off a large refactor, switch to something else, and get notified on my phone when it's done. No terminal babysitting.

Pattern 2: Blocking Risky Bash Commands

PreToolUse with a non-zero exit code blocks Claude from running the tool. Combined with a matcher on Bash, this gives you a guard layer before any shell command executes.

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/check-command.sh"
          }
        ]
      }
    ]
  }
}

The check-command.sh script reads the command from stdin (Claude pipes the tool input as JSON) and checks it against a blocklist:

#!/bin/bash
INPUT=$(cat)
CMD=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('command',''))")

BLOCKED_PATTERNS=(
  "git push --force"
  "rm -rf /"
  "DROP TABLE"
  "git reset --hard"
)

for pattern in "${BLOCKED_PATTERNS[@]}"; do
  if echo "$CMD" | grep -qi "$pattern"; then
    echo "Blocked: $pattern" >&2
    exit 1
  fi
done
exit 0

Exit 1 blocks the command. Claude sees the stderr output and knows why it was blocked. It can then ask for confirmation or try a safer alternative.

This is not a security boundary — it's a guard against accidental mistakes in long autonomous sessions. Claude has permissions to do most things; the hook catches the patterns I've decided should never run without me seeing them first.

I also use this for project-specific rules. One project has a hook that blocks npm install without a lockfile check. Another blocks any mutation to the production database connection string.

Pattern 3: Build Trigger After File Edits

When Claude edits TypeScript or config files, I want the build to run immediately. Not as a separate step I have to remember — as an automatic response to the edit.

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [
          {
            "type": "command",
            "command": "~/.claude/hooks/maybe-build.sh"
          }
        ]
      }
    ]
  }
}

The maybe-build.sh script checks if the edited file is inside the src/ directory and if so runs npm run build --silent. It reads the edited path from the hook JSON payload on stdin.

The key detail: PostToolUse hooks run synchronously. Claude waits for the hook to complete before moving to the next step. If the build fails, Claude sees the output and can fix the issue in the same session — before it moves on and forgets what it just changed.

This pattern catches a class of bugs that normally surface in CI, hours after the edit. Type errors, missing imports, broken exports — they show up immediately instead of in a pull request review.

What Hooks Cannot Do

Hooks run shell commands. They do not modify Claude's behavior mid-session, inject new context, or override Claude's responses. They are side effects only.

Memory and preferences in CLAUDE.md affect how Claude thinks. Hooks affect what happens around Claude's actions. These are different layers — hooks are not a replacement for good prompting or project configuration.

Hooks also do not run if Claude is in a subagent spawned without the parent's settings. Project-level hooks in .claude/settings.json do apply to subagents running in that directory. Global hooks only apply to the top-level session.

The Settings File Structure

Full example of a working hooks configuration:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [{ "type": "command", "command": "~/.claude/hooks/check-command.sh" }]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write|Edit",
        "hooks": [{ "type": "command", "command": "~/.claude/hooks/maybe-build.sh" }]
      }
    ],
    "Stop": [
      {
        "hooks": [{ "type": "command", "command": "~/.claude/hooks/notify-telegram.sh" }]
      }
    ]
  }
}

Hooks run in order within each event type. Multiple hooks on the same event all execute. For PreToolUse, the first non-zero exit code blocks the action — subsequent hooks in that event do not run.

FAQ

Where do hooks run — local machine or Claude's sandbox?
Hooks run on your local machine, in the same environment where Claude Code is running. They have access to your env vars, filesystem, and network. There is no sandboxing.

Can a hook pass data back to Claude?
Yes. Whatever your hook writes to stdout is captured and can influence Claude's next step. For PostToolUse hooks, stdout output becomes part of the tool result Claude sees. For PreToolUse, non-empty stderr is shown to Claude as the reason for a block.

Do hooks work in headless/cron sessions?
Yes. Hooks run wherever Claude Code runs. I use the Stop hook in my automated Paperclip agent sessions — it fires at the end of each heartbeat run and logs completion to a file.

What if the hook script doesn't exist?
Claude logs a warning and continues. A missing hook script does not block the session. This is intentional — hooks are additive, not required.

I share more automation patterns in my Build & Automate community on Skool — including the full Telegram notification setup and how I structure hook scripts across projects.

Published using Notipo — markdown editor with one-click WordPress publishing.