·17 min read

Hooks, Statuslines, and the Automation Layer Nobody Talks About

claude-codetutorialengineering

Most people interact with Claude Code as a prompt-response tool. You type something, it does something, you review, you type again. That loop is productive, and I covered the basics of that interaction model earlier in this series. But it's also incomplete, because underneath it there's an event system that lets you hook into everything Claude Code does.

Session starts? Run a script. About to edit a file? Check a condition first. Finished a task? Trigger a notification. Tool use failed? Log it and alert.

This is the automation layer. It's the part of Claude Code that turns it from an interactive tool you use into a programmable system you build on top of. And almost nobody talks about it, because the documentation is sparse and the use cases aren't obvious until you've lived with them for a while.

I've been running hooks in my setup for months. They handle things I used to do manually -- update checks, guardrails, notifications, formatting. Small things individually, but they compound. The best analogy I have is git hooks. Nobody thinks about their pre-commit hook until it catches a bug. Then they can't imagine working without it. Claude Code hooks are the same idea, applied to an AI agent instead of a version control system.

The hook lifecycle in Claude Code. Each event type fires at a specific moment, and hooks can observe, modify, or block the action.

What Hooks Actually Are

Hooks are shell commands that execute in response to Claude Code lifecycle events. You define them in your settings.json, and they run automatically when the corresponding event fires. No manual triggering. No remembering to run a script. The system handles it.

There are four hook types, each tied to a different moment in Claude Code's lifecycle.

SessionStart fires when a new Claude Code session begins. Before Claude has read any files, before it's processed your first prompt, the SessionStart hook runs. This is where you put environment setup -- loading context, checking for updates, initializing state, verifying that dependencies are available. Anything you'd want to be true before work begins.

Stop fires when Claude finishes its turn. Not when the session ends -- when Claude completes a response and is waiting for your next prompt. This is the hook that powers the Ralph Loop, the autonomous re-feed mechanism I covered in an earlier post. It's also useful for notifications, cleanup, and logging. Every time Claude says "done," your Stop hook has a chance to react.

PreToolUse fires before a tool executes. This is the guardrail hook. Claude wants to write a file? PreToolUse runs first. Claude wants to run a shell command? PreToolUse checks it. If your hook exits with a non-zero status, the tool use is blocked. This gives you veto power over every action Claude takes. It's the most powerful hook type and the one that requires the most care.

PostToolUse fires after a tool executes successfully. This is the observation hook. A file was written -- now you can lint it. A command was run -- now you can log it. A commit was made -- now you can verify it. PostToolUse is reactive rather than preventive. It doesn't block anything. It responds to what happened.

Each hook gets context about the event as environment variables -- which tool is being used, what arguments were passed, what the result was. You can use this context to make your hooks targeted rather than blanket. A PreToolUse hook that only cares about Write tool calls can ignore everything else.

The configuration lives in settings.json and looks like this:

"hooks": {
  "SessionStart": [
    {
      "command": "node /path/to/your/script.js",
      "timeout": 5000
    }
  ],
  "PreToolUse": [
    {
      "command": "/path/to/guardrail.sh",
      "timeout": 2000
    }
  ]
}

The timeout matters. Hooks run synchronously by default. A slow hook blocks everything. More on that later.

My Hooks in Practice

My SessionStart hook runs a GSD update check. Every time I start Claude Code, it executes node gsd-check-update.js, which checks whether the GSD plugin has a newer version available. If there's an update, it prints a one-line notice. If not, it exits silently.

This sounds trivial. It is trivial. That's the point. Before I had this hook, I'd periodically remember to check for GSD updates, find that I was three versions behind, update, and discover that I'd been missing features for weeks. Now it's automatic. I don't think about it. The system handles it.

That's the design philosophy of hooks. Each individual hook does something small. The value comes from the accumulation. You stop thinking about the things hooks handle, which frees your attention for the things that actually need it.

Building Useful Hooks

