Skip to content
ferret.
Bug finding, with the receipt

It finds the bug,
writes the test,
and hands you the fix.

Ferret hunts real defects in your codebase before they ship. Off-by-one, null deref, races, the unhandled reject. For each one you get a failing test that reproduces it and a proposed fix, not a vague warning.

Free on one repo. No card, no trial clock.

src/lib/user-cache.tsferret hunting
function get(id: string): Promise<User> {
const pending = inflight.get(id);
if (pending) return pending;
 
const p = fetchUser(id);
inflight.set(id, p);
return p.then((user) => {
inflight.delete(id); // never runs if p rejects
return user;
});
}

failing test written

poisons the cache on reject

fix ready

clear entry in .finally()

Hiding in the code

The bug is already in your repo. It is just waiting for the right input.

Reviews read the code. Nobody runs the edge case. So the defect ships, sits quiet, and pages someone at the worst possible hour.

more expensive to fix a bug in production than in the branch that introduced it

The math every on-call engineer already knows

~30%

of defects slip past code review because a human is reading, not executing, the code

Reviews catch style. Runtime catches truth.

3:47am

the hour the null deref you shipped on Friday finally pages someone

The bug was in the diff. Nobody ran the edge case.

How Ferret hunts

Three moves, from the whole codebase to a merged fix.

Ferret follows the tunnel end to end. It never stops at a warning.

  1. 01

    Analyze deeply

    Ferret reads the whole tunnel, not the diff in isolation. It builds a model of your call graph, data flow, and the invariants each function quietly assumes, then goes looking for where those assumptions break.

    call graphdata flowinvariants
  2. 02

    Flush out defects

    It corners the real ones: off-by-one, null deref, unhandled rejection, races, and the edge case nobody wrote a test for. No taste-based nits. Every catch is something that would actually fail at runtime.

    off-by-onenull derefunhandled rejectrace
  3. 03

    Hand over test plus fix

    For each defect you get a failing test that reproduces it and a proposed diff that makes it pass. Read it, run it, merge it. The catch comes with the receipt, not a warning label.

    failing testproposed diffone-click apply
Caught

One catch, three parts. Toggle through them.

This is a real defect Ferret finds: an in-flight cache that poisons itself when a fetch fails. Here is the bug, the test it wrote to prove it, and the fix it proposed.

src/lib/user-cache.ts:14unhandled rejection · poisoned cacheseverity: high
const inflight = new Map<string, Promise<User>>();
 
export function createUserCache(fetchUser: (id: string) => Promise<User>) {
return {
get(id: string): Promise<User> {
const pending = inflight.get(id);
if (pending) return pending;
 
const p = fetchUser(id);
inflight.set(id, p);
// BUG: delete runs only if the await resolves. A rejected
// fetch is never cleared, so it is cached and replayed.
return p.then((user) => {
inflight.delete(id);
return user;
});
},
};
}

What Ferret saw

The in-flight map entry is only removed after a successful await. When fetchUser rejects, the rejected promise stays in the map and every future get(id) replays the same failure. One flaky request poisons the id until the process restarts.

Repro

get(id) once while the network blips, then again after it recovers. The second call never retries.

The catch

Everything a catch comes with.

No warning-only findings. Each item here is something Ferret can prove and, usually, fix.

analysis

Deep defect analysis

Ferret traces values across files and functions to find bugs that only appear when three conditions line up. It reasons about what the code does, not what the comments claim.

proof

A failing test per bug

Every catch ships with a test that reproduces it and goes red on your current code. If Ferret can't write a failing test, it does not call it a bug.

✗ test fails on current code

✓ test passes after the fix

no test, no catch

fix

Fixes as diffs

You get a proposed patch, scoped to the defect, in the style of the surrounding code. Apply it as-is or use it as a head start.

edges

Edge-case and race detection

Empty inputs, boundary values, concurrent writes, retries that never resolve. Ferret goes hunting in the corners on-call gets paged from.

ci

CI integration

Run Ferret on every pull request. It comments the catch inline, blocks on new defects if you want, and stays quiet when the burrow is empty.

Set the trap

Drop it in CI. It hunts every pull request.

Add the action and Ferret comments each catch inline with the failing test and the fix. Block on new defects if you want. When the burrow is empty, it stays quiet.

.github/workflows/ferret.yml
# .github/workflows/ferret.yml
name: ferret
on: [pull_request]
 
jobs:
hunt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ferret-dev/hunt@v1
with:
# Fail the check only when Ferret writes a failing test
# that reproduces a new defect on this branch.
fail-on: new-defects
token: ${{ secrets.FERRET_TOKEN }}
# or run it locally before you push
npx ferret hunt --diff main
From the burrow

What teams say after the first catch.

Ferret opened a PR with a failing test for a race in our billing retry loop. We had shipped that code eight months ago. It had been double-charging about one account a week and we could not reproduce it. The test reproduced it on the first run.
DO

Dana Okafor

Staff Engineer, payments

The thing I trust is that it never cries wolf. If Ferret flags something, there is a red test attached. My reviewers stopped arguing about whether a warning was real, because there are no warnings, only reproductions.
MR

Marco Reyes

Eng Lead, platform

Pricing

Free on one repo. Fair on the rest.

Start with a single repo at no cost. Scale to the whole team when you are ready. Self-host when the code cannot leave the building.

save 20%

Free

$0one repo, forever

Point Ferret at a single repo and watch it hunt. No card, no trial clock.

Start hunting
  • One repository
  • Hunts on demand and on pull requests
  • Failing test plus proposed fix per catch
  • TypeScript, JavaScript, Python, Go, Ruby
  • Community support

The whole loop, on one repo, at no cost.

Most teams start here

Team

$40per engineer / month

Every repo, every pull request, the whole team. Billed per active engineer.

Start hunting
  • Unlimited repositories
  • CI checks with inline catches and fix diffs
  • Isolated per-repo sandbox, never trained on
  • Edge-case and race detection
  • Slack and GitHub notifications
  • Priority support

Billed per active engineer. Quiet repos cost nothing.

Enterprise

Customself-host, SSO

Run the whole hunt inside your own network. Your code never crosses the boundary.

Talk to us
  • Everything in Team
  • Self-hosted, air-gapped option
  • SSO and SCIM, SAML
  • Audit logs and role-based access
  • Dedicated support and SLA
  • Security review and DPA

For teams where the code cannot leave the building.

All plans include the full loop: analysis, failing test, proposed fix. See the full comparison.

FAQ

Questions from the trailhead.

Still unsure whether Ferret earns its keep? Start with the free repo and let a catch answer for it. Or ask us anything.

Linters check for patterns and style. Ferret reasons about behavior and proves its claims by writing a test that fails on your real code. If it can't produce a failing test, it stays quiet. You get reproductions, not a wall of warnings you learn to ignore.