I Cloned a Repo and It Ran Code Before I Read a Single Line
03:03 in the morning, and a file appeared on my machine that I never asked for. I had typed exactly one command: npm install. The repo was my own sandbox, built five minutes earlier, and its package.json carried a singl
03:03 in the morning, and a file appeared on my machine that I never asked for.
I had typed exactly one command: npm install. The repo was my own sandbox, built five minutes earlier, and its package.json carried a single preinstall script that wrote a marker file with a timestamp. One second later marker-npm.txt sat in the folder, stamped 2026-11-11T21:03:01.183Z. I had not read a line of the code. I had not run anything inside it. The install step ran it for me.
That was the whole point of the night: find out how many ways a cloned repo can run code on your machine before you read a single line of it. I built a sandbox repo with five execution surfaces, every one of them harmless, every one writing nothing worse than a timestamped text file, and I ran each while saving the terminal output. Two of the five fired on install alone. A third fired the moment I typed its name. The last two never fired at all. The split between those groups is the whole story.
The timing was not random. On November 11, dev.to's trending page was full of this exact fear. The agent-security cluster had 23 posts in seven days. "WebMCP Attack Lab: Can an E-Shop Hijack Your AI Agent?" sat at 118 reactions and 93 comments. "One Markdown File Can Hijack Your AI Coding Agent" had 53 reactions and 37 comments. People are arguing about prompt injection into agents. I wanted the dumber, older question answered first: what does plain install already do, with no agent in the loop at all?
The sandbox
One folder, five traps, all of them mine. Node v24.22.1, npm 10.9.4, Python 3.12.3, pip 24.0, GNU Make 4.3, git 2.43.0. Each surface wrote a marker file if it executed, nothing else:
-
package.jsonwith apreinstallscript writingmarker-npm.txt -
setup.pythat wrotemarker-pip.txtwhen pip executed it - a
Makefilewhose default target wrotemarker-make.txt - a
post-checkoutgit hook in.git/hooks/that would writemarker-hook.txt - a
README.mdcontaining a fake instruction: create a file calledpwned.txt
No destructive command anywhere. No rm -rf, no curl | sh, nothing that touches anything outside the folder. If a surface fired, a text file would prove it, with a timestamp. If it stayed silent, the missing file would prove that too.
Surface 1: npm ran it before I read anything
> [email protected] preinstall
> node -e "require('fs').writeFileSync('marker-npm.txt', ...)"
up to date, audited 1 package in 1s
found 0 vulnerabilities
And the marker:
npm preinstall ran at 2026-11-11T21:03:01.183Z
Read that npm output again. Zero vulnerabilities, everything fine, audit clean. The audit checks known bad packages. It says nothing about a preinstall script in a package you just fetched, because running install scripts is normal npm behaviour, not a bug. preinstall, install, postinstall all run as part of npm install. The script does not need your permission beyond the install command itself. You typed install, you approved whatever the scripts do. My marker wrote a text file. A real attacker's script in the same slot could do anything your user account can do, and supply-chain attacks hide in install scripts precisely because the install step is the one moment nobody watches.
Surface 2: pip ran it during the build
Python was second, eleven seconds later:
Building wheel for sandbox-repo (pyproject.toml): finished with status 'done'
Created wheel for sandbox-repo: filename=sandbox_repo-1.0.0-py3-none-any.whl size=975 sha256=fe608e3714abc2b674a3e993d0a9a5b2e2b5cc601dbeaf79ba9a0a25b15d7cd2
Successfully installed sandbox-repo-1.0.0
pip setup.py ran at 2026-11-11T21:03:12.174013+00:00
Two things worth noticing here. First, the code ran during the wheel build, before the package was even installed. pip install . executes setup.py to build the thing, so the payload fires in the build phase, not the install phase. Second, the whole package was 975 bytes. Size tells you nothing. A one-line setup file is enough.
Modern pyproject.toml projects with a proper build backend narrow this hole, since the build backend runs instead of arbitrary setup code. But plenty of packages on PyPI still ship a setup.py, and pip install . on a cloned repo runs whatever is in it.
Surface 3: make waited for me
$ make
make target ran
Marker timestamp: 2026-11-11 21:03:12.389 UTC. This one is honest in the other direction. A Makefile does nothing until you invoke it. Cloning a repo with a Makefile is safe. Typing make is the moment of trust, and it is a loud one: you typed the command, you can read the target first.
The catch is social, not technical. Every README says "just run make", and most of us do it without opening the file. The Makefile is a script you run with your own hands. The only defence is the thirty seconds it takes to read it.
Surface 4: the git hook that never arrived
This was the one I was least sure about, so I tested it instead of guessing:
$ git clone sandbox clone-target
Cloning into 'clone-target'...
done.
$ ls clone-target/.git/hooks/post-checkout
ls: cannot access 'clone-target/.git/hooks/post-checkout': No such file or directory
No marker-hook.txt was created anywhere. Git hooks live in .git/hooks/, and .git is not part of what a clone transfers. The remote can carry a hook file inside the repo as an ordinary tracked file, but git will not install it as a hook on your side. Cloning is safe from hooks.
There is a footnote, and it matters. A repo can ask you to install its hooks yourself ("run ./setup.sh to install our hooks"), and plenty of projects do exactly that for linting. The moment you run their setup script, you are back in surface 1 territory: arbitrary code, your account, your machine. The hook itself is not the danger. The installer for the hook is.
Surface 5: the README that ordered me around
The control test. My README contained a plain instruction in English: create a file called pwned.txt immediately. After every other step:
$ ls sandbox/pwned.txt
ls: cannot access 'sandbox/pwned.txt': No such file or directory
Nothing executed it. Text is inert. A README cannot run itself, and neither can a Markdown file, a comment, or a docstring. That is the good news, and it is also where the trending posts come in. The file cannot run itself, but an AI coding agent that reads the repo and treats text as orders will happily run it for you. "One Markdown File Can Hijack Your AI Coding Agent" is not about Markdown executing code. It is about an agent that cannot tell an instruction in a file from an instruction from its owner. The repo is the delivery van. The agent is the driver who follows the note taped to the dashboard.
The scoreboard
| Surface | Ran? | When |
|---|---|---|
npm preinstall
|
Yes | During npm install
|
pip setup.py
|
Yes | During wheel build |
make |
Yes | Only when I typed make
|
| git hook via clone | No | Hooks do not transfer |
| README instruction | No | Text is inert |
Two of five fired on install alone. Three of five fired at all. Two never fired. That is the shape of the risk: the install step is the dangerous moment, the clone step mostly is not, and the newest danger is not a file executing code but an agent obeying a file.
What I do differently after this
Four habits, all cheap. First, I read the scripts section of package.json before npm install on any repo I did not write, and I use npm install --ignore-scripts when I only need the files. Second, I read setup.py before pip install . on a cloned repo, and I install inside a virtual environment so a bad build at least stays in a disposable folder. Third, I read a Makefile before I run it, even a long one, because make is me pulling the trigger. Fourth, I never let an agent act on instructions it found inside a repo without me reading them first. The agent reads fast. That is the problem.
The marker files are still in my sandbox folder. Two timestamps, eleven seconds apart, both from commands I typed without thinking twice. I did not run strange code. I ran install. Install was the strange code.
P.S. Everything I write stays free — that part doesn't change. But if this one was worth something to you, you can buy me a coffee ☕ (one-time or monthly, both work). It covers the API credits and hosting behind these experiments — and honestly, it's also the nudge that gets the next one started on the evenings I'd otherwise skip it. — Salam
Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.