Git & CI/CD Advanced

Resolving Git pre-commit Hook Failed Execution on Alpine Linux

Troubleshoot Git pre-commit hook failures on Alpine Linux, often due to shell incompatibilities or missing dependencies. Fix your Bash hooks today!

👨‍💻
Senior Systems Architect • Verified in Staging Labs

Troubleshoot Git pre-commit hook failures on Alpine Linux, often due to shell incompatibilities or missing dependencies. Fix your Bash hooks today!

When working in a Git repository hosted or accessed from an Alpine Linux environment, you might encounter issues with pre-commit hooks failing to execute correctly. This is particularly common when these hooks are written for Bash (#!/bin/bash) and the Alpine system's default shell (sh, provided by BusyBox's ash) attempts to interpret them, or when expected tools are simply not present in Alpine's minimalist distribution. Such failures prevent commits from completing, blocking development workflows and CI/CD pipelines.

Symptom & Error Signature

When you attempt to run git commit, the process halts, displaying an error message similar to the following, indicating the pre-commit hook failed. The exact error details can vary significantly based on the script's content and the underlying cause.

$ git commit -m "Fix: Implement feature X"
hint: The 'pre-commit' hook was ignored because it's not set as executable.
hint: You can correct this with: chmod +x .git/hooks/pre-commit
fatal: cannot run pre-commit hook (exit code 1)

Or, if the hook is executable but encounters a syntax or command issue:

$ git commit -m "Refactor: Cleanup code"
/usr/bin/env: 'bash': No such file or directory
pre-commit: fatal: hook failed with exit code 127

Another common variant points to shell interpretation issues:

$ git commit -m "Update: Dependencies"
.git/hooks/pre-commit: line 5: [[ : not found
.git/hooks/pre-commit: line 10: syntax error: unexpected '('
pre-commit: fatal: hook failed with exit code 2

Root Cause Analysis

The core of pre-commit hook failures on Alpine Linux environments typically stems from one or more of these underlying reasons:

  1. Alpine's Default Shell Incompatibility: Alpine Linux uses BusyBox, and its default shell (/bin/sh) is ash, a lightweight POSIX-compliant shell. Many pre-commit hooks are written specifically for Bash (#!/bin/bash), leveraging Bash-specific syntax (e.g., [[ ... ]], process substitution <(...), specific array declarations, &> redirects) that ash does not support, leading to syntax errors.

  2. Missing bash Interpreter: Even if the shebang (#!/bin/bash) is correct, Bash itself might not be installed on the minimal Alpine system. apk add bash is often required, and sometimes the installed bash binary is not at the exact path specified in the shebang (e.g., /usr/bin/bash instead of /bin/bash).

  3. Missing Dependencies/Tools: pre-commit hooks frequently invoke external tools for linting, formatting, or testing (e.g., npm, node, python, prettier, eslint, jq, terraform). Alpine's base image is extremely minimal and these tools are almost certainly not installed by default. The PATH environment variable might also not include the directories where these tools are located, even if installed.

  4. Incorrect Permissions: The pre-commit script itself must be executable. If it lacks execute permissions, Git will refuse to run it.

  5. Line Ending Issues: While less common on Linux-native environments, transferring files between Windows (CRLF) and Linux (LF) systems can sometimes lead to scripts that fail due to incorrect line endings, particularly if a shebang line isn't parsed correctly.

Step-by-Step Resolution

Follow these steps to diagnose and resolve your pre-commit hook execution issues on Alpine Linux.

1. Verify and Correct Script Permissions

First, ensure your pre-commit hook is executable. This is a fundamental requirement for Git hooks.

# Navigate to your Git hooks directory
cd .git/hooks

# List permissions of the pre-commit hook
ls -l pre-commit

# If it's not executable (e.g., -rw-r--r--), make it so
chmod +x pre-commit

# Verify again
ls -l pre-commit # Should now show -rwxr-xr-x or similar

2. Ensure Bash is Installed and Shebang is Correct

If your hook uses Bash-specific syntax, Bash must be installed, and the script's shebang must point to the correct Bash interpreter path.

# Check if bash is installed on Alpine
which bash
# Expected output: /usr/bin/bash (or /bin/bash)

If which bash returns nothing or an error, Bash is not installed.

# Install Bash on Alpine
apk add --no-cache bash

When building Docker images based on Alpine, always include apk add --no-cache bash in your Dockerfile if your hooks or scripts rely on Bash. The --no-cache flag keeps the image size minimal by not storing package index cache.

Next, inspect the first line of your pre-commit script (the shebang):

# Display the first line of your hook script
head -1 .git/hooks/pre-commit
  • If it's #!/bin/sh: Your script is intended for a POSIX-compliant shell like ash. Review the script for any Bash-specific syntax that might cause errors (see step 3).
  • If it's #!/bin/bash: Ensure that bash is actually located at /bin/bash. In Alpine, bash is typically installed at /usr/bin/bash. You might need to adjust the shebang.
# If 'bash' is at /usr/bin/bash but your shebang is #!/bin/bash, fix it:
sed -i 's|^#!/bin/bash|#!/usr/bin/bash|' .git/hooks/pre-commit

# Alternatively, for maximum portability (if /usr/bin/env is available and functional):
sed -i 's|^#!/.*sh|#!/usr/bin/env bash|' .git/hooks/pre-commit

Using #!/usr/bin/env bash is generally more portable as it relies on the env command to find bash in the system's PATH, rather than hardcoding a path that can vary across distributions.

3. Address BusyBox ash Compatibility for sh Scripts

If your script is intended for /bin/sh but you're still seeing errors like [[ : not found or syntax error, it means your script contains Bash-isms incompatible with ash.

Common Bash-specific constructs that fail in ash:

  • [[ condition ]]: Use [ condition ] instead.
    • Bash: if [[ "$VAR" == "value" ]]; then ...
    • POSIX sh: if [ "$VAR" = "value" ]; then ...
  • Process Substitution <(command): ash does not support this.
  • Array declarations arr=(1 2 3): ash only supports simple indexed arrays in a limited way (e.g., $1, $2), not complex array syntax.
  • &> redirection: Use >& or 2>&1 for redirecting stdout and stderr.
    • Bash: command &> file
    • POSIX sh: command > file 2>&1
  • Arithmetic expressions (( expr )): Use $(expr) or let command.
  • Regex matching with [[ ... =~ ... ]]: Use grep or sed.

You will need to manually review and rewrite these parts of your script to be POSIX-compliant sh or ensure the script is correctly executed by Bash (steps 2 & 4).

4. Install Missing Dependencies/Tools

Your pre-commit hook likely calls external programs. Identify these and install them using Alpine's package manager, apk.

# Example: If your hook uses 'node' and 'npm' for linting
apk add --no-cache nodejs npm

# Example: If your hook uses 'python' for a script
apk add --no-cache python3

# Example: If your hook uses 'jq' for JSON processing
apk add --no-cache jq

# Example: If your hook uses 'docker' or 'docker-compose'
apk add --no-cache docker docker-compose

When installing many development tools inside a Docker image, consider using multi-stage builds. This allows you to install all necessary build/lint tools in a "builder" stage, and then copy only the essential application artifacts to a much smaller final production image.

5. Check PATH Environment Variable

Sometimes, even if tools are installed, the PATH environment variable within the Git hook's execution context might not include the directory where those tools reside (e.g., /usr/local/bin, /usr/bin).

You can debug the PATH by adding echo "PATH: $PATH" to your hook script.

To ensure essential directories are in the PATH for your hook:

#!/usr/bin/env bash
# .git/hooks/pre-commit

# Add common binary paths to PATH if they are not already there
export PATH="/usr/local/bin:/usr/bin:/bin:$PATH"

# Now proceed with your hook logic
# ...

6. Debugging the Hook Script

To get more detailed output on why your hook is failing, add debugging statements.

#!/usr/bin/env bash
# .git/hooks/pre-commit

set -euxo pipefail # 'e' exits on error, 'u' treats unset vars as error, 'x' prints commands, 'o pipefail' exits on pipeline failure

# Add common binary paths to PATH if they are not already there
export PATH="/usr/local/bin:/usr/bin:/bin:$PATH"

echo "Running pre-commit hook..."
echo "Current PATH: $PATH"
echo "Git root directory: $(git rev-parse --show-toplevel)"
echo "Checking for 'prettier'..."
which prettier || { echo "ERROR: prettier not found. Is it installed?"; exit 1; }

# ... rest of your hook commands ...
prettier --check .
eslint . --fix

echo "Pre-commit hook completed successfully."

You can also try running the hook manually from the .git/hooks directory to observe its output directly, simulating the environment Git uses:

cd .git/hooks
./pre-commit

This will often give you more verbose error messages than Git's summary output.

7. Line Ending Normalization

While less common, ensure your script files use LF (Unix-style) line endings, especially if they've been edited on Windows. Git can be configured to normalize line endings.

# Configure Git to handle line endings consistently (typically in .gitattributes)
# For example, in your project's .gitattributes file:
# *.sh text eol=lf
# *.py text eol=lf

If you suspect a specific file is problematic, you can convert its line endings:

# Convert CRLF to LF for a specific file
sed -i 's/r$//' .git/hooks/pre-commit

By systematically working through these steps, you should be able to identify and resolve the root cause of your Git pre-commit hook failures on Alpine Linux environments.

👨‍💻

Johnathon Wheeler

Senior Systems Architect & DevOps Engineer • Austin, TX

Connect on LinkedIn →

Johnathon has over 16 years of hands-on experience designing, debugging, and scaling Linux web hosting stacks, container clusters, and high-availability database architectures. Every guide on ButItWorkedLocal is independently tested against Debian 12, Ubuntu 24.04/22.04 LTS, Rocky Linux, and Docker environments to guarantee reproducibility in production.

🛡️

Our Production Verification Guarantee

Encountering a bug not covered here or running a non-standard kernel configuration? Our solutions are continually refined against real production incidents. Submit an environment trace for our editorial team to replicate.