Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions claude-code-hooks/.claude/hooks/format_with_ruff.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import json
import subprocess
import sys
from pathlib import Path


def main():
event = json.load(sys.stdin)
if event.get("tool_name") not in {"Write", "Edit"}:
return 0
file_path = event.get("tool_input", {}).get("file_path", "")
if not file_path.endswith(".py") or not Path(file_path).exists():
return 0
subprocess.run(["ruff", "check", "--fix", file_path], check=False)
subprocess.run(["ruff", "format", file_path], check=False)
return 0


if __name__ == "__main__":
sys.exit(main())
17 changes: 17 additions & 0 deletions claude-code-hooks/.claude/hooks/guard_pip.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import json
import sys


def main():
event = json.load(sys.stdin)
if event.get("tool_name") != "Bash":
return 0
command = event.get("tool_input", {}).get("command", "")
if "pip install" in command or "pip3 install" in command:
print("Use 'uv add' instead of pip install.", file=sys.stderr)
return 2
return 0


if __name__ == "__main__":
sys.exit(main())
31 changes: 31 additions & 0 deletions claude-code-hooks/.claude/hooks/notify_desktop.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
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)
elif system == "Windows":
toast = f"New-BurntToastNotification -Text '{title}', '{message}'"
subprocess.run(
["powershell", "-NoProfile", "-Command", toast], check=False
)


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


if __name__ == "__main__":
sys.exit(main())
13 changes: 13 additions & 0 deletions claude-code-hooks/.claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"hooks": {
"Stop": [
{ "hooks": [ { "type": "command", "command": "python .claude/hooks/notify_desktop.py" } ] }
],
"PreToolUse": [
{ "matcher": "Bash", "hooks": [ { "type": "command", "command": "python .claude/hooks/guard_pip.py" } ] }
],
"PostToolUse": [
{ "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "python .claude/hooks/format_with_ruff.py" } ] }
]
}
}
10 changes: 10 additions & 0 deletions claude-code-hooks/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Python-generated files
__pycache__/
*.py[oc]
build/
dist/
wheels/
*.egg-info

# Virtual environments
.venv
47 changes: 47 additions & 0 deletions claude-code-hooks/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Claude Code Hooks Demo

Supporting materials for the Real Python tutorial **How to Automate Your Workflow With Claude Code Hooks**.

This is a small [uv](https://docs.astral.sh/uv/)-managed project with three Claude Code hooks wired up in `.claude/`:

- **`Stop` hook** (`notify_desktop.py`): a cross-platform desktop notification each time Claude finishes responding.
- **`PreToolUse` hook** (`guard_pip.py`): blocks `pip install` and steers Claude toward `uv`.
- **`PostToolUse` hook** (`format_with_ruff.py`): formats the file Claude just edited with ruff.

Each hook is a small Python script in `.claude/hooks/`, wired up in `settings.json`.

## Setup

You need Python 3.12 or later, uv, and ruff. Install ruff as a uv tool so the hook can call it from anywhere:

```console
$ uv tool install ruff
```

Then sync the project:

```console
$ uv sync
```

On Windows, the notification hook shows a toast through PowerShell's BurntToast module. Install it once:

```console
> Install-Module -Name BurntToast -Scope CurrentUser
```

## Try the hooks

Start Claude Code in this folder:

```console
$ claude
```

Then:

- Ask Claude to do anything, and you get a desktop notification when it finishes.
- Ask Claude to `pip install requests`. The hook blocks pip, and Claude uses `uv add` instead.
- Ask Claude to edit a Python file. ruff reformats the file on save.

Run `/hooks` inside Claude Code to list the wired hooks, or start with `claude --debug` to watch them fire.
6 changes: 6 additions & 0 deletions claude-code-hooks/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
def main():
print("Hello from hooks-demo!")


if __name__ == "__main__":
main()
7 changes: 7 additions & 0 deletions claude-code-hooks/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[project]
name = "claude-hooks-demo"
version = "0.1.0"
description = "Sample project for the Real Python tutorial on Claude Code hooks."
readme = "README.md"
requires-python = ">=3.12"
dependencies = []
Loading