Skip to content
← Blog

Technical explainer

Vibe coding to CI: when your side project becomes a real target

3 min read

There's a moment in every side project's life where it grows up. You add a Vercel deploy. You hook up GitHub Actions. You pay for the Stripe-enabled tier because you want to charge $9/month for the thing.

At that moment, the supply-chain risk on your repo goes up by an order of magnitude. Not because the packages changed — but because the consequences of a malicious one running in your install step changed.

What CI has that your laptop doesn't

Your laptop has:

  • Your personal AWS dev account (or none)
  • Your GitHub PAT for a single user
  • Your Stripe test keys
  • Your home .env files
  • Cookies for sites you log into

Your CI has:

  • The deploy key for your production environment
  • A GitHub Actions secret named STRIPE_SECRET_KEY (production)
  • A Vercel deployment hook with permission to ship to the live site
  • A DATABASE_URL pointing at the production Postgres
  • An SMTP password that sends real email to real customers

A postinstall script in CI has access to all of those via $GITHUB_TOKEN, $STRIPE_SECRET_KEY, environment variable enumeration, and the filesystem. There is no human watching the install logs. The CI workflow runs to completion and emits the deploy.

If the package was bad, the deploy already happened.

The two specific risk shifts

1. Higher-value secrets in scope. On your laptop, the worst-case is your personal GitHub PAT or AWS dev account. In CI, it's the production deploy key for the company. The same npm package, the same script, executes in both — but the second one can ship a malicious build to your real users.

2. Less human oversight. When you run npm install locally and a postinstall script does something weird, you might notice (slow install, network beachball, unexpected file in your repo). In CI, the only signal is the workflow's final pass/fail. If the script is silent and the build succeeds, you never see it.

The smallest checklist that actually moves the needle

For solo developers and small teams shipping AI-assisted side projects to production:

  1. Use npm ci in CI, not npm install. npm ci requires a lockfile to match exactly. If a package was hijacked but your lockfile is from before the hijack, npm ci will install the old (clean) version. npm install may pull newer transitive deps that include the hijack.

  2. Pin GitHub Actions by SHA, not by @v3. Tags are mutable. SHAs are not. actions/checkout@v4 can have its v4 tag re-pointed at malicious code; actions/checkout@8e5e7e5ab8b370d6c329ec480221332ada57f0ab cannot.

  3. Add a supply-chain gate at the install step. Same idea as your laptop: route the npm install through a local proxy that scores packages before download. If something fails the score, the build fails — better than letting the malicious code ship.

  4. Limit the blast radius. npm install in CI should run with --ignore-scripts if possible. Most postinstall scripts in production deps are unnecessary; the ones that are necessary (native compiles, etc.) you can list explicitly.

  5. Rotate CI secrets on a schedule. The longer a secret lives, the more chances a malicious package has to grab it. Quarterly rotation is the floor; monthly is better.

Veln in GitHub Actions

The shim approach works in CI the same as on a laptop. A typical job:

- name: Install Veln
  run: curl -sSL get.veln.sh | bash

- name: Activate
  run: veln activate --token ${{ secrets.VELN_ACTIVATION_TOKEN }}

- name: Install with gate
  run: veln safe npm ci

veln safe here means npm ci runs with the gate in front of it. Any package below the block threshold returns 403; npm exits non-zero; the workflow fails before the deploy step. The malicious version never gets pulled into the build artifact.

This isn't a replacement for the rest of the checklist — pinning SHAs, using npm ci, rotating secrets, all of that still matters. It's the layer that catches the thing the rest of the checklist doesn't catch: a brand-new malicious version that's never been seen before, published in the hours between your last build and this one.

The honest scope

A gate in CI won't stop a build-time poisoning of dist/ (a malicious bundler plugin can rewrite your output anyway). It won't catch a zero-day in a legitimate library before disclosure. It will catch the high-frequency stuff: slopsquats, hijacked versions, packages with credential-stealing postinstall scripts. That's the bulk of the recent incident corpus, and it's the bulk of what your CI is currently exposed to.

You're already going to ship the side project. Add the check.