A colleague pings me on a Friday with a link. "Can you run this repo through the build agent? It's a fork, should be quick." I open the checkout, and the first thing I see is a postinstall script that curls a URL I've never heard of. The second thing I see is that my shell still exports the staging deploy key from earlier in the day.
Nothing has run yet. Everything is already arranged for a bad afternoon.
The uncomfortable claim I want to defend here is simple. When you point an autonomous build at an unfamiliar checkout, the repository is not the input. It is the attack surface. Forks, vendored Makefiles, hook directories, package.json lifecycle scripts — each of these is code that runs before anyone reads the diff, and every one of them inherits whatever your shell happens to be carrying.
So the question stops being "is this repo good?" and becomes "what does this repo get to touch while I decide?"
The three doors a clean checkout opens
Lifecycle scripts come first, because they are the loudest and the easiest to forget. preinstall, install, postinstall, prepare — four names, one property: the package manager runs them, not you. A fork can add one in a single commit that reviewers skim past.
Hooks are quieter. .husky/, .pre-commit-config.yaml, a Makefile with a default target that does more than compile. None of these fire until you invoke a tool, and the agent invokes tools constantly.
Then there's the door nobody thinks of as a door: your environment. Every API_TOKEN, every SSH_AUTH_SOCK, every cloud CLI profile in $HOME. A build subprocess sees all of it, and an agent build subprocess sees it and then writes about it in a log you'll paste into an issue later.
Ninety seconds of pre-flight, before any agent touches it
I keep one script. It does not sandbox anything — it just makes the invisible parts loud. Run it inside the checkout, before the build, every time.
#!/usr/bin/env bash # preflight.sh — inventory, not enforcement. Run before any agent build. set -uo pipefail if [ -f package.json ]; then echo "== lifecycle scripts ==" jq -r '.scripts // {} | to_entries[] | select(.key|test("^(pre|post)?(install|prepare)$")) | "\(.key): \(.value)"' package.json 2>/dev/null \ || echo " (jq missing — grep package.json by hand)" fi echo "== hook and build entrypoints ==" find . -path ./.git -prune -o -maxdepth 3 \ \( -name '.husky' -o -name '.pre-commit-config.yaml' \ -o -name 'Makefile' -o -name 'setup.py' \) -print echo "== tracked env files ==" git ls-files | grep -E '(^|/)\.env(\.|$)' || echo " none tracked" echo "== secrets currently exported ==" env | grep -Eic '^[A-Z0-9_]*(TOKEN|KEY|SECRET|PASSWORD)[A-Z0-9_]*=' \ | xargs -I{} echo " {} matching variables in this shell" That last line is the one that saves me. It prints a count, not the values — enough to make me stop, nothing that leaks if the transcript ends up in a ticket.
When the count is above zero, the build runs scrubbed instead:
export TMPHOME="$(mktemp -d)" env -i PATH="$PATH" HOME="$TMPHOME" LANG=C.UTF-8 ./build.sh env -i is blunt and I like it for that. What survives is what you typed, not what accumulated since Tuesday.
Blast radius, by trust level
Most teams don't need a policy document. They need four rows and a habit of picking one deliberately.
| Checkout origin | Has lifecycle hooks | Outbound network | Scrubbed env | Lane I'd pick |
|---|---|---|---|---|
| My own branch | Any | Allowed | No | Local shell |
| Teammate's branch, same org | Any | Allowed | Yes | Local shell, scrubbed |
| External fork, code reviewed | Yes | Denied | Yes | Container, --network=none |
| External fork, unreviewed | Yes | Denied | Yes | Disposable VM, throw it away |
Two details in that table do the actual work. --network=none converts a malicious postinstall from an exfiltration channel into a failing command, which is exactly what you want to see in the log. And the last row, the throwaway VM, is the only row I trust to survive a genuinely hostile repo — everything above it is a convenience trade.
docker run --rm --network=none \ -v "$PWD":/work:ro -w /work \ --user 1000:1000 --memory 2g --pids-limit 256 \ node:22-slim sh -c 'npm ci --ignore-scripts && npm test' --ignore-scripts gets you most of the way for a Node repo and costs nothing to type. --read-only on the mount means the build can't leave a modified package.json behind for the next run to trust.
Where free capacity actually fits
The lane that matters most here is the first one — the cheap dry run that proves a checkout is boring before it goes near anything expensive. At the start of a review I want to know three things fast: does it build, does it talk to the network, and does the test suite pass with no credentials at all.
That's a job for someone else's capacity, not my laptop's. MonkeyCode publishes free model access and a free server option — that's the operator's stated availability, and current quotas and terms are worth confirming on the project page before you lean on them. The reason it fits is unglamorous: a scrubbed, network-denied build that produces one log line is not worth provisioning for, and a shared free tier is a reasonable place to put it.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
To be clear about the order of operations: the pre-flight inventory runs first, the scrub runs second, and the model-assisted part runs third on a diff the first two steps already declared safe. Reverse that order and you've built a very efficient way to let a fork read your staging key.
What this doesn't save you from
env -i doesn't stop a script from reading ~/.ssh/id_ed25519 if you bind-mounted your home directory. It doesn't stop a Makefile from writing to a path outside the working tree if the mount is read-write. It doesn't detect a dependency that behaves well for weeks and then flips, and it doesn't help at all if you're running as root inside the container.
Threat modelling by script gets you the first ninety percent of peace of mind and none of the last ten. Be honest about which one you have.
Who should ignore all of this
If your pipelines already execute every build in a hermetic container with a deny-all egress policy, you're done — this article is describing your existing setup in worse words. Same if you only ever build repos your own team wrote on branches you control. The cost of the ritual only pays off when a stranger's commit gets to run code on your machine.
What's the one check you refuse to skip before handing a repo to something that runs without asking? Mine used to be two lines long. Every time someone surprises me it grows, and I'd rather hear about yours than learn it the hard way.


