Darkul
2026.09.27670 WORDS3 MIN

Your build config is code, so review it like code

Someone ran git pull, then npm run build, and got malware. No new package, no strange file opened. The payload was sitting in vite.config.js, carried in by a merge commit that looked legitimate.

What happened

According to the write-up on dev.to, an attacker forged a merge commit that planted code in a Vite config. The moment the build ran, it connected out to an external server. The author says several security vendors have reported this as part of a wider campaign against the npm and Vite ecosystem. I haven't verified the campaign details myself, so read the original for the indicators and the response steps.

What I care about is the shape of the attack. It lines up exactly with how I work every day.

Config files run with your permissions

We treat vite.config.js, next.config.js and tailwind.config.js as settings. They are not settings. They are JavaScript that Node executes with whatever access your user account has. Your SSH keys. Your .env files. Your cloud CLI tokens.

When I review a pull request, I read the components carefully and skim the config. That is backwards. A bad component breaks a page. A bad config owns the machine.

Merge commits are where review goes blind

The author name and email on a git commit are just text. Anyone can set them to anything. A merge commit with a familiar name on it gets waved through because nobody expects a merge to contain anything new.

It's worse than that. By default, git log -p does not show diffs for merge commits at all. If a merge slipped in changes, you won't see them unless you ask. This shows what each merge on your main line actually changed in any config file:

git log -p -m --first-parent -- '*.config.*'

Run it on any repo you inherited. It takes ten seconds and it's the command I wish I'd been running all along.

Signed commits help too, but only if you require them. git log --show-signature makes an unsigned commit from a "trusted" teammate stand out. If nobody on the project signs, it tells you nothing.

Where I build matters more than what I pull

Here is the part that actually worries me. I work on client projects from one laptop. That laptop holds SSH access to client VPSes. So a poisoned config in one client's repo isn't just my problem. It's a path into every other client's server.

The fix is not "be more careful". Careful doesn't scale and it fails on a tired Thursday night. The fix is structural. The machine that holds deploy keys should not be the machine that executes every build config anyone pushes.

The cheapest version is a container. Install dependencies with install scripts disabled, then build with no network at all:

docker run --rm -v "$PWD":/app -w /app node:22 npm ci --ignore-scripts
docker run --rm --network none -v "$PWD":/app -w /app node:22 npm run build

If the build breaks without network, you've learned something about your build that you should have known already. If a package genuinely needs its install script, you now know which one, and you allow it on purpose instead of by default.

This isn't a sandbox you can trust blindly. The container still mounts the repo, so a payload can tamper with your source or your output. What it can't do is read your home directory or phone home during the build. That's the blast radius I care about.

What I'm changing

Three things, starting this week.

First, config diffs get read line by line on every pull, including merges. Components can wait. Config can't.

Second, builds for client repos run in a throwaway container with no network. My laptop's Node install stops being the default place untrusted code executes.

Third, one deploy key per client, scoped to that client's server. If one project gets compromised, the damage stops at that project's door.

None of this is clever. It's the same thinking I already apply to servers: assume something will get in, and decide in advance how far it gets to walk. I just never applied it to the ten lines of JavaScript at the root of every repo.

One issue a week

What I wrote, what broke, and what I would do differently. No cadence beyond weekly, and nothing else in your inbox.