whatleaks 0.1.0 is out: open source, MIT, on npmwhatleaks 0.1.0 is out →
Docs · v0.1

Everything whatleaks does, on one page.

Run a command through it, read what it sent, test whether an opt-out works. Needs Node ≥ 22.12 on macOS or Linux. Nothing is installed in your system trust store, and no system proxy setting changes.

Quickstart

Put npx whatleaks run -- in front of any command:

$ npx whatleaks run -- npm run build          # what does my build send?
$ npx whatleaks compare -- ./node_modules/.bin/next build   # does opting out work?

The command runs as usual. When it exits, whatleaks prints a report: where the traffic went, how often, which fields each payload carried, and whether any of it is about you. The raw capture is saved in the current folder as whatleaks-<label>-<time>.har, readable only by you.

Start with compare when the question is "does the opt-out work?" It runs the command twice in fresh processes, defaults first, and shows exactly what is still sent.

Install

$ npx whatleaks run -- <command>     # no install
$ npm install -g whatleaks           # the whatleaks command

It needs Node 22.12 or later and runs on macOS and Linux. The install pulls about 195 packages (41 MB), mostly the proxy library, mockttp.

Recent npm versions may say they skipped an install script for tls-impersonate. Nothing needs approving: mockttp loads that native module only to mirror TLS fingerprints, which whatleaks never turns on, and it ships prebuilt binaries anyway.

run

$ whatleaks run [options] -- <command> [args...]

Captures one run and prints a report. The app runs with its normal output; whatleaks writes its own notes to stderr.

$ whatleaks run -- npm run build
$ whatleaks run --duration 60 -- some-dev-server     # long-running apps: stop after 60 s
$ whatleaks run -e NODE_ENV=production -- node app.js
$ whatleaks run --md report.md -- ./my-tool         # also write a Markdown report

After the app exits, whatleaks waits up to 10 s (--linger) for processes it left running, and keeps recording for at least 1 s (--grace) for helpers that start sending a moment later. Some tools send telemetry from a detached helper after they exit.

compare

$ whatleaks compare [options] -- <command> [args...]

Runs the command twice, in fresh processes, and diffs the two captures:

  1. Defaults. Every known opt-out variable is removed from the environment.
  2. Opted out. All 32 documented opt-out variables are set, or only the ones you name.
$ whatleaks compare -- ./node_modules/.bin/next build
# test one switch, and fail CI if telemetry is still sent
$ whatleaks compare --no-known-opt-outs --off-env DO_NOT_TRACK=1 --strict -- ./node_modules/.bin/turbo run build

The diff has these parts: still sending after opt-out, stopped, other traffic (not identified as telemetry; check it yourself), still sent about you, and persistent identifiers: values that were the same in both runs, so the app stored them or built them in, and they can link your runs together.

Defaults run first. Events a tool sends only on first use can make a "stopped" result look better than it is. A "still sending" result is always a direct observation. Repeat a comparison before trusting a stop.

Tools whose opt-out is a setting

When the switch is in the tool's settings rather than an environment variable, make two captures and diff them:

$ whatleaks run --label on -- my-tool
# turn telemetry off in the tool's settings
$ whatleaks run --label off -- my-tool
$ whatleaks diff whatleaks-on-*.har whatleaks-off-*.har

report and diff

$ whatleaks report <file.har>                # analyse a HAR from whatleaks or another tool
$ whatleaks diff <before.har> <after.har>    # what is still sent in the second

report reads whatleaks captures and browser-devtools-style and mitmproxy-style HAR, including their WebSocket messages. It was tested with hand-made samples, not real exports from Proxyman, Charles or HTTP Toolkit. Both commands take --md, --json and --raw; diff also takes --strict.

The 32 opt-outs

compare and run --opt-out set every documented opt-out variable below. whatleaks opt-outs --json lists each one with its source.

