Reading untrusted GitHub repos without running them
Job-seeker workflows have a steady hostile artefact in them: the "try our stack" GitHub repo. Some are bait. We have read several without running them, in a short-lived container on a segregated host. This is the methodology, what each path catches, and what we are honest about not catching.
The hostile repo is a routine artefact of the modern job-seeker email. A recruiter describes a fictional stack, sends a GitHub link, says "feel free to try it locally." A take-home prompt arrives as git clone me and run npm install. A "scoring demo" arrives as a multi-service docker-compose with a footgun on every famous YAML key. Through several months of interviewing, Mark saw these roughly weekly; the more polished variants were engineered to do something bad on first execution, and the proportion that lands hostile varies by trade and is not stable.
DSC has now performed deliberate reviews of untrusted repos on the order of half a dozen times. One case — the NitroGem-style incident — generated a long private writeup because the project was a fake-employment scam aimed at engineers. Other cases were less dramatic but tighter to the line, and those cases are what shaped the second, heavier isolation path. This post is the methodology we settled on, separated into the lighter review and the heavier review, and the parts of every path we are honest about not catching.
The first lesson of the series is that the danger is mostly after the files arrive, not in transit. A git clone does not execute the cloned repository's hooks. It does not run postinstall scripts, does not instantiate language-tooling state against the user's editor, does not even know the URL it cloned was an attack now. The repo lands as inert files until something with side effects touches them — npm install, pip install, go build, cargo build, yarn, pnpm, mvn, gradle, docker compose up, the editor opening a package.json, the IDE's plugin calling into a .gradle or a Cargo.toml. The attack surface is what comes next.
The vectors we have actually seen used:
- post-install scripts that exfil environment variables and SSH credentials to a remote endpoint on first dependency install;
- "little helper" binaries that look like build tooling but call home before writing the user's
~/.bashrc; - test runners whose CI config quietly swivels a stub back-end into a real back-end when run in an environment that has the right DNS;
- pre-commit or pre-push hooks that only fire when the user runs a build, never when the attacker demos the project;
- IDE-side plugin execution on first open, when language tooling warms up against a hostile manifest.
The defensive posture we took was shaped by that fact. The goal is not to chase zero side-effects on a real working box — impossible — but to put a wall between the candidate's main host and the first opportunity for a hostile artefact to fire side-effects. Two isolation paths cover what we have actually run.
Light path: container on a segregated host, static review
The lighter path is the one we ran on the case that became the long writeup. The hostile project was credential-exfil and remote-code-execution in the install path, but the path was simple enough to identify by reading. We never needed to detonate the project to find the bad art.
What the light path actually looks like:
- A segregated remote host, not the candidate's workstation. The host is not the host we use for anything else. We do not need it to be pristine; we need it to be segregated.
- A short-lived Docker container started with the obvious flags:
--rmso cleanup is automatic,--cap-drop=ALL --security-opt no-new-privilegesto remove Linux capabilities the project does not need to read its own files, and the project files mounted on a tmpfs inside the container so filesystem persistence is bounded. - Checkout happens on the container, not on the host. The hostile repo's commits are inspected inside the container's tmpfs, never on the segregated host's filesystem.
- The work is static. List the files. Read the manifests. Grep for credential references, network endpoints, base64 chunks, suspicious shellouts, suspicious child-process calls. Optionally run ClamAV against the tree; the false-positive rate is tolerable for an early signal.
- The project is not installed, the project is not built, the project is not run. This is the discipline. The README's "try it" instructions are not followed.
- After review, the container is removed, the idle host is wiped free of the tmpfs artefacts, and the host is rebooted if any deviation in discipline has occurred.
Network is needed for two things and only two things: cloning the repo, and refreshing the static-review tooling. Some runs cannot avoid the first, and we do not advertise a permanent zero-network fantasy in cases where the host has to clone. The discipline is "no project traffic," not "no network at all."
What this path catches well: credential-exfil patterns, postinstall-side-effect patterns, network callouts in build tooling, suspicious package-lock churn, suspicious pre-commit / pre-push scripts, anything that becomes visible on a careful read. It also has a low false-positive rate, because the reviewer is reading, not detonating. This is the right path for cases where the project is small enough to read end to end and where the threat model is "someone will skim and run."
What this path does not catch: anything that activates only at runtime, multi-stage droppers that need a built and packed artefact, or memory-resident implants that don't surface in the source. We are not running a behavioral lab here, and we should not pretend we are. The light path is honest on this point: it finds what reading finds, and signs off.
Fuller path: tighter isolation when reading isn't enough
The heavier cases we have taken on involved projects that read safely at the file level — manifest looks clean, no obvious shellouts, no pre-commit hooks — and only become hostile when built or run. They are rarer than the static-readable variety, but they exist, and the methodology has to handle them.
What changes for the heavier path:
- The host moves to a dedicated junk host, never used for anything else, treated as if burned after each review. Filesystem snapshot before, filesystem wipe after, restore from image. The host is not a persistent identity.
- The container starts in a two-stage network posture. The clone happens with the network on. Before any build, analyze, or open step, the network drops. From that moment forward, the container has no egress. If the source attempts an outbound call, the container logs the attempt and the attempt fails.
- Mounts are tighter: only the project tree, mounted read-only after checkout. The host filesystem is not visible inside the container.
/procand/sysare stripped where possible. - Capability drops expand:
--cap-drop=ALL --security-opt no-new-privilegesis still the floor, plus--read-onlyfilesystem, plus disabled--init, plus explicit--usermapping so the build runs as a UID that does not exist on the host. - If the case is dynamic-analysis-worthy and the host has been authorized to detonate, the container is treated as terminally burned at the end of the run — image wiped, host dropped offline, scratch recreated. The container is not reused.
- Network egress is logged at the wire, not just at the syscall surface. We don't hunt for syscall-coverage theater here.
This path is good enough to catch most things that become hostile post-build. It is not a behavioral malware lab in the academic sense, and we are not going to dress it up as one. The honest claim is: this catches the cases where the static path doesn't, and it catches them by closing the network rather than by exhaustive monitoring.
If you have read this far thinking "this is what a sandbox ought to be": yes. And the heavier path is the right place to start from when the case warrants it.
What this methodology doesn't catch
Every methodology has a ceiling, and ours is shorter than the marketing around malware labs would suggest.
We are not catching multi-stage droppers whose payloads arrive over time as second-stage downloads — the heavier path closes egress for analysis, not for the original clone, and a clever second-stage payload can arrive during the clone window. The mitigation is clone-via-mirror then read-the-mirror, and we use it when the threat model warrants it, but it is not the universal practice on every case.
We are not catching runtime-only implants that surface in the memory of a running build. Those require a behavioral lab, and we have not built one for these reviews. If the engagement needs that tier, we say so before the engagement starts; we have not yet taken one at that depth.
We are not catching social-layer artefacts — the README, the issue tracker, the contributor history, the commit-time meta-analysis. We treat those as input to the read step, not as part of the inspection.
We are not catching supply-chain compromises upstream of the cloned repo. We are reading what the artefact is, not auditing the chain.
We say all of this so the methodology reads as one. The light path is honest about what reading catches. The heavier path is honest about what closing the network catches. The product surface is "we read the repo," not "we run a malware lab." Staying honest here is what keeps the methodology trustworthy.
How to pick between them
The decision tree is short:
- If a reviewer can read the whole project tree in under an hour and the project is small enough that the build would not surprise them, use the light path. Static review, segregated host, no run.
- If the project is large, the build is non-obvious, and the manifest hints at runtime activation, escalate to the heavier path. Two-stage network, junk host, treat the container as burned.
- If the case needs a behavioral lab, say so up front. The honest move is to point at teams that run one.
That's the methodology.
If your team is handling untrusted-repo risk — open-source candidates, take-homes, hostile firmware images, "try our stack" interview artefacts — and wants to talk about what shape the inspection should take, tell us what you are trying to secure. The honest opening question is: what are you trying to secure, and from what. We will tell you whether our methodology fits.