Skip to content

CLI

The PulsCheck CLI runs static analysis over your source files to catch race-prone patterns at lint time — before they ever hit the browser. It's designed for CI pipelines and complements the runtime detector you get from devMode().

bash
npx pulscheck --version

Commands

pulscheck scan [dir]

Scan a directory for race condition patterns. Default output is human-readable text.

bash
npx pulscheck scan src/

Flags:

FlagDefaultDescription
--format <type>texttext, json, or sarif
--out <file>stdoutWrite output to a file
--severity <level>warningMinimum severity: info, warning, critical
--ignore <glob>Glob pattern to exclude (repeatable)
--quietSuppress progress output

pulscheck ci [dir]

CI mode — defaults to SARIF output and exits with a non-zero code when findings hit a threshold. Suitable for dropping into a GitHub Actions job.

bash
npx pulscheck ci src/ --fail-on critical --out pulscheck.sarif

Additional flags:

FlagDefaultDescription
--fail-on <level>criticalExit 1 if findings at or above this severity exist
--format <type>sarifDefault is SARIF in CI mode

pulscheck help

Print the usage summary:

bash
npx pulscheck help

Static patterns

The CLI runs 9 source-level detectors. Each one is a regex-based rule mapped to one of the runtime detection patterns, with a concrete fix suggestion.

RuleSeverityMaps toWhat it catches
fetch-no-abort-in-effectcriticalafter-teardownfetch() inside useEffect without AbortController
setInterval-no-cleanupwarningafter-teardownsetInterval with no clearInterval in cleanup
setTimeout-in-effect-no-clearwarningafter-teardownsetTimeout inside useEffect with no clearTimeout
state-update-in-thenwarningafter-teardownsetState inside .then() — may update unmounted component
async-onclick-no-guardwarningdouble-triggerAsync onClick without a loading guard — rapid clicks race
concurrent-useQuery-same-tableinfodouble-triggerMultiple useQuery hooks on the same key
supabase-concurrent-queriesinfodouble-triggerConcurrent Supabase queries to the same table
websocket-no-reconnect-handlerinfosequence-gapnew WebSocket() — ordering gaps possible on reconnect
promise-race-no-cancelinfostale-overwritePromise.race without cancelling losing promises

Each finding includes the file, line, matched code, severity, and a fix string.

GitHub Action

A prebuilt Action is available in the action/ directory of this repo. Wire it into your workflow:

yaml
name: PulsCheck

on:
  pull_request:
  push:
    branches: [main]

jobs:
  pulscheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: Qubites/pulscheck/action@main
        with:
          path: src/
          severity: warning
          fail-on: critical

Inputs (defaults read from action/action.yml):

InputDefaultDescription
pathsrcDirectory to scan
severitywarningMinimum severity to report (info, warning, critical)
fail-onnoneSeverity threshold for non-zero exit (none, info, warning, critical)
formattextOutput format for the step summary (text, json, sarif)

The composite action internally runs pulscheck ci ... --format sarif --out pulscheck-results.sarif and uploads that SARIF file to GitHub code scanning via github/codeql-action/upload-sarif@v3. You do not need to wire up the upload yourself — it happens inside the action. To fail the check on findings, set fail-on to warning or critical (it defaults to none, which means the action reports findings but never fails).

Static vs runtime — when to use which

The static CLI catches patterns before they ship. The runtime detector (devMode()) catches bugs as they happen and has access to real timing, real call graphs, and real data. Use both:

LayerStrengthsBlind spots
Static (CLI)Fast, runs in CI, no runtime neededCan't see async timing, can't see dynamic data
Runtime (devMode)Real traces, real bugs, structured findingsOnly finds what you actually execute

Treat the CLI as the first wall of defense and devMode() as the one that catches everything the first wall missed.

PulsCheck — originated by Oliver Nordsve, 2026