Dear future contributor who just got their commit rejected: yes, the hook is working as intended.

Git hooks are your silent guardians, enforcing standards before bad commits make it into history. The commit-msg hook runs after you write your commit message but before the commit is created - perfect for validation.

The Problem

AI assistants (including Claude Code) sometimes add co-authorship lines to commits. While well-intentioned, you might want commits to reflect your voice only, without AI attribution. Standard git hooks can enforce this.

The Solution: A Custom commit-msg Hook

Hook Script

Store your hook template in .githooks/commit-msg:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
#!/bin/bash
# Git hook to prevent commits with AI co-authorship lines

commit_msg_file="$1"

# Check if commit message contains Claude attribution
if grep -qi "Co-Authored-By:.*Claude" "$commit_msg_file"; then
    echo "❌ ERROR: Commit message contains AI co-authorship line"
    echo ""
    echo "Found: $(grep -i "Co-Authored-By:.*Claude" "$commit_msg_file")"
    echo ""
    echo "Please remove 'Co-Authored-By: Claude' lines from your commit message."
    echo "See .claude/instructions.md for project commit guidelines."
    exit 1
fi

exit 0

How it works:

  • Git passes the commit message file path as $1
  • grep -qi searches case-insensitively for the pattern
  • Exit code 1 blocks the commit and shows error message
  • Exit code 0 allows the commit to proceed

Installation Script

Make it easy for team members with scripts/install-hooks.sh:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/bin/bash
# Install git hooks from .githooks directory

HOOKS_DIR=".githooks"
GIT_HOOKS_DIR=".git/hooks"

echo "Installing git hooks..."

# Check if .git directory exists
if [ ! -d ".git" ]; then
    echo "❌ Error: Not a git repository. Run this from the project root."
    exit 1
fi

# Copy hooks
for hook in "$HOOKS_DIR"/*; do
    if [ -f "$hook" ]; then
        hook_name=$(basename "$hook")
        cp "$hook" "$GIT_HOOKS_DIR/$hook_name"
        chmod +x "$GIT_HOOKS_DIR/$hook_name"
        echo "✓ Installed $hook_name"
    fi
done

echo ""
echo "✅ Git hooks installed successfully!"
echo "Hooks will now prevent commits with 'Co-Authored-By: Claude' lines."

Usage:

1
2
3
4
5
# Make the installer executable
chmod +x scripts/install-hooks.sh

# Run the installer
./scripts/install-hooks.sh

Why .githooks/ Instead of .git/hooks/?

The .git/ directory is local and not tracked by version control. Storing hooks in .githooks/ means:

  • ✅ Hooks are version-controlled
  • ✅ New contributors get them via clone
  • ✅ Updates propagate to the team
  • ✅ Consistent standards across all environments

The installation script copies hooks from .githooks/ to .git/hooks/ where git actually executes them.

Testing the Hook

Try to commit with forbidden pattern:

1
2
3
git commit -m "Add new feature

Co-Authored-By: Claude Sonnet 4.5 <[email protected]>"

Result:

1
2
3
4
5
6
❌ ERROR: Commit message contains AI co-authorship line

Found: Co-Authored-By: Claude Sonnet 4.5 <[email protected]>

Please remove 'Co-Authored-By: Claude' lines from your commit message.
See .claude/instructions.md for project commit guidelines.

Commit blocked.

Clean commit message:

1
2
3
4
git commit -m "Add new feature

Implemented user authentication with JWT tokens.
Updated API endpoints to require authorization headers."

Result: Commit succeeds. 🎉

Other Useful Patterns to Block

Block WIP commits

1
2
3
4
if grep -qi "^WIP" "$commit_msg_file"; then
    echo "❌ ERROR: WIP commits not allowed on main branch"
    exit 1
fi

Enforce conventional commits

1
2
3
4
5
if ! grep -qE "^(feat|fix|docs|style|refactor|test|chore):" "$commit_msg_file"; then
    echo "❌ ERROR: Commit must follow conventional commits format"
    echo "Examples: feat:, fix:, docs:, refactor:"
    exit 1
fi

Block sensitive information

1
2
3
4
if grep -qiE "(password|secret|api[_-]?key)" "$commit_msg_file"; then
    echo "❌ ERROR: Commit message may contain sensitive information"
    exit 1
fi

Project Structure

1
2
3
4
5
6
7
8
.
├── .githooks/
│   └── commit-msg          # Hook template (version controlled)
├── .git/
│   └── hooks/
│       └── commit-msg      # Active hook (copied from .githooks/)
└── scripts/
    └── install-hooks.sh    # Installation automation

Updating Hooks

When you modify hooks in .githooks/:

1
2
3
4
5
6
# Re-run the installer to update .git/hooks/
./scripts/install-hooks.sh

# Or manually copy
cp .githooks/commit-msg .git/hooks/commit-msg
chmod +x .git/hooks/commit-msg

Troubleshooting

Hook not running?

1
2
3
4
5
6
7
# Check if hook is executable
ls -l .git/hooks/commit-msg

# Should show: -rwxr-xr-x (executable permission)

# If not, make it executable
chmod +x .git/hooks/commit-msg

Need to bypass the hook temporarily?

1
2
# Use --no-verify flag (use sparingly!)
git commit --no-verify -m "Emergency hotfix"

Hook runs but doesn’t block commits?

Check the exit code:

1
2
3
# Test the hook manually
bash .git/hooks/commit-msg .git/COMMIT_EDITMSG
echo $?  # Should be 1 if pattern matched, 0 if clean

Beyond commit-msg: Other Useful Hooks

  • pre-commit: Run linters, tests, or formatting before commit
  • pre-push: Block pushes to protected branches
  • post-checkout: Auto-install dependencies after branch switch
  • post-merge: Notify team of merge completion

Pro tip: Store all your hooks in .githooks/ and use the installation script pattern. This ensures every team member runs the same validation rules. For complex validation (like running tests or linters), consider using tools like pre-commit framework which offers a plugin ecosystem and better performance. But for simple text pattern matching like blocking AI attribution, a plain bash script is perfect - no dependencies, instant execution, clear error messages.


More from me on www.uk4.in.