UrsaKit cover for the guide to auditing Git hooks for credential theft

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 .git folder you did not create, treat running any git command as running code from its author. That includes git 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 filter driver plus .gitattributes. The repository commits .gitattributes referencing 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 clone does 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

MistakeWhy it failsFix
Running git status in a folder someone sentHooks and config can execute codeInspect .git first, or use core.hooksPath
Trusting a repo because it has no post-checkoutOther hooks and config keys fire tooRead the whole hooks directory and local config
Assuming clones are always safeSafe only when hooks are not in the treeClone from a URL; never adopt a handed-over .git
Trusting core.hooksPath onlyfilter.* and core.fsmonitor also executeCheck every exec-capable config key
Deleting the hook after the factThe payload already ranRotate 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.

Frequently 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.

Back to articles

Related articles

Related tools