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
|
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.