VariableTools
DO_NOT_TRACK=1Bun, Turborepo, Nuxt, Netlify CLI, Wrangler, GitHub CLI, Hugging Face Hub, Prisma CLI 8 (a cross-tool convention; only tools that implement it honour it)
NEXT_TELEMETRY_DISABLED=1Next.js
ASTRO_TELEMETRY_DISABLED=1Astro
NUXT_TELEMETRY_DISABLED=1Nuxt
GATSBY_TELEMETRY_DISABLED=1Gatsby
NG_CLI_ANALYTICS=falseAngular CLI
TURBO_TELEMETRY_DISABLED=1Turborepo
VERCEL_TELEMETRY_DISABLED=1Vercel CLI
CHECKPOINT_DISABLE=1Prisma 7, Terraform, Packer (also turns off version checks)
PRISMA_DISABLE_TELEMETRY=1Prisma CLI 8
STORYBOOK_DISABLE_TELEMETRY=trueStorybook
EXPO_NO_TELEMETRY=1Expo CLI
NETLIFY_TELEMETRY_DISABLED=1Netlify CLI ≥27.13
WRANGLER_SEND_METRICS=falseWrangler
YARN_ENABLE_TELEMETRY=0Yarn Berry
HOMEBREW_NO_ANALYTICS=1Homebrew
DOTNET_CLI_TELEMETRY_OPTOUT=1.NET CLI
AZURE_CORE_COLLECT_TELEMETRY=falseAzure CLI
SAM_CLI_TELEMETRY=0AWS SAM CLI
CDK_DISABLE_CLI_TELEMETRY=trueAWS CDK CLI
CLOUDSDK_CORE_DISABLE_USAGE_REPORTING=truegcloud
VAGRANT_CHECKPOINT_DISABLE=1Vagrant (also turns off version checks)
GH_TELEMETRY=0GitHub CLI
DISABLE_TELEMETRY=1Claude Code
DISABLE_ERROR_REPORTING=1Claude Code (error reports)
DENO_NO_UPDATE_CHECK=1Deno (update check, not telemetry)
HF_HUB_DISABLE_TELEMETRY=1Hugging Face Hub
GRADIO_ANALYTICS_ENABLED=FalseGradio
STREAMLIT_BROWSER_GATHER_USAGE_STATS=falseStreamlit
WANDB_ERROR_REPORTING=falseWeights & Biases
SF_DISABLE_TELEMETRY=trueSalesforce CLI
SLS_TELEMETRY_DISABLED=1Serverless Framework v3

Options

run and compare share these. report and diff take the output ones.

--md <file>, --json <file>
Also write the report (for compare and diff, the diff) as Markdown or JSON. The Markdown has every field and full payload samples.
--samples <n>
Payload samples per endpoint (default 2).
-d, --duration <s>
Stop the app after this many seconds, together with the processes whatleaks tracked for it.
--linger <s>
After the app exits, wait up to this long for processes it left running (default 10).
--grace <s>
After the app exits, keep recording at least this long (default 1), for helpers that start sending a moment later.
--passthrough <host>
Tunnel a host without decrypting it, e.g. *.mybank.com. It is still counted. Repeatable.
-e, --env <KEY=VALUE>
Extra environment variable for the app. Repeatable.
-l, --label <name>
run: name for this capture (default run), used in the HAR file name.
-o, --out <file>
run: where to write the HAR capture.
--opt-out
run: set every documented opt-out variable.
--off-env <KEY=VALUE>
compare: an extra variable that opts out, for the second run. Repeatable.
--no-known-opt-outs
compare: use only --off-env, not the documented list.
--out-dir <dir>
compare: where to write both HAR captures (default .).
--strict
compare and diff: exit 1 if a known telemetry endpoint from the catalog is still contacted in the second run, or if the second run did not exit cleanly.
--raw
Turn off redaction (see below).
--no-watch
Do not look for connections that bypass the proxy.
--no-preload
Do not load the Node, Python and Ruby hooks into the app.
--no-color
Plain output.

Reading a report

The terminal report opens with a summary line, an activity strip over the run's duration, then:

  • What it sent about you. This machine's identifiers found in any payload, how they were encoded and where they appeared, and ids repeated across requests.
  • Where it went. Each destination with its category, request count and bytes sent.
  • Endpoints. Requests grouped by endpoint, with path ids templated (/users/123 → /users/:id), timing, status, every field and redacted samples.
  • Not visible. Hosts that refused the CA, counted by name, and connections that bypassed the proxy, by address and process.

Endpoints are classified from a catalog of 69 known telemetry hosts, each with a source link. Anything else gets a URL keyword guess, always marked ?. Headlines and --strict count only catalog hosts; guesses are listed separately, as "possibly telemetry".

When a payload carries something about you, the report says what, how it was encoded and where. This line is from the test suite's synthetic capture:

  ● A MAC address of this machine as sha256 — 3 requests to api.segment.io
      in batch[].context.device.id

Redaction and HAR files

Reports are meant to be shared, so by default:

  • this machine's identifiers become labels (‹home›, ‹sha256(mac)›, …);
  • secret-looking headers, fields and query parameters become ‹redacted›;
  • emails become ‹email›;
  • id-shaped values (install ids, session ids, tokens) become per-report aliases (‹id-1›). The same value always gets the same alias, so you can still follow it;
  • control characters are neutralised, so a payload cannot inject terminal escapes.

--raw turns this off. Redaction is pattern-based, so read a report before you post it.

Share the report, not the HAR. Every run saves its capture as a HAR file that only you can read. It holds the raw traffic and is never redacted.

How it works

A CA for one run

Each run generates a fresh certificate authority. Its private key stays in memory. Its certificate is written to a private temp folder that is deleted when the run ends.

