CLI tool
cc1a2b/JShunter avatar
cc1a2b/JShunter

JSHunter: reading JavaScript bundles for the endpoints and keys they leak

jshunter is a command-line tool designed for analyzing JavaScript files and extracting endpoints. This tool specializes in identifying sensitive data, such as API endpoints and potential security vulnerabilities, making it an essential resource for and bug bounty hunters and security researchers.

543 stars59 forksGoMIT

At a glance

What is it?
JSHunter is a Go command-line tool that fetches or reads JavaScript files and reports the API endpoints, hidden parameters and hardcoded credentials inside them, with a detection engine that parses the ECMAScript rather than only pattern-matching it. It suits a security engineer or developer auditing a front end before it ships, and the detail to understand first is that the tool now suppresses what it can prove is not a secret, which changes what a finding list means.
Who is it for?
Adopt JSHunter if you are auditing a web front end and want to know what its bundles expose, since the endpoint list and the credential findings come out of one pass over the same files.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 33 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

What the tool is for

JSHunter is described as a command-line tool designed for analyzing JavaScript files and extracting endpoints, and it specialises in identifying sensitive data such as API endpoints and potential security vulnerabilities.

The framing in the README is aimed at a particular audience: security professionals, penetration testers and developers, with the description calling it an essential resource for bug bounty hunters and security researchers. The tagline is a professional JavaScript security analysis tool, with complete endpoint discovery, sensitive data detection and advanced code analysis.

The reason a tool like this exists is straightforward. A modern web front end ships its application logic to the browser, and that logic is a readable artefact. If the bundle contains the base URL of an internal API, a route table, a third-party integration key or a token that was supposed to be server-side, it is in the file the browser downloaded, and it is discoverable by anyone who opens the developer tools.

That makes this a legitimate audit tool as much as a reconnaissance one. A developer running it against their own build before release is checking for the same class of mistake that a researcher finds on someone else's site.

The tool is written in Go, MIT licensed, and built as a single binary with no runtime dependencies beyond the standard library and `golang.org/x/net`. The module requires Go 1.25.0 and the badges state a Go version of 1.25 or newer, with binaries released for Linux, macOS and Windows. The default branch is main, the last push was on 2026-09-20, and the releases are v0.8 from that day, v0.7.6 from 2026-07-07 and v0.7.5 from 2026-05-09.

The structural engine, and why it exists

The most interesting part of the README is the Structural Detection Engine, because it describes a change in what a finding means.

JSHunter parses the JavaScript it scans instead of only pattern-matching it. A single-pass ECMAScript scanner classifies every byte of the response as string literal, template literal, comment, regular-expression literal, or code, and the detection rules are evaluated against that classification.

The stated reason is precise: a regex has no idea what it matched. The same forty base64 characters mean credential inside a string literal, chunk hash inside a minified identifier, and nothing at all when they straddle the seam between two adjacent tokens. Knowing which one it is replaces a pile of proximity heuristics with a structural answer.

Six mechanisms follow from that.

Region gating rejects a match inside a regex literal, since that is a pattern rather than a value. It rejects a match crossing a token boundary, since that is not a single literal. And it rejects a match in code, since that is an identifier fragment.

Binding context recovers the identifier or property key the value is actually bound to, rather than scanning a fixed window of characters for the word key. The example given is that `const stripeSecret = "..."` reads very differently from `{contentHash: "..."}`, and a shape-only rule now requires that binding.

Sibling key-sets recognise the members of a Firebase web config, an Algolia search config, a Segment analytics config and a Supabase browser client by the shape of the object they sit in.

Value shape identifies digests, UUIDs, prose, paths, placeholders, and base64 that decodes to a PNG, a font, a JSON document or an English sentence for what they are.

Exposure classification handles values the issuer publishes on purpose, naming Stripe publishable keys, Mapbox public tokens, Supabase anon JWTs read from their own role claim and Twilio SIDs. Those are classified rather than reported as leaks, and the `--include-public` flag reports them anyway.

File-relative surprisal uses a character-transition model built from the file being scanned to score how unlike the rest of that file a candidate reads.

The safety property is stated just as clearly. Every rejection is conditional on the body being confidently JavaScript or JSON. On anything else, a `.env` file, prose or raw HTML, the engine contributes evidence but never suppresses, so a misclassified input can never hide a secret. The `--no-structural` flag turns the whole layer off and restores the v0.7 behaviour.