Let me walk through four practical hooks that I think most Claude Code users would benefit from. These aren't theoretical. Each one addresses a real problem I've hit.

Auto-Lint on File Save

When Claude edits or writes a file, the result is usually correct but not always formatted to your project's standards. Claude knows about your linting rules if they're in CLAUDE.md, but it doesn't always apply them perfectly. Rather than catching formatting issues during review, you can catch them immediately.

#!/bin/bash
# post-tool-lint.sh -- runs after Edit or Write tool use
TOOL_NAME="$CLAUDE_TOOL_NAME"
FILE_PATH="$CLAUDE_TOOL_ARG_FILE_PATH"
 
if [[ "$TOOL_NAME" == "Edit" || "$TOOL_NAME" == "Write" ]]; then
  if [[ "$FILE_PATH" == *.ts || "$FILE_PATH" == *.tsx || "$FILE_PATH" == *.js ]]; then
    npx eslint --fix "$FILE_PATH" 2>/dev/null
  fi
fi

This is a PostToolUse hook. After Claude writes or edits a TypeScript or JavaScript file, it runs eslint with auto-fix. The 2>/dev/null suppresses noise. If eslint finds nothing, it exits silently. If it fixes something, the file is updated in place before you ever see it.

The key decision here is using --fix instead of just --check. I want the hook to fix problems, not just report them. If I wanted reporting, I'd add it to my review process instead. Hooks should act, not inform.

Block Commits to Main

This one has saved me more than once. In the middle of a fast session, it's easy to ask Claude to commit directly to main when you meant to be on a feature branch. A PreToolUse hook can catch this before the commit happens.

#!/bin/bash
# block-main-commit.sh -- prevents direct commits to main
TOOL_NAME="$CLAUDE_TOOL_NAME"
 
if [[ "$TOOL_NAME" == "Bash" ]]; then
  COMMAND="$CLAUDE_TOOL_ARG_COMMAND"
  BRANCH=$(git branch --show-current 2>/dev/null)
 
  if [[ "$BRANCH" == "main" || "$BRANCH" == "master" ]]; then
    if echo "$COMMAND" | grep -q "git commit"; then
      echo "BLOCKED: Cannot commit directly to $BRANCH. Create a feature branch first."
      exit 1
    fi
  fi
fi

When this hook exits with status 1, Claude Code blocks the tool use and shows the error message. Claude sees the block, understands why, and typically offers to create a feature branch instead. The hook doesn't just prevent mistakes -- it redirects Claude toward the correct workflow.

Security Scan on New Files

This is more paranoid than most people need, but if you work on anything that handles credentials, API keys, or user data, it's worth having. A PostToolUse hook that scans newly written files for common vulnerability patterns.

#!/bin/bash
# security-scan.sh -- checks written files for common issues
TOOL_NAME="$CLAUDE_TOOL_NAME"
FILE_PATH="$CLAUDE_TOOL_ARG_FILE_PATH"
 
if [[ "$TOOL_NAME" == "Write" ]]; then
  # Check for hardcoded secrets
  if grep -qE '(password|secret|api_key|token)\s*=\s*["\x27][^"\x27]+["\x27]' "$FILE_PATH" 2>/dev/null; then
    echo "WARNING: Possible hardcoded secret detected in $FILE_PATH"
  fi
 
  # Check for eval usage
  if grep -qE '\beval\b' "$FILE_PATH" 2>/dev/null; then
    echo "WARNING: eval() usage detected in $FILE_PATH"
  fi
fi

This is a PostToolUse hook, not a PreToolUse hook. It warns rather than blocks. That's a deliberate choice. Sometimes there are legitimate reasons to have what looks like a hardcoded secret in a file -- test fixtures, documentation examples, placeholder values. Blocking would be too aggressive. Warning lets me decide.

Notification on Completion

When Claude finishes a long task and you've switched to another window, you want to know. On macOS, you can use the built-in notification system. There's a community plugin called claude-notifications-go that handles this, but you can also build it yourself as a Stop hook.

