You get a take-home repo, an OSS sample, or a chunk of agent-generated code and your hands do the usual thing: git clone, npm install, npm run dev. Your brain files it under “just run it locally for a minute.”
That framing is the bug.
You do not need a dramatic malware anecdote to see the problem. The docs are plain about it. npm can run lifecycle scripts during install, as the npm scripts docs describe. Git tooling can wire hooks in after install. Devcontainers have lifecycle commands such as initializeCommand, postCreateCommand, and postStartCommand, per the devcontainer spec. By the time you open src/, you may already have executed the repo’s automation.
A take-home repo is a program before you ever reach src/
The problem starts with labeling. “Interview exercise” sounds like homework. “Demo app” sounds disposable. “The agent scaffolded it” sounds low stakes. Your laptop does not care. It sees a repository with code, dependency metadata, scripts, config files, and maybe automation that runs before the app itself does.
Ask what code paths execute before you have inspected anything, even if the sender seems legitimate.
For a Node and TypeScript workflow, that usually means you are trusting far more than src/index.ts. You are trusting the package manager, repo automation, local developer tooling, and whatever secrets are already sitting in your shell, browser, home directory, and cloud CLI state.
That is why I think “just run it locally” is one of the more dangerous habits in modern dev work. What makes the repo risky is simpler than the label attached to it: it is untrusted code with startup plumbing.
Treat every repo you did not author or fully control as untrusted code with automation attached.
The first dangerous surface is the stuff that runs before the app runs
For this audience, the first place to look is not the feature code. It is the wiring.
Start with Git hooks. Git’s own hooks live under .git/hooks; a normal clone does not bring the other side’s hook directory with it, per the Git hooks documentation. That nuance matters, because people sometimes hear “git hooks” and imagine hidden scripts teleporting in during clone.
Hooks still belong in your threat model. Tooling such as Husky often installs hooks after you run package manager commands, usually via prepare. So the useful question is: what later step wires repo-controlled behavior into Git actions on your machine? If you set core.hooksPath to an empty directory up front, you remove one class of surprise before you start.
Then there are npm lifecycle scripts. This is the bigger surface for most Node repos. npm supports preinstall, install, postinstall, prepare, and friends, and those scripts run as part of normal package manager flows, exactly as the npm docs describe. A lot of engineers say “I haven’t run the app yet” right after they ran npm install. They may already have executed the repo’s chosen shell commands.
That is why package.json deserves inspection before any install command. The part that matters first is often a tiny scripts entry, not the TypeScript you were asked to review.
Devcontainers belong in the same bucket. The devcontainer spec supports lifecycle commands such as initializeCommand, onCreateCommand, postCreateCommand, and postStartCommand. If you let a repository reopen itself in a container before reading .devcontainer/devcontainer.json, you may be executing its automation under the pleasant fiction that “it’s isolated anyway.” Isolation helps. Blind startup automation still deserves a read first.
The same principle applies to editor tasks, bootstrap scripts, Makefiles, and convenience wrappers. Usually the risky part is the ordinary developer ergonomics you stop seeing because they look normal.
Draw the boundary at the environment, not at your intentions
“I’m only checking it for 20 minutes” does nothing for you.
Attack surface does not care about your schedule. The dangerous combination is a property of your wiring: untrusted code plus ambient authority. If that code can see your home directory, your cloud auth state, your SSH agent, your browser profile, or your registry tokens, then “temporary evaluation” is just a story you are telling yourself.
So draw the boundary at the environment.
The good options are boring:
- a disposable VM
- a disposable container you control
- a Codespace or remote workspace with no real secrets loaded
- a separate local OS user profile with no personal or work credentials in it
The details matter. A container with your whole home directory mounted into it is not much of a boundary. A remote workspace that inherits all your usual tokens is not much of a boundary. A repo-supplied devcontainer is useful only after you have inspected the config that tells it what to do.
My read is simple: for unknown repos, the default should be a throwaway environment you can delete without thinking. If you cannot delete the environment right after the review, it was probably too close to your real machine.
Credentials are the blast radius, so keep them out of the room
For a Node shop shipping AI features on Azure, the obvious blast radius is credentials.
I would keep unknown repos away from all of this by default:
- Azure CLI state from
az login
- service principal variables such as
AZURE_CLIENT_ID, AZURE_TENANT_ID, and AZURE_CLIENT_SECRET
- GitHub CLI auth and
GITHUB_TOKEN
- private registry tokens in
~/.npmrc
- SSH agent access through
SSH_AUTH_SOCK
- real project
.env files
- browser sessions in the same user profile you use for work consoles
That list is just the normal pile of ambient authority on a working engineer’s machine.
If the repo needs secrets to demonstrate a feature, give it fake ones, or point it at a purpose-built low-privilege test tenant and test subscription. Do not “just use your real login for a minute.” Do not evaluate in the same shell profile that already exports your cloud creds. Do not mount your host home directory into a container and call that isolation.
Evaluation and real credentials should never share a room.
This lands the same way for AI-generated repos. The fact that a model wrote the code changes nothing about the trust boundary. Generated code is still code you did not author and do not fully control. If anything, its speed makes the habit worse, because the whole point is that you are tempted to run first and inspect later.
A small preflight routine beats cleanup after the fact
You do not need to become a malware analyst. You need a repeatable preflight that removes ambient trust from ordinary engineering work.
Mine would look like this:
- Open the repo only inside a disposable environment you control. VM, container, remote workspace, or spare OS user. Your environment first, the repo second.
- Disable Git hook execution in that repo before you do anything else. Git will not clone remote hook directories by default, but setting an empty
core.hooksPath protects you from later hook installation and from local surprises.
- Read
package.json before any install command. Look at scripts, packageManager, and anything that shells out. Check .npmrc, Makefiles, and .devcontainer/devcontainer.json too.
- Install dependencies with lifecycle scripts disabled first. In npm that means
npm ci --ignore-scripts or npm install --ignore-scripts, depending on the situation.
- Search for shell-outs and network calls before you run the app.
child_process, exec, spawn, curl, wget, PowerShell invocations, and bootstrap scripts deserve a look.
- Run only with fake env and no real tokens loaded. If the app needs Azure, give it a throwaway subscription or a narrow test principal. If it needs npm access, use a token scoped for the review, not your personal one.
That routine will not catch everything. I still think it is worth doing because it catches the category error that causes most self-inflicted damage: treating untrusted repos as if they are data. The wiring is your responsibility.