That last point is the one that makes the change safe to adopt. An engine that can decide a candidate is not a secret is dangerous unless it is also careful about when it is allowed to decide anything at all.

Reading the evidence, and the audit trail

Findings carry the reasoning as an `evidence` object in the JSON output, the NDJSON output and the SARIF property bag, and this is the part that changes how you read a report.

json
"exposure": "secret",
"evidence": {
  "region": "string-literal",
  "role": "assignment",
  "bound_to": "awsAccessKeyId",
  "shape": "opaque-token",
  "charset": "alphanumeric",
  "signals": [
    {"name": "in-literal", "delta": 0.04, "detail": "value is a complete string-literal"},
    {"name": "credential-binding", "delta": 0.12, "detail": "bound to 'awsAccessKeyId'"}
  ]
}

Each field answers a different question. The region says where in the parsed source the value sits. The role says what the value is doing there. The bound_to field names the identifier or property it is assigned to. The shape classifies the value's form, and the charset narrows that further. The signals array then lists the individual pieces of reasoning with a delta for each, so a reader can see not only what was concluded but which observations contributed.

The deltas are the interesting part. A signal that contributed 0.04 is a weak hint and one that contributed 0.12 is a stronger one, and the fact that they are shown separately means a reviewer can disagree with one of them without discarding the finding.

That matters because the whole point of the structural layer is that it now says no to things as well as yes. A tool that only reports has a false positive problem. A tool that also suppresses has a false negative problem, and the false negative is the one that gets someone hurt. The evidence object is the mechanism for making the suppressions reviewable after the fact.

The `--stats` flag reports what each stage dropped, so the pipeline stays auditable. That is the second half of the same idea. If the engine dropped nine hundred candidates at the region gate and forty at the binding gate, you can see that in the summary rather than having to reconstruct it from the absence of findings.

The three output formats matter too. JSON and NDJSON are for piping into other tools, and SARIF is the standard format for static analysis results, which means a finding can be loaded into a code scanning dashboard alongside everything else your pipeline already reports.

Chunk graphs, and the routes a crawler never sees

The second major feature addresses a problem that has become common enough that it deserves its own section.

A modern application ships one entry bundle and several hundred lazily loaded chunks whose URLs are assembled at runtime from a manifest the bundler inlines. Nothing links to them, so a crawler never sees them.

That is a serious blind spot for this kind of tool. The endpoints and the credentials that matter are often in the lazily loaded parts, the admin screens and the settings pages, and those are exactly the parts a naive fetch of the entry bundle misses.

The `-G` flag recovers the manifest and prints the full asset list and client route table:

console
$ jshunter -u https://target.example/_next/static/chunks/main-a1b2c3.js -G
[CHUNKS] https://target.example/...: next runtime, 214 chunks, 37 routes
[CHUNK]  https://target.example/...  https://target.example/_next/static/chunks/settings.11aa22bb33cc.js
[ROUTE]  https://target.example/...  /admin/users/:id

Look at the route line. `/admin/users/:id` is a client-side route pattern, and it is exactly the sort of thing an endpoint inventory should contain and a scanner of fetched pages never learns, because the route only exists in the bundle.

The supported runtimes are listed: webpack 4 and 5 with `__webpack_require__.u`, `miniCssF` and `jsonpScriptSrc`, Next.js build manifests, Vite and Rollup with `__vite__mapDeps` and `__vitePreload`, plain dynamic `import()`, and route tables from React Router, Vue Router and Angular. That is a broad coverage of what the JavaScript build tooling world actually produces.

The output is tab-separated and deduplicated, so it pipes straight back in as the input list of a follow-up scan:

bash
jshunter -u https://target.example/main.js -G -q | awk -F'\t' '/^\[CHUNK\]/{print $3}' > chunks.txt
jshunter -l chunks.txt -s -j > findings.json

Two things to note in that pipeline. The first command discovers the chunks, and the second scans them, so the discovery step and the analysis step are separate and composable. And the flags differ: `-G` for the graph, `-q` to quiet it, then `-l` to read a file list, `-s` to scan and `-j` for JSON output.

The design is what makes this useful at scale. Discovering a few hundred chunks and then scanning them is a pipeline that fits in two commands, and because the graph output is deduplicated and tab-separated, it composes with anything else that reads lists.

Input methods, output formats and the project layout

The features list covers the operational side of the tool, and it is worth reading as a description of how the tool is meant to be driven.