#!/bin/bash
# notify-done.sh -- sends macOS notification when Claude finishes
osascript -e 'display notification "Claude Code finished its task" with title "Claude Code"'

Two lines. That's it. Every time Claude finishes a turn, you get a macOS notification. If you're on Linux, replace the osascript line with notify-send. If you want it to only trigger for long tasks, add a duration check by comparing timestamps.

This one changed how I use Claude Code more than I expected. Before the notification hook, I'd alt-tab back to the terminal every 30 seconds to check if Claude was done. Now I start a task, switch to something else, and wait for the ping. Small thing, big quality-of-life improvement.

Agent Hooks

Everything I've described so far uses shell scripts. They're fast, simple, and limited to what bash can do. Agent hooks remove that limitation.

Instead of running a shell command, an agent hook spawns a Claude subagent. The subagent has full access to Claude Code's capabilities -- it can read files, search code, analyze context, and make decisions. The parent session pauses while the subagent runs, and the subagent's output determines what happens next.

The canonical use case is security review. You want a PreToolUse hook that doesn't just pattern-match against regex but actually understands the code being changed. A shell script can check if a file contains the word "eval." A subagent can check if the proposed edit introduces a SQL injection vulnerability by analyzing the data flow from user input to database query.

Here's the concept. Your PreToolUse hook spawns a subagent with a prompt like: "Review this proposed file edit. The original file is at this path. The proposed change is in the tool arguments. Does this change introduce any security vulnerabilities? If yes, output BLOCK and explain why. If no, output ALLOW."

The subagent reads the file, understands the context, evaluates the change, and makes a judgment call. If it outputs BLOCK, the parent session's tool use is prevented. If it outputs ALLOW, execution continues.

This is powerful and expensive. Every tool use that triggers an agent hook incurs the cost of a full subagent session. For a PreToolUse hook that fires on every file edit, that adds up fast. I only recommend agent hooks for high-stakes operations -- writing to production configs, modifying authentication code, changing database schemas. For everything else, shell scripts are fine.

The other consideration is latency. A subagent hook takes seconds, not milliseconds. If your PreToolUse agent hook takes 5 seconds to evaluate every file write, and Claude needs to write 20 files, you've added nearly two minutes of overhead. That's fine for a security-critical workflow. It's unacceptable for everyday development.

Use agent hooks surgically. Target them at specific tools and specific file patterns. Don't run an agent hook on every tool use unless you have a very good reason and a very generous budget.

The Statusline

Separate from hooks but part of the same automation layer, Claude Code supports a custom statusline -- a command that runs continuously and displays information in the terminal status bar.

My statusline-command.sh shows session information: how long the session has been running, how many tool calls have been made, and current context window usage. It's configured in settings.json:

"statusline": {
  "command": "~/.claude/statusline-command.sh",
  "padding": 2
}

The script runs periodically and its output appears in the terminal's status bar area. It's a passive information display. You don't interact with it. You glance at it.

Why does this matter? Because long Claude Code sessions can drift. You start a task, Claude works on it, you prompt a few more times, and an hour later you're deep in a session with a ballooning context window and no clear sense of how much work has been done. The statusline keeps you anchored. One glance tells you the session state without interrupting the flow.

It's especially useful when running Ralph Loop or other autonomous workflows. When Claude is working unsupervised, the statusline is your window into what's happening. Is the session still active? How many iterations has it completed? Is the context window getting close to the limit? All visible without switching focus.

The implementation is just a shell script that reads session metadata and formats it as a single line. Nothing complex. The value is in having the information surface passively rather than requiring you to actively check.

Composition: Hooks Plus Skills Plus Plugins

Individual hooks are useful. The real power is composition -- hooks working alongside skills and plugins to create an automated development workflow.

