breakhound StarGitHub stars

Zero config bug finder for JavaScript and TypeScript

Find bugs in any JavaScript library automatically

Point breakhound at an npm package or a local folder. It reads the TypeScript types, throws nasty edge cases at every exported function, runs each call in an isolated process under several time zones, and hands you a minimal reproduction plus a ready Vitest test for every bug. No tests to write first.

Terminal
npx breakhound ./my-lib

Born from 130 merged bug fixes atGoogleMicrosoftGitHubVue

Sniff, the breakhound bloodhound detective, holding a magnifying glass
Eight oracles

The bugs it sniffs out

Each oracle is an invariant that almost every library should keep. breakhound checks all eight on every exported function, with no configuration.

Split emoji

lone-surrogate-output

A string result contains half of a surrogate pair, so an emoji or astral character is cut in two.

truncate('\u{1F600}', 1) -> '\ud83d'

Time zone dependence

timezone-dependent

The same call returns different results when only the host TZ changes.

TZ=UTC yearLabel(0) -> '1970' TZ=New_York yearLabel(0) -> '1969'

Crash on valid input

unexpected-crash

A value the types allow, like an empty string, throws a TypeError.

initials('') -> throws TypeError

NaN leak

nan-leak

Finite numbers go in, NaN comes out where a real number was expected.

average([0]) -> NaN

Broken roundtrip

roundtrip

Partners such as encodeX and decodeX do not give the input back.

decodeBase(encodeBase('_')) -> ' '

Not idempotent

not-idempotent

Normalizing twice gives a different result than normalizing once.

slugify('x y') -> 'x_y' slugify(slugify('x y')) -> 'x-y'

Lost minus sign

negative-zero-sign

Values between -1 and 0 are formatted as if they were positive.

formatFixed(-0.5) -> '0.50'

Nondeterminism

nondeterministic

Same call, same input, different result, from hidden state or clocks.

label('a') -> 'a' label('a') -> 'A'
How it works

From package name to failing test in four steps

Everything below happens inside one command. You keep only the part you care about: the tests.

  1. Read the types

    breakhound installs the package with --ignore-scripts or opens your folder, then reads its TypeScript declarations to learn every exported function and parameter.

  2. Generate inputs

    Each parameter gets values from edgegoat: DST transitions, dates before 1970, emoji, lone surrogates, -0, NaN. Callbacks are generated too.

  3. Run in isolation

    Calls run in a child process with a timeout, repeated under several TZ values, with a fixed seed so every run is reproducible.

  4. Check and report

    Eight oracles judge the results. Each finding comes with a severity, a minimal reproduction and a Vitest test.

Real output

Every finding is a test you can keep

This is real --plain output from the buggy fixture in the breakhound repository. The reproduction runs on its own, and the Vitest test fails until the bug is fixed.

Terminal output
[negative-zero-sign] formatFixed drops the minus sign of numbers between -1 and 0 (confidence medium)
  formatFixed(-0.5)  -> '0.50'
  formatFixed(0.5)  -> '0.50'
  formatFixed(-1.5)  -> '-1.50'

Reproduction repro.js

repro.js
// formatFixed drops the minus sign of numbers between -1 and 0
// Found by breakhound. Package: buggy-lib@1.0.0
import { formatFixed } from 'buggy-lib';

console.log(formatFixed(-0.5));
console.log(formatFixed(0.5));
console.log(formatFixed(-1.5));

Generated test Vitest

formatFixed.test.ts
import { expect, test } from 'vitest';
import { formatFixed } from 'buggy-lib';

test("formatFixed keeps the sign of -0.5", () => {
  expect(formatFixed(-0.5)).not.toBe(formatFixed(0.5));
});

Reproduction repro.js

repro.js
// yearLabel returns different results in different time zones
// Found by breakhound. Package: buggy-lib@1.0.0
import { yearLabel } from 'buggy-lib';

for (const tz of ['UTC', 'America/Sao_Paulo']) {
  process.env.TZ = tz;
  console.log(tz, yearLabel(0));
}

Generated test Vitest

yearLabel.test.ts
import { expect, test } from 'vitest';
import { yearLabel } from 'buggy-lib';

test("yearLabel does not depend on the host time zone", () => {
  const original = process.env.TZ;
  try {
    process.env.TZ = 'UTC';
    const expected = yearLabel(0);
    process.env.TZ = 'America/Sao_Paulo';
    expect(yearLabel(0)).toEqual(expected);
  } finally {
    if (original === undefined) delete process.env.TZ;
    else process.env.TZ = original;
  }
});

Output modes: the rich TTY view by default, --plain for logs, --json or --output report.json for tools, --log run.jsonl for a JSON Lines run log and --verbose for debug lines.

Continuous integration

Gate pull requests on real bugs

Add one step to GitHub Actions. --fail-on high fails the job on serious findings, and breakhound writes annotations and a job summary on its own when it runs inside Actions. Read the CI guide.

.github/workflows/breakhound.yml
name: breakhound
on: [push, pull_request]

jobs:
  sniff:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci --ignore-scripts
      # annotations and a job summary are written automatically
      - run: npx breakhound . --fail-on high
Comparison

Where breakhound fits

breakhound complements these tools rather than replacing them. A good workflow: a free sweep with breakhound first, important properties in fast-check, parsers in Jazzer.js.

AspectbreakhoundTests by handfast-checkJazzer.js
ApproachType driven invariant checksExamples you think ofProperty based testing libraryCoverage guided fuzzing
You writeNothingEvery case and assertionProperties and arbitrariesFuzz targets
Input sourceTypes plus edgegoat edge casesYour imaginationYour arbitraries, with shrinkingMutation guided by coverage
Best atUnicode, time zone and number formatting bugs in public APIsDocumenting intended behaviorPrecise properties of your own codeCrashes and security issues in parsers
Works on code you never readYesNoNoPartly, after writing a target
OutputReproduction plus ready Vitest testPass or failFailing counterexampleCrashing input
FAQ

Questions

Run npx breakhound with a package name or a path. breakhound reads the TypeScript types to learn what each exported function accepts, feeds it edge case inputs, and reports calls that break invariants such as roundtrips, idempotence, sign preservation or time zone independence. You do not need to write any test first.

Let Sniff hunt your library

One command, no config, no tests to write. You get the bugs, the reproductions and the Vitest tests.