Skip to content
← Back to blog
Security·September 29, 2026·7 min read

How to check a client's git repo for malicious hooks before you open it

A 10-minute triage for agencies: where malicious code hides in a client's repo (hooks, VS Code tasks, npm scripts) and the commands to find it safely.

A git repository can run code on your machine before you've decided to run anything. On September 29 a fake client sent us a project archive and asked us to check out a branch called "NDA", and a hook inside it would have downloaded and run a script the moment we did (the full story is here). We caught it because we looked first. This is the ten-minute triage we now run on every repository that arrives from someone we haven't worked with, with the exact commands, so you can run it too.

The principle behind every step: read the project as text before you let any tool act on it. Git, your editor and your package manager all execute things on your behalf. Until you know what they would execute, don't give them the chance.

Where code can run before you run it

Most people know that npm install runs scripts. Fewer know how many other places a repository can hide something that fires on its own. Here's the list we check, and whether each one survives a normal clone:

LocationWhen it firesCarried by git clone?
.git/hooks/*On checkout, commit, merge, rebaseNo, only in an archive of the folder
.git/config (core.hooksPath, core.fsmonitor, filters)On many everyday git commandsNo, only in an archive
.githooks/, .husky/After a setup script points git at themYes
.vscode/tasks.json with runOn: folderOpenWhen you open and trust the folderYes
package.json preinstall / postinstallOn npm installYes
.envrc (direnv)When you cd into the folder, once allowedYes
Makefile, setup.sh, "run the demo"When you run themYes

Read the third column carefully. A fresh git clone from a hosting service never brings .git/hooks or .git/config with it, which is why the lure we received arrived as a .tar.gz of the whole folder. An archive of a repository is more dangerous than a link to one, and it's a fair question to ask why a client can't just send a link.

Step 1: list the archive, don't extract it

Listing an archive reads its table of contents. Nothing inside runs.

For a tarball: tar -tzvf project.tar.gz | grep -E '\.git/(hooks|config)'

For a zip: unzip -l project.zip | grep -E '\.git/(hooks|config)'

A normal repository shows a set of files ending in .sample, and those are inert. Any hook that isn't a .sample file is a stop sign. In our case the listing showed an executable post-checkout sitting among the samples, dated two weeks before the repository itself was created. That's all it took.

Also look at the owner names and dates in the verbose listing. Ours was packed by a user account with a different name from the sender, which is not proof of anything, but it's one more question.

Step 2: read git's own files as plain text

You can print a single file from inside an archive to your terminal without extracting it anywhere:

tar -xzOf project.tar.gz project/.git/hooks/post-checkout

tar -xzOf project.tar.gz project/.git/config

In a hook, look for anything that downloads or executes: curl, wget, base64, chmod +x, Invoke-WebRequest, iwr, mshta, or a URL ending in vercel.app or another free hosting domain. The FBI's September 2026 alert on this campaign lists several of the same strings as warning signs.

In .git/config, look for hooksPath, fsmonitor, sshCommand, and any [filter or [diff section with a command in it. A plain repository config has a handful of core settings and maybe a remote. Anything that names a program to run deserves a hard look, because git will run it for you during commands as ordinary as git status.

Step 3: check the files that run on open or install

If the archive passes the first two steps, it's reasonable to extract it (into a folder you don't open in your editor yet) and search it:

grep -rnE 'postinstall|preinstall|folderOpen|curl |wget |base64|chmod \+x|vercel\.app' . --include='*.json' --include='*.sh' --include='*.js' --include='Makefile'

Then read these by eye, because a grep only finds what you thought to look for:

  • package.json, the scripts block in particular
  • .vscode/tasks.json and .vscode/settings.json
  • anything in .githooks/, .husky/ or a scripts/ folder
  • the README's setup instructions (they tell you what they want you to run)

One more editor-specific note: when VS Code asks whether you trust the authors of a folder, "No" is the right answer for anything from a stranger. Restricted mode stops tasks and many extensions from running automatically.

Step 4: read documents without checking out branches

Often the whole point of the repository is to hand you a document, as ours was with its NDA. You don't need to switch branches to read it. Git can print any file from any branch straight from its object store:

git --git-dir=project/.git ls-tree -r NDA lists what's on the branch.

git --git-dir=project/.git cat-file -p <hash> > nda.docx writes one file out.

Neither command triggers a checkout hook. If you must switch branches, disable hooks for that one command: git -c core.hooksPath=/dev/null -c core.fsmonitor=false checkout NDA. That points git at an empty hooks location and turns off the file-system monitor, so nothing in .git/hooks or the monitor setting can fire.

When to stop and use a throwaway machine

Everything above is about reading safely. The moment you need to run a stranger's project (install dependencies, start the demo, run tests) do it in a disposable virtual machine or cloud box that holds none of your credentials: no SSH keys, no browser logged into client systems, no cloud tokens. The IC3 alert gives the same advice for unknown code.

Our rule of thumb for agencies, where one developer's laptop can hold keys to a dozen client systems:

SituationWhat we do
Hosted repo link from a known clientClone and review normally
Archive of a repo from anyone newSteps 1 to 4, never open in an editor first
Anything we're asked to install or runThrowaway VM only
An NDA or contract "in the repo"Ask for a PDF or e-signature link instead

This takes about ten minutes the first time and two once it's habit. We think it's the cheapest security measure an agency can adopt, and it would have been enough on its own to stop the attack we received. If you want help building it into how your team onboards new clients, our cybersecurity team can set it up with you, and for the lead-qualification side of the same problem, see what we look for when vetting a development partner.

Sources

Frequently asked questions.

List the archive without extracting it and look under .git/hooks for any file that does not end in .sample. Print suspicious hooks and .git/config as plain text and look for curl, wget, base64, chmod +x or a download URL. Then search package.json, .vscode/tasks.json, .githooks and .husky before opening the folder in an editor.

A fresh git clone does not bring the .git/hooks folder or .git/config with it, so cloning is safer than opening an archive of the folder. It does bring committed files such as package.json scripts, .vscode tasks and .githooks folders, so review those before running npm install or trusting the folder in your editor.

Use git ls-tree to list the branch, then git cat-file with the file's hash to print or save it. Neither command triggers a checkout hook. If you must switch branches, run the checkout with core.hooksPath set to /dev/null and core.fsmonitor set to false for that one command.

Whenever you need to run anything from it: installing dependencies, starting a demo or running tests. Use a disposable machine that holds none of your SSH keys, cloud tokens or logged-in browser sessions, which is also what the FBI advisory on this campaign recommends for unknown code.