Tickd.ai
← The Tickd Guide

Tutorials & Guides

How to Build a Self-Correcting Conventional Commit Linter Using Claude and Git Hooks

Tired of sloppy, chaotic git histories? Build a local Python Git hook that intercepts lazy commit messages, evaluates them against the Conventional Commits specification, and uses Claude 3.5 Sonnet to instantly refactor them on the fly.

Updated 10/2/2026

We have all done it. You have been debugging a race condition for three hours, you finally find the missing await, and in a fit of exhausted triumph, you run git commit -m "fixed the stupid bug".

While that feels cathartic in the moment, it is a nightmare for your team and a disaster for automated changelog generators. A clean, standardised git history is the backbone of healthy software engineering. But forcing humans to strictly adhere to the Conventional Commits specification manually is a battle you will lose. Linters usually just reject the commit, forcing the developer to retype it in frustration.

Let us build a better developer experience. In this tutorial, we will write a local, self-correcting Git hook using Python and Claude 3.5 Sonnet. Instead of blocking the developer with a dry error message, our hook will intercept the commit message, validate it, and—if it fails—quietly ask Claude to refactor it into a perfect conventional commit, keeping the developer's original intent intact.

It is time to make sure your git history ticks all the right boxes without slowing down your shipping velocity.

The Architecture of a Self-Correcting Hook

Git hooks are scripts that run automatically before or after specific git events. We will use the commit-msg hook, which runs after you write your commit message but before the commit is actually created.

Our hook will follow this workflow: 1. Git triggers the commit-msg hook, passing the path to the temporary commit message file. 2. Our Python script reads the developer's drafted message. 3. A local regex check evaluates whether the message already matches the Conventional Commits standard. 4. If it matches, the commit proceeds untouched (saving API latency and cost). 5. If it fails, the script grabs the messy message, passes it to Claude via the Anthropic API, and asks for an immediate refactoring based on strict rules. 6. The script overwrites the commit message file with Claude's polished version and allows the commit to complete.

Step 1: Writing the Local Regex Validator

To prevent unnecessary API calls, we need a fast, local validator. The Conventional Commits standard follows a structured format:

` <type>(<scope>): <description>

[optional body]

[optional footer(s)] `

Let us create a python module to handle this logic. Create a directory for your global or repository-specific tools and name this script commit_linter.py:

`python import re import sys

A standard regex pattern for conventional commits CONVENTIONAL_REGEX = re.compile( r"^(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)" r"(?:\([a-zA-Z0-9_-]+\))?!?: " r".{1,100}$" )

def is_conventional(message: str) -> bool: # Strip out comments and whitespace lines = [line for line in message.splitlines() if not line.strip().startswith('#')] if not lines: return False first_line = lines[0].strip() return bool(CONVENTIONAL_REGEX.match(first_line)) `

Step 2: Crafting the Refactoring Prompt

If the local check fails, we route the draft to Claude. We need a system prompt that is incredibly strict. We do not want Claude adding conversational pleasantries, markdown blocks, or inventing details that were not in the developer's original message.

You can experiment with different variations of this prompt in our dedicated /prompts playground, but for our Python script, we will hardcode a highly optimised version.

`python SYSTEM_PROMPT = """ You are a strict git hook helper. Your task is to rewrite messy, non-conventional git commit messages into the strict Conventional Commits format.

Rules: 1. Use one of these types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert. 2. Do NOT invent information. If the input is too brief (e.g., "fixed centering"), use your best guess for the type (e.g., "style: fix centering issues"). 3. Do NOT wrap your response in markdown code blocks. Return ONLY the raw commit message. 4. Maintain the developer's original intent. 5. If the user provided a body in their message, preserve it but format it cleanly. """ `

Step 3: Integrating Claude via Python

Now, let us build the integration using the Anthropic API client. Ensure you have the library installed via pip install anthropic. If you hit configuration snags here, you can consult our dedicated debugging resource at /platforms/claude/articles to resolve environment variables or dependency conflicts.

Here is how we integrate the API call into our commit_linter.py:

`python import os from anthropic import Anthropic

def get_claude_correction(messy_message: str) -> str: api_key = os.getenv("ANTHROPIC_API_KEY") if not api_key: # Fallback gracefully to the raw message if no API key is set print("Warning: ANTHROPIC_API_KEY not found. Committing messy message.") return messy_message

client = Anthropic(api_key=api_key) try: response = client.messages.create( model="claude-3-5-sonnet-20241022", max_tokens=150, temperature=0.1, system=SYSTEM_PROMPT, messages=[ { "role": "user", "content": f"Rewrite this commit message: '{messy_message}'" } ] ) return response.content[0].text.strip() except Exception as e: print(f"Warning: Claude correction failed ({e}). Falling back to original message.") return messy_message `

Step 4: Connecting the Git Hook

To make this run automatically on every commit, we need to tie this script to Git's commit-msg lifecycle event.

  1. In your git repository, navigate to .git/hooks/.
  2. Create a file named commit-msg (no extension).
  3. Make the file executable by running chmod +x .git/hooks/commit-msg in your terminal.

Paste the following bash wrapper into .git/hooks/commit-msg. This script passes the commit message path to our Python script:

`bash #!/usr/bin/env bash

Ensure our virtualenv or python environment is loaded if needed # export ANTHROPIC_API_KEY="your_key_here"

python3 path/to/your/commit_linter.py "$1" `

Now, complete the Python file (commit_linter.py) by adding the execution entry point that handles the file reading and writing:

`python if __name__ == "__main__": if len(sys.argv) < 2: sys.exit(0)

commit_msg_filepath = sys.argv[1]

with open(commit_msg_filepath, "r") as f: original_message = f.read()

Only touch the message if it does not already conform if not is_conventional(original_message): print("Checking commit message format... [NON-CONVENTIONAL]") corrected_message = get_claude_correction(original_message) print(f"Original: \"{original_message.strip()}\"") print(f"Corrected: \"{corrected_message}\"") with open(commit_msg_filepath, "w") as f: f.write(corrected_message) else: print("Checking commit message format... [OK]") ```

Testing the Loop

Let us take it for a spin. Run a commit with a sloppy message:

`bash git commit -m "swapped the user dropdown sorting logic so recent items show first" `

If you watch your terminal, you will see the script hook into action. Within a couple of seconds, Claude evaluates your draft and rewrites it. Your git log will show:

` refactor: sort user dropdown by recent items first `

By leveraging Claude 3.5 Sonnet directly in your local dev loop, you remove the mental tax of syntax parsing. Your codebase benefits from perfectly structured, changelog-ready commits, and you get to keep typing exactly how you think.

claudegitautomationpythondeveloper-workflow

Keep going

Build something with the prompt generator, decode the jargon in the glossary, or compare the tools on our platform deep-dives.