Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

PostToolUse Hook for Code Formatting and Linting

Hello again!

In our last lesson, we built a crucial safety feature: a PreToolUse hook that acts as a gatekeeper, deterministically blocking Claude from modifying protected files. We learned that these hooks fire before a tool runs, giving us ultimate control over what the agent is allowed to do.

Today, we'll explore the other side of this automation coin. Instead of preventing actions, we'll react to them. Our goal is to create a PostToolUse hook that automatically formats or lints code after file edits, using your project's tooling. This moves us from defense to proactive quality control, ensuring that any code touched by Claude instantly conforms to your project's standards.

1. From Gatekeeper to Housekeeper: The Role of PostToolUse

While PreToolUse hooks are gatekeepers, PostToolUse hooks are more like diligent housekeepers or validators. They spring into action after a tool has successfully completed its job.

This video provides an excellent, concise example of this concept.

Why Most Developers Are Using Claude Code Wrong (Here's What You're Missing)!

To see the core idea of a PostToolUse hook in action, watch this brief clip from the 'Why Most Developers Are Using Claude Code Wrong' video by DIY Smart Code.

Watch from 08:09 to 08:22. The narrator presents the exact use case we're building today: a PostToolUse hook that runs a code formatter every time Claude saves a file.

As the video suggests, the primary purpose of a PostToolUse hook is to perform actions like:

  • Logging: Recording that a specific tool was used with certain parameters.
  • Validation: Checking the output or result of a tool.
  • Post-processing: Cleaning up, formatting, or transforming the results of a tool's action.

To do this effectively, the hook receives a richer payload than a PreToolUse hook, including the tool's response.

disler/claude-code-hooks-mastery - GitHub

To understand the data our hook will receive, let's look at the 'claude-code-hooks-mastery' GitHub repository. It clearly defines the payload for this hook type.

Find and read the short section titled '3. PostToolUse Hook'. Note that its payload includes not just tool_name and tool_input, but also tool_response.

This tool_response is what allows us to validate the outcome of an operation, but remember: the action has already happened. A PostToolUse hook can't prevent a file from being written, but it can report if the written content is invalid and ask Claude to fix it.

2. The Realities of Implementation

In theory, setting up an auto-formatter sounds simple. In practice, as with many real-world engineering tasks, there are several nuances to consider. A naive implementation could cause more problems than it solves.

This excellent Reddit post discusses the exact challenges you'd face when building this for a professional project.

Using Claude Code Hooks for File-Specific Type Checks in ... - Reddit

Read the 'Problems' section of this Reddit post to see the practical hurdles you'd encounter when setting up a file-based hook.

Focus on the '2. Problems' section. The author outlines key issues: the hook triggers on all file types (not just Python), and running tools on the entire codebase is inefficient.

Based on that post, our script must be smart enough to:

  1. Filter by tool: Only run on Edit or Write operations.
  2. Filter by file type: Only format Python files (e.g., those ending in .py).
  3. Operate efficiently: Format only the specific file that was just modified, not the entire project.

Let's build a hook that correctly handles all these points.

3. Building the ruff Auto-Formatter Hook

We will create a PostToolUse hook that automatically runs ruff format on any Python file that Claude edits. Given your background, ruff is an ideal choice as a modern, high-performance tool for Python projects.

Prerequisites:
Make sure you have ruff installed in your project's environment. If not, you can add it with:
pip install ruff

Step A: The Formatter Script (format-python.sh)

Create a .claude/hooks directory in your project if it doesn't exist. Inside, create a new file named format-python.sh. This script will contain the logic to intelligently apply formatting.




#!/bin/bash
# .claude/hooks/format-python.sh




# Read the JSON input from stdin
INPUT=$(cat)




# Extract tool name and file path.
# We use `// empty` in jq to prevent errors if file_path is not present.
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')




# --- Main Logic ---
# 1. Check if the tool was Edit or Write.
# 2. Check if the file path is not empty and ends with .py.
if [[ ("$TOOL_NAME" = "Edit" || "$TOOL_NAME" = "Write") && "$FILE_PATH" == *.py ]]; then



    # 3. If both are true, run ruff format on the specific file.
    # The --silent flag suppresses non-error output from ruff.
    ruff format "$FILE_PATH" --silent
fi




# Exit with 0 to indicate success. This hook doesn't need to block or
# send feedback to Claude; it just does its job quietly.
exit 0

Make the script executable: chmod +x .claude/hooks/format-python.sh

This script is a robust solution that directly addresses the challenges we identified. It's targeted, efficient, and file-type aware.

Step B: The Configuration (settings.json)

Now, let's wire up the hook. Open your project's .claude/settings.json file and add the PostToolUse configuration.

{
  // Your other settings, like PreToolUse hooks, may be here.
  "hooks": {
    "PreToolUse": [
      // Your file protection hook from the previous lesson
      {
        "matcher": "Edit|Write",
        "handler": {
          "type": "command",
          "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/protect-files.sh"
        }
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "handler": {
          "type": "command",
          "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/format-python.sh"
        }
      }
    ]
  }
}

Note: We again use a matcher for Edit|Write. This is an optimization that prevents our script from even being called for irrelevant tool uses like Bash or ReadFile.

Step C: Testing the Hook

  1. Create a new Python file in your project, e.g., test_formatting.py, and add some deliberately messy code:
    def my_func(  arg1,
      arg2   ):
     print("This is poorly formatted")
     return arg1+arg2
    
  2. Restart your Claude Code session to load the new settings.
  3. Give Claude a simple prompt to modify this file: In test_formatting.py, add a comment at the top that says '# My test function'.
  4. Claude will use the Edit tool to add the comment. Immediately after, you should see output in your terminal indicating the PostToolUse hook is running.
  5. Now, open test_formatting.py. You will see that not only has the comment been added, but the entire file has been automatically formatted by ruff:
    
    
    
    # My test function
    def my_func(arg1, arg2):
        print("This is poorly formatted")
        return arg1 + arg2
    

You've successfully created an automated code quality workflow!

4. Next Level: From Formatting to Linting with Feedback

Formatting is a "fire and forget" operation. A more advanced use is linting, where you check for errors and report them back to Claude if they exist.

You could adapt our script to run ruff check "$FILE_PATH" instead of ruff format.

  • If ruff check passes, the script exits with 0.
  • If ruff check fails, it exits with a non-zero code. Your script could capture the error output from ruff, print it to stderr, and exit 2.

This would provide direct feedback to Claude, which might respond with, "I see my last edit introduced a linting error. I will now fix it." This creates a powerful self-correction loop. The claude-code-hooks-mastery repository you reviewed earlier has excellent examples of this pattern in its validators/ directory.

Conclusion

You've now implemented hooks that run both before and after tool execution, giving you comprehensive control over the agent's actions. PostToolUse hooks are essential for integrating your existing development toolchain (formatters, linters, type-checkers) directly into your AI-assisted workflow.

Key Takeaways:

  • PostToolUse hooks execute after a tool succeeds, making them ideal for validation, logging, and post-processing.
  • A robust hook script must filter by tool_name and file type to ensure it only runs when relevant.
  • For simple formatting, the hook can run silently and exit 0.
  • For validation and linting, a hook can exit 2 and write to stderr to provide corrective feedback to Claude, creating a self-healing workflow.

Next Up:
We've explored declarative automation with hooks that react to events. In the next lesson, we'll shift to imperative automation by designing a powerful, multi-step custom command: /test-and-fix. This will combine shell execution, parsing output, and prompting Claude to create a complete, automated debugging cycle.

Can't find a good explanation? Sign up and we'll make it for you

Sign up