Inspiration

Deleting a secret from a file doesn't delete it. It removes it from your working tree and leaves it in history forever, reachable by anyone who clones the repo.

Tools like gitleaks and trufflehog already scan history, and they're good at it. But they hand you a flat list of a few hundred matches, most of them long dead, with no indication of which ones still work or how long they've been public. That list gets skimmed once and ignored.

We wanted to answer the question that actually drives action: not "does this look like a secret" but "is this key still live, what can it do, and how long has it been sitting in public?"

What it does

You paste a public GitHub URL. RepoHunter mirrors the repo, walks every blob in every commit across every ref, and finds credentials that were ever committed — including ones deleted years ago.

For each unique secret it reports:

  • Whether it is still live, checked against a read-only endpoint for that provider
  • The real scopes it carries, when the provider exposes them
  • How many days it has been public, counted from the commit that introduced it
  • An estimated cost of abuse, as a dollar range rather than a severity label
  • First commit, author, file paths, occurrence count, and whether the file still exists at HEAD

Findings are deduplicated by the secret's value, so one key committed forty times is one finding with forty occurrences rather than forty rows. They're ranked by danger and split into three tiers, with dead and unverifiable findings collapsed by default — which is what makes the output readable.

It also renders a timeline of the repo's history with each secret drawn from its first commit to today or to the commit that removed it, and generates a fix kit: per-provider rotation steps, a scoped git filter-repo command, a .gitignore, and a pre-commit hook.

If the repo has forks, it says so — history rewriting cannot recall what a fork already holds, so the advice changes from "rewrite" to "rotate now, rewriting is pointless."

How we built it

Next.js App Router and TypeScript, with the scan running server-side in a route handler and streaming progress to the browser over Server-Sent Events.

No AI anywhere in the scan path. It's regex, Shannon entropy, and a git walk — deterministic, fast, and defensible. Every finding traces to a commit hash.

The walk is two passes of git plumbing rather than one pass of porcelain:

  1. git log --all --reverse --raw builds a map from blob SHA to every commit and path that content appeared at. Identical content committed repeatedly collapses to a single blob here, which is where most of the speed comes from.
  2. git cat-file --batch streams each unique blob's bytes exactly once, through a single long-lived process, with binary content skipped on a NUL-byte check.

Detection runs prefixed provider patterns first — near-zero false positives — then high-entropy generic strings, which are only reported when at least two independent signals corroborate them.

Validation probes each unique secret in parallel against a read-only endpoint: GitHub token metadata, AWS STS GetCallerIdentity, Stripe balance retrieval, Slack auth.test, model listing for OpenAI and Anthropic, and others. Every probe is side-effect-free by construction. Results stream in after the findings they belong to, so a slow provider never blocks the render.

No database. Results live in memory for an hour and then drop. It deploys as a container on Render.

Challenges we ran into

The deploy target was the biggest one, and it took us longer than it should have. We put it on Vercel, the build succeeded, and every scan failed. Vercel's Node runtime is Lambda-style: no git binary and a read-only filesystem. RepoHunter is a git client, so there is no configuration that fixes this. We deleted and redeployed twice before accepting that the host was wrong rather than the code. It now runs in a container with git installed, the preflight names the host in its error message so the failure can never be ambiguous again, and the README states the requirement up front.

Making the output readable was the second. Our first working version emitted hundreds of rows for a handful of actual secrets, because a key committed on every deploy appears once per commit. Deduplicating by SHA-256 of the value, and keeping occurrences as a nested list, cut the noise by an order of magnitude.

Entropy detection is the highest false-positive risk in the project, which is why we built it last. Testing against a wordlist repository made the failure mode obvious: its contents are credential-shaped on purpose, so every detector fires and every hit is wrong. We added corroboration requirements, path suppression for lockfiles and bundles and vendored trees, and placeholder detection.

Handling other people's credentials safely shaped the code rather than sitting on top of it. Masking happens at the moment of detection, not at render time, so there is no code path where a full value reaches the client, the cache, or a log line.

Accomplishments that we're proud of

It's fast. A 4,316-commit repository scans in about 11 seconds on a free-tier container, and roughly 8 seconds locally — inside our 15-second target. The speed comes from the blob-level deduplication in the index pass, not from cutting corners on coverage: it still walks every ref.

It declines cleanly. Repos over 500 MB or 20,000 commits are refused in about half a second, before any cloning starts, with a message explaining why. A demo that hangs is worse than one that says no.

Every validation probe is read-only. Not by policy, but because each one was chosen to be a documented no-op: identity endpoints, balance reads, model listings. We are handling someone else's credential and nothing we do should be able to charge, send, or delete anything.

"Unknown" is a first-class result. A network timeout is not evidence that a key is dead, and we'd rather say we don't know than guess.

What we learned

Git plumbing is dramatically faster than the porcelain most tools reach for. cat-file --batch through one process, driven by an index built from a single log --raw pass, made the difference between a scan you wait for and one that finishes while you're still reading the header.

Detection is the easy half. The hard half is triage — ranking, deduplication, and hiding the dead findings by default. The scanner that finds the most secrets is not the one that gets acted on.

Knowing your runtime's constraints matters more than knowing your framework's. Our build was green and our app was completely non-functional for the same reason, and nothing in the build log said so.

The number that makes people act is the exposure duration. "Full write scope" is abstract. "Public for 412 days" is not.

What's next for RepoHunter

Broader validation coverage. Ten providers have read-only probes today; the long tail of Twilio, Cloudflare, Datadog, Postmark and others is mostly mechanical.

Organisation-wide scanning, so a security team can point it at every repo they own rather than one at a time, with results that persist beyond the current hour.

CI integration. The pre-commit hook in the fix kit stops the next leak on one developer's machine, but a check that runs on every pull request is what actually holds the line.

Private repos via a GitHub App install — the most requested thing we deliberately left out of scope for the hackathon.

Better cost modelling. The dollar ranges are currently informed estimates per provider and scope; grounding them in observed abuse data would make the most persuasive number in the report a defensible one.

Built With

Share this project:

Updates

Submission history