Recommended rollout

Last updated: June 7, 2026

Adding any static analyzer to an existing codebase has the same failure mode: you turn it on, it finds 137 things, the team looks at the wall of red, you turn it off. We watched it happen to ourselves dogfooding Nark on our own SaaS this week — first run, 137 violations. We didn't turn it off, because we've been here before with other scanners. This page is the playbook.

The three phases

Three phases over roughly two weeks. The point of the structure isn't to be slow — it's to make sure the team understands the gate before it starts blocking PRs. Skipping straight to Phase 3 is how scanners get ripped out.

Phase 1

Shadow mode — week 1

Add Nark to CI with continue-on-error: true. The scanner runs on every PR + main push, uploads its audit JSON as an artifact, posts a step summary — but does not block merges. The goal of this phase is the receipt, not the gate: prove to the team that the scan runs reliably, and capture the first baseline.

What to expect: your first run will typically surface 50 to 250 findings on a codebase that wasn't built with Nark in mind. Frameworks like Nest, Fastify, and Next often centralize error handling in ways Nark's profiles don't yet model. Expect a small number of framework-related false positives in month 1. We tune these out with pattern suppressions during the coverage push.

Phase 2

Triage — week 1 → week 2

Walk the baseline. Every violation has one of three outcomes:

  • True positive (fix it) — the call really is missing error handling. Wrap in try/catch, expose the error to the caller, add the onError handler the profile says is required.
  • False positive (suppress it) — the framework / your wrapper / the call site context already handles the failure mode. Add an entry to .nark/suppressions.json with a reason field explaining why. The reason field is mandatory and lives in the commit so it doesn't rot.
  • Profile is wrong (file it) — if a profile flags a pattern that's definitively not an error path for that package, file a corpus issue at github.com/nark-sh/nark-corpus. Profile tightening is how the scanner gets less noisy for everyone, not just you.

For the founder introducing Nark to their team: most triage work is done with nark-fix --audit. It runs Claude to classify each violation as true positive, false positive, or suppressible and writes a plan document. You review the plan, then nark-fix --apply executes the fixes. Two-command flow, minutes not hours.

Realistic target: ~80% of the baseline gets suppressed (with reasons), ~20% gets fixed. The 20% that gets fixed is your launch narrative.

Phase 3

Block — week 2+

Drop continue-on-error: true from the Nark step in your workflow. New violations now block PR merges. The suppressions you committed in Phase 2 cover the pre-existing baseline, so the blocker is strictly net-new error-handling gaps introduced by the current PR.

Pair this with nark --diff origin/main..HEAD in PR-mode jobs so the gate is line-level: pre-existing violations on unchanged lines never fail a PR; only the lines the PR actually touched do. The README of the nark repo has the full PR-mode snippet.

Month 2

Long-tail coverage push

Long-tail package coverage push. We onboard the packages your team uses that aren't yet in the Nark corpus. Rescan at the end of the month with expanded coverage.

Month 3

Enable PR-gate block mode

Enable PR-gate block mode. New violations are blocked at merge, and historical violations are tracked as a burndown.

Phase 1: the workflow YAML

Drop this into .github/workflows/nark.yml. Three lines matter; the rest is boilerplate.

name: Nark on: push: branches: [main] pull_request: branches: [main] jobs: nark: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v2 with: { version: 9 } - uses: actions/setup-node@v4 with: { node-version: '20' } - run: pnpm install - run: pnpm prisma generate # if you use prisma - name: Run nark id: nark continue-on-error: true # ← Phase 1: shadow mode run: | npx -y nark@latest \ --tsconfig apps/web/tsconfig.json \ --output nark-audit.json env: NODE_OPTIONS: '--max-old-space-size=8192' # ← prevents OOM NARK_TELEMETRY: 'false' - name: Verify nark produced an audit if: steps.nark.outcome == 'success' || steps.nark.outcome == 'failure' run: | test -f nark-audit.json || { echo "::error::Nark did not produce nark-audit.json" exit 1 } - uses: actions/upload-artifact@v4 if: always() with: name: nark-audit path: nark-audit.json retention-days: 30

The three non-boilerplate lines:

  • continue-on-error: true on the scan step — this is what makes it Phase 1 instead of Phase 3. Remove it to flip the switch later.
  • NODE_OPTIONS: --max-old-space-size=8192 — GitHub-hosted runners default Node old-gen heap to ~2 GB, which is too small for mid-sized TypeScript projects. The symptom is FATAL ERROR: Reached heap limit and exit code 134. We hit it on saas; you will too. Skip this only on a small project.
  • The Verify nark produced an audit step — without it, an OOM or crash leaves nark-audit.json missing, upload-artifact emits a warning instead of failing, and continue-on-error: true reports the whole job as green. The verify step makes silent-success impossible.

Same setup covers full-repo scans and PR-only scans. Use nark scan for the baseline, and nark scan --diff origin/main in the PR-blocking workflow step.

What "baseline" looks like

Our first Nark run on the nark.sh SaaS — a Next.js + Prisma + Stripe + Clerk codebase, ~500 TS files — surfaced 137 violations: 94 errors, 43 warnings.

PackageFindingsNotes
next70Framework error-handling is implicit (try/catch at the route layer). Mostly suppressible with a single pattern entry.
undici39Server-side fetch() is backed by undici under the hood. Same framework-handling story as next.
stripe9Real targets. Stripe errors aren't optional — these get fixed, not suppressed.
date-fns9Likely profile tightening needed. Filed upstream.
others10Long tail across 6 packages. Mixed TP/FP.

The distribution above is typical for a Next.js + Prisma codebase. The shape will be different for an Express + Postgres stack, but the principle holds: two packages dominate, and those two are usually the framework layer where errors are handled implicitly. Triage the dominant packages first with pattern-level suppression; the long tail is faster to review per-instance.

Why this beats "just turn it on and block"

Two failure modes we've watched teams hit when they skip Phase 1:

  • The wall-of-red revert. Day-one merge gets blocked by 137 unfixable-in-the-PR findings. The team rips the scanner out within 48 hours. Net effect: no scanner at all.
  • The blanket suppression. Team patches around the gate by nark-ignore-all-style suppressions just to land the next PR. Net effect: scanner present but lying — looks like coverage, actually ignores everything.

Phase 1 gives the team a week to look at the findings without pressure. Phase 2 produces a justified baseline (every suppression has a reason field someone wrote). Phase 3 is then enforceable because the baseline is real.

See also