A loopback proxy

The proxy is built on mockttp and listens on 127.0.0.1 only. mockttp would listen on every interface by default; whatleaks forces loopback, and a test checks it.

Standard environment variables, plus hooks

  • Proxy: HTTP_PROXY and HTTPS_PROXY in both cases, and NODE_USE_ENV_PROXY=1.
  • CA: NODE_EXTRA_CA_CERTS, SSL_CERT_FILE, REQUESTS_CA_BUNDLE, CURL_CA_BUNDLE, GIT_SSL_CAINFO, DENO_CERT.
  • Java: JAVA_TOOL_OPTIONS with the proxy and a truststore file whatleaks writes itself; the JVM ignores the variables above.
  • Node: a small preload (NODE_OPTIONS=--require). On Node older than 22.21 it routes http, https and fetch through the proxy. On newer Node it gives agents the app creates itself the proxy settings. On every Node it tunnels node:http2, which reads no proxy variables.
  • Python (PYTHONPATH) and Ruby (RUBYOPT): small hooks for builds that ignore SSL_CERT_FILE, such as Apple's /usr/bin/python3 and the macOS system Ruby. The Python hook also makes aiohttp honour the proxy variables.
  • All three hooks report each direct TCP connection and UDP destination as it opens. --no-preload turns them off.

Trust reaches only the app's own process tree. Nothing is installed in your system's trust store, and no system proxy settings change.

The process tree

whatleaks follows the processes the app starts, identified by pid and start time. It also adopts any process connected to the proxy, which catches detached helpers whose parent already exited. A process re-parented to launchd that started during the run, in the app's folder, is treated as a likely helper: whatleaks waits for it but never signals it unless it connects to the proxy. Everything stops on --duration, on Ctrl-C, or when the wait ends.

Traffic that is not visible

An app that refuses the CA, such as one that pins certificates, fails that request. whatleaks lists the host by name, with a count. It does not try to get around pinning. Connections that ignore the proxy are listed by address and process:

  • macOS: netstat -anvW every 20 ms while the app is busy and every 100 ms when it idles, plus a closing sweep for connections that already closed.
  • Linux: /proc at the same rates, so no ps, lsof or netstat is needed.
  • Node, Python and Ruby report each direct socket themselves as it opens, with the hostname the app asked for.

Analysis

JSON, NDJSON, form and binary bodies are decoded and every field is inventoried. Payloads are searched for this machine's identifiers, raw and as md5, sha1 and sha256 hex and base64. The local values are read into memory only and never written.

Platforms and limits

Measured limits are in the benchmark.

  • Platforms. macOS and Linux are benchmarked; Linux is also checked in a slim container with no ps, lsof or netstat. Windows is untested and has no process-watching backend.
  • Only processes you launch through whatleaks. GUI apps, Electron apps, browsers and system daemons were not tested.
  • Apps that refuse the CA can be named, not read. Go programs on macOS (they read only the system keychain) and pinned clients refuse it.
  • Connections that bypass the proxy are named, not read. From Node, Python and Ruby each one is reported as it opens, including UDP sent with sendto(). From Go, Rust or C they are found by polling: one reset within about 10 ms is often missed (Go in the lab: caught 10 to 30% of the time), and UDP sent with sendto() is never seen. On Linux a closed socket has no owner, so only open ones count.
  • Not covered by the Node preload: the undici npm package with its own dispatcher, and node:http2 over plain-text http: (h2c). Their connections are named as direct connections.
  • Late helpers outside the project folder. One that runs from / or your home folder and first connects more than --grace seconds after the app exits is missed. Raise --grace for tools like that.
  • Node 22 prints a warning (UNDICI-EHPA) when the app starts: that is Node's built-in proxy support, which whatleaks turns on.
  • Uploads over 8 MiB are counted and marked cut off. The first MiB is kept and scanned.
  • Exact matching only. Identity matching cannot see salted hashes. Redaction and classification are pattern-based.
  • Next.js on Node 24 and 26. Next.js 16.4.2's telemetry fails inside the process on Node 24.22 and 26.0, with or without whatleaks, so you will see none there.

Exit codes

CommandExit code
runThe app's own exit code; 0 when --duration stopped it.
compare, diff0, or 1 with --strict when a known telemetry endpoint is still contacted in the second run or that run did not exit cleanly.
any2 for a usage or input error, 1 for a system error such as a full disk.

From source

$ git clone https://github.com/devanshranjan10/whatleaks && cd whatleaks
$ npm ci && npm run build
$ node dist/cli.js --help
$ npm run check     # typecheck, lint, tests (offline; integration tests use local servers only)

CI builds and tests every commit on Ubuntu and macOS with Node 22.12.0, 22, 24 and 26, then installs the packed package into an empty project and captures a real request. Design notes, the decision log and progress notes are in docs/.