Here's what my typical session looks like with all the pieces running:

  1. I start Claude Code. The SessionStart hook runs gsd-check-update.js, checking for plugin updates. Silent if everything is current. One-line notice if something needs updating.

  2. I use /brainstorm to think through a feature. This is a Superpowers skill -- it enforces structured thinking before implementation. No hooks involved here, just methodology.

  3. I tell Claude to implement the plan. As Claude writes files, the PostToolUse lint hook auto-formats each one. I never see formatting issues in review.

  4. If Claude tries to commit to main, the PreToolUse guard hook blocks it and suggests creating a branch. Claude course-corrects without my intervention.

  5. I run /review-pr for a multi-agent code review. This is a plugin that spawns parallel review agents.

  6. When Claude finishes, the Stop hook sends a notification to my desktop.

Each piece handles one concern. The hooks handle automation. The skills handle methodology. The plugins handle capabilities. They don't know about each other. They don't need to. The composition happens naturally because they all operate on the same session lifecycle.

This is the Unix philosophy applied to AI tooling. Small, focused tools that compose well. A lint hook doesn't need to know about the notification hook. The notification hook doesn't need to know about the branch guard. Each one does its job. Together, they create something more capable than any individual piece.

Gotchas

Hooks run in the shell, which means all the usual shell problems apply. But there are some specific pitfalls with Claude Code hooks that are worth calling out.

Speed matters more than you think. A PreToolUse hook runs before every tool call that matches its filter. If Claude is doing a task that involves 50 file edits and your PreToolUse hook takes 500ms, you've added 25 seconds of overhead. That doesn't sound terrible until you realize Claude's momentum is broken 50 times. Keep hooks under 1 second. Under 200ms is better. If you need complex logic, spawn it as a background process and let it run asynchronously.

A broken PreToolUse hook locks your session. If your PreToolUse hook always exits with a non-zero status -- due to a bug, a missing dependency, a bad path -- Claude can't use any tools. It can't edit files. It can't run commands. It can't do anything. The session is effectively frozen. I learned this the hard way when a path change broke my guard hook and I spent five minutes wondering why Claude kept saying it couldn't execute any actions. Test your hooks outside of Claude Code first. Run them manually with the expected environment variables.

Environment variables aren't always what you expect. The exact set of environment variables available to hooks varies by hook type and can change between Claude Code versions. Don't assume a variable exists without checking. Use defaults. Handle missing values gracefully.

Hooks don't have access to Claude's context. A hook can see what tool is being called and what arguments are being passed. It cannot see Claude's reasoning, the conversation history, or the current plan. If you need that level of awareness, you need an agent hook, not a shell hook. Don't try to reconstruct Claude's intent from tool arguments alone -- you'll get it wrong.

Timeouts are your friend. Always set a timeout on your hooks. A hook that hangs -- waiting for a network request, stuck on a file lock, blocked on user input -- will hang Claude Code. The timeout config option exists for a reason. Use it. 5000ms is a reasonable default. Lower is better.

Don't write hooks that modify Claude's output. Hooks should affect the environment, not the conversation. A PostToolUse hook that silently modifies a file Claude just wrote will confuse Claude in subsequent turns, because what Claude thinks the file contains and what the file actually contains won't match. If you need to modify output, do it through the conversation -- tell Claude to reformat, not a hook.

Closing

Hooks are the difference between a tool you use and a system you've built.

Every developer who gets serious about Claude Code eventually hits the same wall. The interactive loop is great but there are things you want to happen automatically. Checks you always want to run. Guards you always want in place. Notifications you always want to receive. You can remember to do these things manually, or you can encode them in hooks and never think about them again.

The automation layer -- hooks, statuslines, agent hooks -- is the most under-documented feature of Claude Code. It's not flashy. It doesn't make for good demos. But it's the feature that power users reach for when they want to make Claude Code truly their own. It's the customization that persists across every session, every project, every task. Your CLAUDE.md tells Claude how to behave. Your hooks tell the system how to operate.

Start small. One SessionStart hook that checks something useful. One PostToolUse hook that catches a common mistake. One Stop hook that sends a notification. Live with those for a week. Then add more as you notice patterns in your workflow that could be automated.

The best hook is the one that handles something you've already forgotten you used to do manually.