What's Actually Happening
Git hooks are scripts that run automatically before or after git commands like commit, push, or receive. When a hook script lacks execute permissions, has incorrect ownership, or encounters other permission issues, git cannot run it and the operation fails.
The Error You'll See
During commit:
error: cannot run .git/hooks/pre-commit: Permission deniedDuring push:
remote: error: cannot run hooks/pre-receive: Permission denied
remote: error: hook declined to update refs/heads/main
To github.com:user/repo.git
! [remote rejected] main -> main (hook declined)During receive (on server):
error: cannot run hooks/post-receive: Permission deniedOr more generically:
fatal: cannot exec '.git/hooks/pre-commit': Permission deniedWhy This Happens
Git hook permission errors occur when:
- The hook script lacks execute (+x) permission
- The script has incorrect owner or group
- The script has Windows line endings (CRLF) instead of Unix (LF)
- The shebang line (#!/bin/bash) is incorrect or missing
- File system permissions prevent execution
- SELinux or AppArmor blocks execution
- The .git/hooks directory has wrong permissions
Step 1: Check Hook File Permissions
List the hooks and their permissions:
ls -la .git/hooks/You'll see something like:
-rw-r--r-- 1 user group 478 Apr 1 10:00 pre-commit.sample
-rwxr-xr-x 1 user group 478 Apr 1 10:00 pre-commitThe active hook (pre-commit) should have x (execute) permission.
Step 2: Add Execute Permission
Make the hook executable:
chmod +x .git/hooks/pre-commitFor all hooks:
chmod +x .git/hooks/*Or for specific hooks:
chmod +x .git/hooks/pre-commit
chmod +x .git/hooks/pre-push
chmod +x .git/hooks/commit-msgStep 3: Fix Ownership Issues
If the hook has wrong owner:
ls -la .git/hooks/pre-commitChange ownership:
chown user:group .git/hooks/pre-commitOr for all hooks:
chown -R user:group .git/hooks/Step 4: Fix Line Endings
Windows line endings cause "bad interpreter" errors:
file .git/hooks/pre-commitOutput shows:
.git/hooks/pre-commit: Bourne-Again shell script, ASCII text executable, with CRLF line terminatorsFix line endings:
sed -i 's/\r$//' .git/hooks/pre-commitOr using dos2unix:
dos2unix .git/hooks/pre-commitStep 5: Verify Shebang Line
Check the first line of the script:
head -1 .git/hooks/pre-commitShould be:
#!/bin/shOr for bash:
#!/bin/bashEnsure the interpreter exists:
which bash
which shIf using a different language:
#!/usr/bin/env python3
#!/usr/bin/env nodeStep 6: Check Script Syntax
Verify the script has no syntax errors:
bash -n .git/hooks/pre-commitFor Python hooks:
python3 -m py_compile .git/hooks/pre-commitStep 7: Test the Hook Manually
Run the hook directly to see errors:
./.git/hooks/pre-commitThis reveals the actual error without git's abstraction.
Step 8: Fix Server-Side Hooks (Bare Repositories)
For bare repositories (servers):
ls -la /path/to/repo.git/hooks/
chmod +x /path/to/repo.git/hooks/pre-receive
chmod +x /path/to/repo.git/hooks/post-receiveEnsure the git user can execute:
sudo -u git ls -la /path/to/repo.git/hooks/
sudo -u git /path/to/repo.git/hooks/pre-receiveStep 9: Handle SELinux/AppArmor Issues
On systems with SELinux:
```bash # Check SELinux context ls -Z .git/hooks/pre-commit
# Fix context restorecon -v .git/hooks/pre-commit ```
On systems with AppArmor:
# Check if blocked
dmesg | grep -i apparmor | grep pre-commitStep 10: Debug Hook Execution
Enable shell debugging:
# Add to the top of the hook script
set -xOr run with debug:
bash -x ./.git/hooks/pre-commitVerify the Fix
Test the commit process:
git commit --allow-empty -m "Test commit"Should run the pre-commit hook without permission errors.
Check hook is properly installed:
ls -la .git/hooks/pre-commitShould show execute permission:
-rwxr-xr-x 1 user group 478 Apr 1 10:00 .git/hooks/pre-commitFor pre-push:
git push origin main --dry-runThe --dry-run flag executes hooks without actually pushing.
For server-side hooks, make a test push:
git commit --allow-empty -m "Test hook"
git push origin mainShould execute without errors on both client and server.