A woman leans against a factory conveyor belt carrying code files and folders, while three overhead hooks lower onto the line beside a machine panel showing the Python logo.

How to Automate Your Workflow With Claude Code Hooks

When you pair with Claude Code, some steps shouldn’t be left to chance, and Claude Code hooks are how you lock them in. You want a heads-up when it finishes a long task, a guarantee that it sticks to your project’s approved tools, and a promise that every edit is formatted to your code standards, without repeating yourself each time. Asking the model to remember all of that works until it doesn’t.

Hooks make these actions deterministic: they fire on the event you choose, every time. A hook is nothing more than a command you register to run when a specific event happens. It doesn’t wait for the model to choose to act, which is what makes it reliable.

In this tutorial, you’ll set up these hooks in a project of your own. Here’s the auto-formatter at work, cleaning up a file the moment Claude edits it:

Claude Code Hooks: a PostToolUse Hook Reformatting a Python File With ruff on Save
A PostToolUse Hook Formatting Claude's Edit With Ruff

As you build them, you’ll learn the three parts a hook is made of and the exit-code contract that lets a hook block or redirect what the model is about to do.

Prerequisites

To follow along, you should be comfortable with intermediate Python, the terminal, and reading a bit of JSON. You should also have spent some time in Claude Code already.

If it’s new to you, then Real Python’s How to Use Claude Code to Write and Debug Python tutorial walks through the basics, and the Getting Started With Claude Code video course covers the same ground on screen.

You’ll need a few tools on your system:

  • Claude Code 2.1.187 or later
  • Python 3.12 or later
  • uv 0.11.6 or later
  • Ruff 0.15.20 or later

If you don’t have uv yet, then Real Python’s guide on installing uv walks you through it. You’ll install Ruff later, when you build the formatting hook.

Step 1: Ping Yourself After Every Claude Response

Start with a small uv-managed project, the kind you’d actually work in. Spin one up with uv:

Language: Shell
$ uv init --no-package hooks-demo
Initialized project `hooks-demo` at `/home/you/hooks-demo`
$ cd hooks-demo
$ uv sync
...

That gives you a pyproject.toml, a starter main.py, and a few dotfiles to work in. The uv sync step creates the project’s virtual environment up front, so the hooks can run cleanly from the start.

Now for your first improvement. When you hand Claude a long task and look away, you might miss the moment Claude wraps up and lose a few minutes before you circle back. A hook on the Stop event closes that gap, since Stop fires every time Claude finishes a response. That makes it the right place to send yourself a desktop notification.

Notifications look different on each operating system, so rather than hard-code one command, you’ll point the hook at a small Python script that works everywhere. Create a .claude/hooks/ folder at the root of your project, then save this as .claude/hooks/notify_desktop.py:

Language: Python Filename: .claude/hooks/notify_desktop.py
import json
import platform
import subprocess
import sys


def notify(title, message):
    system = platform.system()
    if system == "Darwin":
        script = f'display notification "{message}" with title "{title}"'
        subprocess.run(["osascript", "-e", script], check=False)
    elif system == "Linux":
        subprocess.run(["notify-send", title, message], check=False)
    else:
        return False
    return True


def main():
    json.load(sys.stdin)
    try:
        notified = notify("Claude Code", "Claude just finished responding")
    except FileNotFoundError:
        notified = False
    if not notified:
        print("Claude just finished responding")
    return 0


if __name__ == "__main__":
    sys.exit(main())

The script reads the event that Claude Code sends on standard input, then picks the right notifier: osascript on macOS and notify-send on Linux. On Windows and other systems, or if the notifier isn’t installed, the hook falls back to a plain-text message so it never interrupts your session. You could also wire up a native Windows toast with a PowerShell module like BurntToast.

Project-level hooks live in .claude/settings.json, and every hook is made of three parts:

  • Event: The moment the hook runs, like Stop
  • Matcher: An optional filter that limits the hook to certain tools
  • Command: The shell command to run

Locked learning resources

Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Article

Already a member? Sign-In

Locked learning resources

The full article is for members only. Join us and get access to thousands of tutorials and a community of expert Pythonistas.

Unlock This Article

Already a member? Sign-In

About Vivek Sahu

Vivek is a software engineer from a small town in central India, with 10+ years in the field. He fell for Python in college and likes to teach and share what he learns. Off the clock, he tinkers with side projects and takes on freelance work.

» More about Vivek

Each tutorial at Real Python is created by a team of developers so that it meets our high quality standards. The team members who worked on this tutorial are:

What Do You Think?

What’s your #1 takeaway or favorite thing you learned? How are you going to put your newfound skills to use? Leave a comment below and let us know.

Commenting Tips: The most useful comments are those written with the goal of learning from or helping out other students. Get tips for asking good questions and get answers to common questions in our support portal.


Looking for a real-time conversation? Visit the Real Python Community Chat or join the next “Office Hours” Live Q&A Session. Happy Pythoning!

Become a Member to join the conversation.

Keep Learning

Related Topics: intermediate projects tools