Split emoji
lone-surrogate-output
A string result contains half of a surrogate pair, so an emoji or astral character is cut in two.
Zero config bug finder for JavaScript and TypeScript
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.
npx breakhound ./my-lib
pnpm dlx breakhound ./my-lib
yarn dlx breakhound ./my-lib
bunx breakhound ./my-lib
Born from 130 merged bug fixes atGoogleMicrosoftGitHubVue
Each oracle is an invariant that almost every library should keep. breakhound checks all eight on every exported function, with no configuration.
lone-surrogate-output
A string result contains half of a surrogate pair, so an emoji or astral character is cut in two.
timezone-dependent
The same call returns different results when only the host TZ changes.
unexpected-crash
A value the types allow, like an empty string, throws a TypeError.
nan-leak
Finite numbers go in, NaN comes out where a real number was expected.
roundtrip
Partners such as encodeX and decodeX do not give the input back.
not-idempotent
Normalizing twice gives a different result than normalizing once.
negative-zero-sign
Values between -1 and 0 are formatted as if they were positive.
nondeterministic
Same call, same input, different result, from hidden state or clocks.
Everything below happens inside one command. You keep only the part you care about: the tests.
breakhound installs the package with --ignore-scripts or opens your folder, then reads its TypeScript declarations to learn every exported function and parameter.
Each parameter gets values from edgegoat: DST transitions, dates before 1970, emoji, lone surrogates, -0, NaN. Callbacks are generated too.
Calls run in a child process with a timeout, repeated under several TZ values, with a fixed seed so every run is reproducible.
Eight oracles judge the results. Each finding comes with a severity, a minimal reproduction and a Vitest test.
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.
[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
// 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
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
// 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
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.
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.
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
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.
| Aspect | breakhound | Tests by hand | fast-check | Jazzer.js |
|---|---|---|---|---|
| Approach | Type driven invariant checks | Examples you think of | Property based testing library | Coverage guided fuzzing |
| You write | Nothing | Every case and assertion | Properties and arbitraries | Fuzz targets |
| Input source | Types plus edgegoat edge cases | Your imagination | Your arbitraries, with shrinking | Mutation guided by coverage |
| Best at | Unicode, time zone and number formatting bugs in public APIs | Documenting intended behavior | Precise properties of your own code | Crashes and security issues in parsers |
| Works on code you never read | Yes | No | No | Partly, after writing a target |
| Output | Reproduction plus ready Vitest test | Pass or fail | Failing counterexample | Crashing input |
breakhound is built by Alexandre Kohler, who has landed 130 merged bug fixes in projects at Google, Microsoft, GitHub, Vue and more. The oracles come straight from that work.
The year check used UTC instead of the display time zone.
timezone-dependent Mergedgoogle/budoux#1411Surrogate pairs were split when breaking text.
lone-surrogate-output Mergedmicrosoft/vscode-js-debug#2423Surrogate pairs were split in debugger output.
lone-surrogate-output Mergedmozilla/pdf.js#22017Negative numbers above -1 lost their minus sign.
negative-zero-sign Mergedvuejs/core#15618Vue 2.7 components compiled from script setup crashed in the Vue 3 compat build.
vuejs/coremerged bug fixes across open source projects, each one a pattern breakhound now checks for.
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.
Tools such as fast-check ask you to write the properties and generators yourself. breakhound ships the properties, a fixed set of invariants that hold for most libraries, and derives the generators from the type signatures, so it works on code you have never read. When it finds something, it hands you a regular Vitest test you can keep.
breakhound executes the code it scans, so treat it like running the package's own test suite. Installs use --ignore-scripts and every call runs in a child process with a timeout, which is isolation for robustness, not a security sandbox. Scan untrusted packages inside a container or a disposable VM.
It works best with TypeScript declarations, either bundled .d.ts files or DefinitelyTyped. Without types it falls back to a smaller set of generic inputs.
Some invariants, such as time zone dependence, can be intentional for a given function. Every finding has a confidence level and a minimal reproduction so you can judge it in seconds, and --rules, --only and --exclude let you narrow the hunt.
Yes. Inputs come from a seeded generator (seed 42 by default, change it with --seed), the call budget is fixed with --max-calls, and each run is saved to .breakhound/last-run.jsonl and .breakhound/report.json unless you pass --no-save.
One command, no config, no tests to write. You get the bugs, the reproductions and the Vitest tests.