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:

bash
error: cannot run .git/hooks/pre-commit: Permission denied

During push:

bash
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):

bash
error: cannot run hooks/post-receive: Permission denied

Or more generically:

bash
fatal: cannot exec '.git/hooks/pre-commit': Permission denied

Why 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:

bash
ls -la .git/hooks/

You'll see something like:

bash
-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-commit

The active hook (pre-commit) should have x (execute) permission.

Step 2: Add Execute Permission

Make the hook executable:

bash
chmod +x .git/hooks/pre-commit

For all hooks:

bash
chmod +x .git/hooks/*

Or for specific hooks:

bash
chmod +x .git/hooks/pre-commit
chmod +x .git/hooks/pre-push
chmod +x .git/hooks/commit-msg

Step 3: Fix Ownership Issues

If the hook has wrong owner:

bash
ls -la .git/hooks/pre-commit

Change ownership:

bash
chown user:group .git/hooks/pre-commit

Or for all hooks:

bash
chown -R user:group .git/hooks/

Step 4: Fix Line Endings

Windows line endings cause "bad interpreter" errors:

bash
file .git/hooks/pre-commit

Output shows:

bash
.git/hooks/pre-commit: Bourne-Again shell script, ASCII text executable, with CRLF line terminators

Fix line endings:

bash
sed -i 's/\r$//' .git/hooks/pre-commit

Or using dos2unix:

bash
dos2unix .git/hooks/pre-commit

Step 5: Verify Shebang Line

Check the first line of the script:

bash
head -1 .git/hooks/pre-commit

Should be:

bash
#!/bin/sh

Or for bash:

bash
#!/bin/bash

Ensure the interpreter exists:

bash
which bash
which sh

If using a different language:

bash
#!/usr/bin/env python3
#!/usr/bin/env node

Step 6: Check Script Syntax

Verify the script has no syntax errors:

bash
bash -n .git/hooks/pre-commit

For Python hooks:

bash
python3 -m py_compile .git/hooks/pre-commit

Step 7: Test the Hook Manually

Run the hook directly to see errors:

bash
./.git/hooks/pre-commit

This reveals the actual error without git's abstraction.

Step 8: Fix Server-Side Hooks (Bare Repositories)

For bare repositories (servers):

bash
ls -la /path/to/repo.git/hooks/
chmod +x /path/to/repo.git/hooks/pre-receive
chmod +x /path/to/repo.git/hooks/post-receive

Ensure the git user can execute:

bash
sudo -u git ls -la /path/to/repo.git/hooks/
sudo -u git /path/to/repo.git/hooks/pre-receive

Step 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:

bash
# Check if blocked
dmesg | grep -i apparmor | grep pre-commit

Step 10: Debug Hook Execution

Enable shell debugging:

bash
# Add to the top of the hook script
set -x

Or run with debug:

bash
bash -x ./.git/hooks/pre-commit

Verify the Fix

Test the commit process:

bash
git commit --allow-empty -m "Test commit"

Should run the pre-commit hook without permission errors.

Check hook is properly installed:

bash
ls -la .git/hooks/pre-commit

Should show execute permission:

bash
-rwxr-xr-x 1 user group 478 Apr  1 10:00 .git/hooks/pre-commit

For pre-push:

bash
git push origin main --dry-run

The --dry-run flag executes hooks without actually pushing.

For server-side hooks, make a test push:

bash
git commit --allow-empty -m "Test hook"
git push origin main

Should execute without errors on both client and server.