
In brief
Hooks live in .git, and a .git directory handed to you in a zip or shared folder carries them. A post-checkout hook runs the moment you switch branches, before you ever read the code.
A hook is a program Git runs at a fixed point in its workflow, and hooks live inside .git. That is why normal clones are safe and why a shared folder is not: hand someone a working directory with its .git folder intact and you hand them everything needed to execute code the moment they check out a branch.
This attack has a name in the wild now. A developer was sent a plausible project archive, told to switch to an "NDA branch", and found a real post-checkout hook that fetched an OS-specific binary from a command-and-control endpoint, marked it executable, ran it and deleted itself. Inspecting hooks and config costs about thirty seconds.
01How a post-checkout hook becomes code execution
post-checkout runs after a checkout completes. Git calls it with three arguments — the previous HEAD, the new HEAD and a flag for whether a branch or a file was checked out — and the hook's exit code does not block the operation, so it runs silently.
Because it fires on the transition, the attacker does not need you to run a script. Telling you to git checkout nda is enough. The hook then has your full user privileges: it can read ~/.ssh, ~/.aws, the macOS keychain, environment variables and any credential helper the machine has.
A checkout is not a read-only action in an untrusted repository
If the working directory contains a
.gitfolder you did not create, treat running anygitcommand as running code from its author. That includesgit status, because other config keys execute programs too.
02Why the hooks appear despite not being cloned
The usual clone path is genuinely safe: git clone <url> does not copy remote hooks, and the post-checkout Git may run after cloning comes from your template directory, not the repository. The attack routes around that in three ways:
- A handed-over working tree. An archive, a Dropbox folder or a copied directory that includes
.git. This is what happened in the reported incident. - A tracked hooks directory via
core.hooksPath. Frameworks that manage hooks this way make the hook scripts ordinary committed files, so installing dependencies silently activates them. - A
filterdriver plus.gitattributes. The repository commits.gitattributesreferencing a filter whose command lives in.git/config, and checkout runs that command.
Only the first requires no dependency install. It is also the easiest to fall for, because the folder looks like a normal project.
03Audit an untrusted repository before you run git
Start with the hooks directory and the local config. Every command below was verified against Git 2.50.1.
# Where does this repo keep hooks?
git rev-parse --git-path hooks/post-checkout
# .git/hooks/post-checkout
# List anything that is not a .sample file.
ls -la "$(git rev-parse --git-path hooks)"
A hooks directory containing only *.sample files is the default. A real, non-sample file is a program that will run at its trigger point; read it in full before running Git.
# Any of these indicate hooks, filters or helpers that execute code.
git config --local --list
git config --get core.hooksPath # exit code 1 means unset
git config --get core.fsmonitor
git config --get core.sshCommand
Pay attention to filter.*, core.fsmonitor, core.sshCommand, core.pager, core.editor and any alias whose value begins with !. Each can run a command as a side effect of an ordinary operation.
# Inspect a hook for the tell-tale pattern: download, chmod, execute, delete.
grep -nE 'curl|wget|chmod|base64 -d|/tmp/|rm -f|exec' .git/hooks/post-checkout
Reading a hook is safe; the danger is a Git command triggering it. If a hook is present and you only need to look at the tracked files, open them with an editor or tar rather than with Git.
04Run Git safely on a suspicious checkout
When you must run Git in a repository you do not trust, disable hooks for that invocation and keep the session inert:
# Verified: an empty hooks path disables every hook for this command only.
git -c core.hooksPath=$(mktemp -d) status
git -c core.hooksPath=$(mktemp -d) checkout nda
# Never prompt for credentials from an untrusted repo.
GIT_TERMINAL_PROMPT=0 git -c core.hooksPath=$(mktemp -d) log --oneline -5
For anything more than a glance, copy the tracked files into a fresh directory and initialise a new repository instead of using the provided .git. The entire attack depends on that folder, so discarding it removes the vector.
Clone from a URL, not from a folder
If a collaborator shares a project, ask for a Git URL or a bare bundle, not a working directory with
.git.git clonedoes not run the remote's hooks; copying a folder does.
05If a hook already ran
Assume credentials on the machine are compromised. Disconnect from the network, then inventory what was reachable during the checkout: SSH keys and their passphrases, cloud CLI sessions, browser sessions, the macOS keychain, and any token in your shell environment or .env files. Rotate those credentials from a different machine, and check Git hosting and cloud audit logs for activity from unfamiliar IPs or user agents. Deleting the hook does not undo the execution.
06Common mistakes and fixes
| Mistake | Why it fails | Fix |
|---|---|---|
Running git status in a folder someone sent | Hooks and config can execute code | Inspect .git first, or use core.hooksPath |
Trusting a repo because it has no post-checkout | Other hooks and config keys fire too | Read the whole hooks directory and local config |
| Assuming clones are always safe | Safe only when hooks are not in the tree | Clone from a URL; never adopt a handed-over .git |
Trusting core.hooksPath only | filter.* and core.fsmonitor also execute | Check every exec-capable config key |
| Deleting the hook after the fact | The payload already ran | Rotate credentials and review audit logs |
The habit that matters: a .git folder you did not create is executable content. Look inside it before you type git, and prefer a fresh clone from a URL so there is nothing hostile to look at in the first place.
Build the pattern that finds curl, chmod and base64 decoding inside a hook script.
Regex TesterFrequently asked questions
Do Git hooks get cloned from a remote?
No. A normal `git clone` copies tracked files, not the remote's `.git/hooks`. Cloning even a hostile repository from a URL does not run hooks the attacker committed, because hooks are not part of the tracked tree.
Then how does a post-checkout hook reach my machine?
Someone hands you a working directory that already contains a `.git` folder — in a zip, a shared folder or an archive. That `.git` includes its hooks, so running `git checkout` in it triggers the attacker's script.
What does a post-checkout hook run on?
Git runs `post-checkout` after `git checkout`, after `git clone` finishes its initial checkout, and after `git switch`. It receives the old and new HEAD and a flag indicating a branch checkout or a file checkout.
Can I run git in an untrusted repository without hooks firing?
Yes. Point `core.hooksPath` at an empty directory for that command: `git -c core.hooksPath=$(mktemp -d) status`. Verified locally, this disables hooks for the invocation without changing the repository's config.
Which other Git config keys can execute code?
`core.fsmonitor`, `core.sshCommand`, `core.pager`, `core.editor`, `filter.<driver>.*` paired with a committed `.gitattributes`, and `!command` aliases can all run programs. Always inspect `.git/config` in a repository you did not clone yourself.
Related articles
- How to migrate a Git repository from SHA-1 to SHA-256
A migration is also a moment to audit hooks and local config.
- Compare two texts with a diff checker
Diff a suspicious .git/config against a clean one.
Related tools
- Regex Tester
Find download, chmod and decode calls in a hook.
- Base64 Encoder / Decoder
Decode an obfuscated payload before you execute it.
- Diff Checker
Spot injected lines in config or hook scripts.