Input is flexible: URLs, file lists, local files, stdin piping and recursive discovery are all supported. That last one matters for the chunk graph workflow, since a scan that can walk a directory of downloaded bundles handles the case where you have already fetched the assets.

The processing is described as high-performance architecture with multi-threaded concurrent processing and intelligent rate limiting, and the networking suite offers proxy support, custom headers, user-agent rotation and bypass detection.

The core capabilities are the three that the tool is named for: thorough endpoint discovery that automatically extracts URLs, API endpoints and hidden parameters from JavaScript files; advanced security analysis that identifies API keys, JWT tokens, credentials and potential vulnerabilities; and the professional reporting layer.

The repository layout shows a conventional Go project. There is a `cmd/` directory with the CLI entry point and an `internal/` directory for the implementation, a `go.mod` and `go.sum`, a `Makefile`, and a `.golangci.yml` for the linter configuration. There is a `patterns.json` at the root, which is presumably the rule set, and a `RULES.md` documenting it. There is a `.jshunterignore.example`, which tells you the tool supports an ignore file for excluding paths the way a linter does.

The Makefile is worth a look because it documents the project's own quality process. The test target runs the full suite with the race detector, `go test $(PKGS) -race`. The lint target runs golangci-lint. And there is a selftest target that exercises the rule registry against its built-in true-positive and false-positive fixtures:

code
## selftest: exercise the rule registry against its built-in TP/FP fixtures.
selftest:
	go run $(CMD) --self-test

A self-test over both true positives and false positives is exactly what a detector with a suppression layer needs, since the failure mode there is a rule that stops firing or starts firing on the wrong thing.

There is also a CREDITS.md, a CHANGELOG.md and a LICENSE file, and the CI workflow is referenced in the Makefile comment as mirroring the checks it runs, so the local targets and the pipeline are meant to be the same set of checks.

Editorial conclusion

Adopt JSHunter if you are auditing a web front end and want to know what its bundles expose, since the endpoint list and the credential findings come out of one pass over the same files. Do not treat a finding as a confirmed vulnerability without reading the evidence object, because the engine now classifies deliberately public values and suppresses matches it can prove are inside a regex or straddling a token boundary, and check first whether your build bundles are even reachable, because the chunk graph feature exists precisely because a crawler will not see them.

Frequently asked questions

What is JSHunter?

It is a Go command-line tool for JavaScript security analysis and endpoint discovery. It fetches or reads JavaScript files and extracts URLs, API endpoints, hidden parameters, API keys, JWT tokens and credentials, and it is MIT licensed with releases for Linux, macOS and Windows.

How does JSHunter decide whether a match is a real secret?

It parses the JavaScript with a single-pass ECMAScript scanner that classifies every byte as string literal, template literal, comment, regular-expression literal or code, and evaluates the detection rules against that. Matches inside regex literals, matches crossing a token boundary and matches in code are rejected, and the engine recovers the identifier or property key a value is bound to before applying shape-only rules.

What does the exposure classification in JSHunter do?

It classifies values that an issuer publishes on purpose, such as Stripe publishable keys, Mapbox public tokens, Supabase anon JWTs read from their own role claim, and Twilio SIDs, rather than reporting them as leaks. The --include-public flag reports them anyway, and the --stats flag reports what each stage dropped so the pipeline stays auditable.

What does the JSHunter chunk graph feature do?

The -G flag recovers the bundler manifest from an entry bundle and prints the full asset list and client route table, including lazily loaded chunks that nothing links to and that a crawler would never see. It supports webpack 4 and 5, Next.js build manifests, Vite and Rollup, plain dynamic import, and route tables from React Router, Vue Router and Angular, with tab-separated deduplicated output that pipes into a follow-up scan.

How do I run JSHunter's own checks?

The Makefile mirrors the CI checks: `go test $(PKGS) -race` runs the suite with the race detector, `golangci-lint run` lints, and `go run $(CMD) --self-test` exercises the rule registry against the built-in true-positive and false-positive fixtures. The cover target produces a coverage profile and prints the total.

Is JSHunter still being developed?

The repository is not archived and the last push was on 2026-09-20. The most recent release is v0.8 from that day, after v0.7.6 in July and v0.7.5 in May, and the structural detection layer is the notable addition, with a --no-structural flag to restore the v0.7 behaviour.

Official sources

  1. cc1a2b/JShunter on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/cc1a2b-jshunter.svg)](https://hysenlabs.com/projects/cc1a2b-jshunter)

Was this useful?