Back to glossary

Telemetry

Software telemetry is code that sends usage and performance data back to its developers. It uses the same mechanism as data exfiltration.

Also known as Software telemetry, Usage analytics, Phone-home code, Product analytics

Software telemetry, also called usage analytics or phone-home code, is code that collects data about how a program is used and sends it to a server its developers control. In open source packages it is often on by default and can run at install time. Telemetry and data exfiltration share one mechanism; consent and payload content separate them.

The mechanism is ordinary: read some local state, serialize it, make an HTTPS request. What makes telemetry a supply chain topic is that the code arrives inside a dependency, runs with your user's privileges, and decides for itself what "local state" means. Our position is simple. Telemetry in a dependency should be opt-in, should never run at install time, and should be judged by what its payload contains, not by what the code calls itself. Malware authors label their exfiltration code trackingData and trackKeyDerivation, and we have the payloads to prove it.

What is software telemetry?

#

Software telemetry is data a program reports about its own use and behavior back to its maintainers. The usual contents are counters (which commands ran, how often), environment facts (OS, CPU, runtime version, whether it ran in CI), timings, and crash signatures. It exists because maintainers are otherwise blind. Bug reports only arrive when something visibly breaks, and most users never file one.

Russ Cox made the strongest case for open source telemetry in his 2023 transparent telemetry series, arguing that "the choice between invasive tracking and doing nothing at all is a false dichotomy, and it's harming open source." His lead example was a macOS bug in the go command that "no one reported" over three years of releases, because users assumed the behavior was normal. The design he proposed used counters instead of traces, no stable identifiers, sampled uploads, and fully public raw data. After community feedback, Go switched the design to opt-in before it shipped.

As of Go 1.23, the Go toolchain defaults to local mode, which writes counters under os.UserConfigDir()/go/telemetry and never uploads them. Uploading requires go telemetry on. The cost of opt-in is a small sample. In September 2024 the Go team reported that after prompting 5% of VS Code Go users, uploads had grown to "around 1800 weekly participants." The Go team says that sample still surfaced real crashes. A smaller sample is the honest price of asking first.

Most JavaScript tooling made the other choice. Next.js, Astro, Gatsby, Storybook, Turborepo, and Prisma all collect by default. Several print a one-time notice instead of asking. A notice is not consent when the command runs in a CI job nobody reads.

How does telemetry get into your dependency tree?

#

Telemetry enters a project at three layers: the package manager, the frameworks and CLIs you invoke directly, and libraries you never chose, pulled in as a transitive dependency. Each layer collects different data and needs a different control.

The package manager reports on every request

#

Package managers send environment details to the registry on every metadata request. We pointed npm 10.9.9, pnpm 10.34.9, and pip 26.2.1 at a local HTTP server that logs request headers, then ran an install. This is what the registry sees:

Plain text
# npm install left-pad, with CI=true and GITHUB_ACTIONS=true set
user-agent: npm/10.9.9 node/v22.23.4 linux x64 workspaces/false ci/github-actions
npm-auth-type: web
npm-command: install

# pnpm add left-pad
user-agent: pnpm/10.34.9 npm/? node/v22.23.4 linux x64

# pip download requests, with CI=true set
User-Agent: pip/26.2.1 {"ci":true,"cpu":"x86_64","distro":{"id":"noble",
  "libc":{"lib":"glibc","version":"2.39"},"name":"Ubuntu","version":"24.04"},
  "implementation":{"name":"CPython","version":"3.13.18"},
  "installer":{"name":"pip","version":"26.2.1"},
  "openssl_version":"OpenSSL 3.5.9 29 Sep 2026","python":"3.13.18",
  "rustc_version":"1.97.0","setuptools_version":"84.0.0",
  "system":{"name":"Linux","release":"6.18.44-fc-v310"}}

pip's user agent is a JSON telemetry record. To fill in rustc_version, pip spawns rustc --version with a 0.5 second timeout whenever rustc is on the PATH. The ci field is set if any of four environment variables exists (BUILD_BUILDID, BUILD_ID, CI, PIP_IS_CI), and pip's source says the list exists "to provide some indication of and lower bound for CI traffic to PyPI." PyPI's Linehaul project streams these download logs into the public bigquery-public-data.pypi.file_downloads dataset.

None of this is secret or malicious. It is coarse, documented, and goes only to the registry you already trust with your dependency list. npm lets you override the string with its user-agent config setting, but it still sends npm-command, and pip has no switch to drop its fields. That is a useful baseline: your installs are never fully silent, even before any package code runs.

Install-time telemetry in a library you never chose

#

The more interesting layer is a library that starts collecting the moment it lands in node_modules. @mui/x-telemetry is a good case study because it is legitimate, documented, and pulled in transitively: @mui/x-data-grid-pro depends on @mui/x-license, which depends on @mui/x-telemetry. Version 9.17.0 declares "postinstall": "node ./postinstall/index.mjs".

We installed it with npm 10 inside a scratch Git repository whose origin remote was https://github.com/acme/secret-internal-app.git. The install script ran git config --local --get remote.origin.url and git rev-parse --show-toplevel, read the machine ID (via node-machine-id, then /etc/machine-id), hashed each value, and wrote the result into the package directory:

JavaScript
export default {
  "config": { "isInitialized": true },
  "traits": {
    "isDocker": false,
    "isCI": false,
    "machineId": "87d696c166932a34f2557ca3ce5f97d7a75f0b1e1bf494033120a8e5349275aa",
    "repoHash": "4ed443f0550b8b6c41e10de633058d53c1d0f48a21a3033fb6fe9d900b1d2e00",
    "postinstallPackageNameHash": "19d0d46380da5c4253d05453878d9c4119d9c29b946e713f187404462a94437b",
    "rootPathHash": "0108d3c23da46724d516d1f0d97f0cb175e9c8074b142d137293140c902a07a3",
    "projectId": "4ed443f0550b8b6c41e10de633058d53c1d0f48a21a3033fb6fe9d900b1d2e00",
    "sessionId": "ce90f9c4b0695d6abf2515781949fd16cbd95471560a6ba4890670fe89e5f9c2",
    "anonymousId": "66ad812682c5315ef7cd2c0d4c60e8ff640160ed8493d603bf12b6c57c3ac6f0"
  }
};

The repoHash is a plain, unsalted SHA-256 of the remote URL. Anyone can recompute it:

Bash
$ printf %s https://github.com/acme/secret-internal-app.git | sha256sum
4ed443f0550b8b6c41e10de633058d53c1d0f48a21a3033fb6fe9d900b1d2e00  -

Nothing leaves the machine at install time. Transmission happens later, in the browser, in development builds only. When a Pro or Premium component verifies its license, the runtime merges these traits with a FingerprintJS browser fingerprint (fonts, canvas, audio, video card, and more) and the license key, then POSTs the event to https://x-telemetry.mui.com/v2/telemetry/record. MUI's telemetry guide documents these fields, calls the data "pseudonymous," and says the purpose includes license compliance. The runtime skips sending in CI, in production builds, and when MUI_X_TELEMETRY_DISABLED=true is set.

Two details matter for anyone writing a policy. First, the opt-out variable gates transmission only. We set MUI_X_TELEMETRY_DISABLED=true and reinstalled; the postinstall still ran Git, read the machine ID, and wrote context.mjs and ~/.config/mui-x/config.json. Second, an unsalted hash of a guessable string is a pseudonym, not anonymization. Repository URLs are guessable, and public ones are enumerable.

1. Install time

postinstall, runs on every npm install before npm v12

  • git config --local --get remote.origin.url
  • git rev-parse --show-toplevel
  • Machine ID: node-machine-id, then /etc/machine-id
  • Unsalted SHA-256 of each value, written to context.mjs
  • A random anonymousId saved to ~/.config/mui-x/config.json

NetworkNothing sent. MUI_X_TELEMETRY_DISABLED=true does not stop this phase.

2. Development runtime

In the browser, when a Pro or Premium component checks its license

  • Loads the install-time traits from context.mjs
  • Adds a FingerprintJS browser fingerprint and a session ID
  • Adds the license key and package version
  • POST to x-telemetry.mui.com

Skipped whenCI is detected, NODE_ENV=production, or the opt-out is set.

Figure 1. In @mui/x-telemetry 9.17.0, the opt-out stops sending, not collecting.

Figure 1, in text. At install time, the @mui/x-telemetry postinstall runs git config --local --get remote.origin.url and git rev-parse --show-toplevel, reads the machine ID, computes SHA-256 hashes, and writes context.js, context.mjs, and ~/.config/mui-x/config.json. It sends nothing, and MUI_X_TELEMETRY_DISABLED does not stop it. In a development build running in the browser, a license check triggers the runtime, which adds a FingerprintJS fingerprint, a session ID, and the license key, then POSTs to x-telemetry.mui.com. That second step is skipped in CI, in production builds, and when the opt-out is set.

Not every project hashes the same way, and the difference is the whole point of hashing. We read the project-ID code in three packages:

Package (version checked)Raw value hashedSaltCan the vendor reverse it?
next 16.4.5git config --local --get remote.origin.url16 random bytes per user, stored locally and never sentNo. The source says this makes it "impossible for us to reverse or try to guess the remote."
@astrojs/telemetry 3.4.2First commit SHA from git rev-list --max-parents=0 HEADNoneFor public repos, yes. Root commit SHAs are public.
@mui/x-telemetry 9.17.0Git remote URL, package name, repo root pathNoneFor any URL it can guess, yes.

Verdict: a salted hash with a locally stored, never-transmitted salt is the only one of these that behaves like an anonymous project ID. The other two are stable, linkable identifiers that happen to be written in hexadecimal.

Where does telemetry end and data exfiltration begin?

#

Telemetry becomes data exfiltration when code collects data the user did not agree to share and that the stated purpose does not need. The network call is identical in both cases. The difference lives in three places: what the code reads, when it runs, and whether the user had a real chance to say no.

Our research team has documented the far end of this spectrum many times. In April 2024 we analyzed React ReExt, a component library that peaked near 11,000 downloads in one October 2023 week. Its preinstall.js read ~/.gitconfig and sent the OS username, Git name, and Git email as query parameters to 2tak.l.serverhost.name:1962. Asked about it, the author told us "we do gather developer information, but only on developer consent." The versions we examined showed no consent prompt.

In May 2025 we found 60 malicious packages on npm in a host-fingerprinting campaign across three accounts with more than 3,000 combined downloads. Every one ran the same install-time script, and the author called the payload what it pretended to be:

JavaScript
const trackingData = JSON.stringify({            // Builds large JSON blob:
     package, directory: __dirname, home_directory: os.homedir(),
     username: os.userInfo().username, dns: dns.getServers(),
     internal_hostname: os.hostname(), internal_ip: getIPAddress(),
     external_ip: ext.ip, external_hostname: ext.hostname, organization: ext.org,
     resolved_url: packageJSON.___resolved, package_version: packageJSON.version,
     package_json: packageJSON, package_type: 'npm'
});

The script skipped AWS, GCP, and known research sandboxes, then posted to a Discord webhook. Real telemetry wants the most representative sample it can get. Code that hides from analysis environments is not measuring usage.

Legitimate tools cross the line too. In February 2025, devenv 1.4 shipped an AI generate command that, in NixOS committer Martin Weinelt's words, "tars up all files it can find through git ls-files -z" and uploads them. The maintainer removed the feature after community backlash. Nothing about the author's intent changed the fact that a repository left the machine without consent.

The hardest case is the Injective SDK compromise in June 2026. @injectivelabs/sdk-ts@1.20.21, a package with about 50,000 weekly downloads, shipped what looked like telemetry: fromMnemonic and fromHex called a new trackKeyDerivation function. That function recorded the full mnemonic or private key material, base64-encoded it, and POSTed it to an Injective-owned testnet endpoint. It ran at call time, not install time, and the destination was the vendor's own infrastructure. The malicious commits came through a hijacked GitHub account with a real contribution history, a variant of maintainer account takeover, and the attacker pinned 17 more @injectivelabs packages to the bad version. Functionally it was an infostealer wearing a telemetry label.

Consented telemetryExfiltration
  • CaseReadsConsentRuns
  • Go toolchainReadsCounters onlyConsentUpload is opt-inRunsWhen you use the toolchain
  • Next.jsReadsCommand, version, machine facts, salted project hashConsentOn by default, env var opt-outRunsOn CLI use
  • @mui/x-telemetryReadsUnsalted repo and machine hashes, browser fingerprint, license keyConsentOn by default, opt-out gates sending onlyRunsHashes at install, sends in dev
  • React ReExtReadsOS username, Git name and emailConsentNone, undisclosedRunspreinstall
  • @injectivelabs/sdk-ts@1.20.21ReadsWallet mnemonics and private keysConsentNone, undisclosedRunsOn key derivation, to the vendor's own domain
Figure 2. The network call is the same in every row. What the code reads and when it runs is what changes.

Figure 2, in text. Five cases ordered from consented telemetry to exfiltration. Go toolchain: counters only, upload opt-in, runs when you use the toolchain. Next.js: command, version, and machine facts with a salted project hash, on by default with an env var opt-out, runs on CLI use. @mui/x-telemetry: unsalted repo and machine hashes plus a browser fingerprint and license key, on by default with an opt-out that gates sending only, hashes computed at install. React ReExt: OS username and Git name and email, no disclosure or consent, runs at preinstall. @injectivelabs/sdk-ts@1.20.21: wallet mnemonics and private keys, no disclosure or consent, runs when keys are derived, sent to the vendor's own domain.

The pattern across these cases gives a test we apply in analysis:

SignalTelemetryData exfiltrationCan it be faked?
Function or variable nametrackEvent, telemetrytrackingData, trackKeyDerivationYes, trivially
DestinationFirst-party, documented endpointWebhook, paste site, or the vendor's own domainYes. Injective used vendor infrastructure.
DisclosureREADME, docs page, first-run noticeNone, or a vague line in the READMEPartly
When it runsFirst use of a CLI or dev serverpreinstall/postinstall, or a hot code pathNo, observable
What it readsCounters, versions, OS family, hashed IDs~/.gitconfig, env vars, keys, DNS servers, home pathsNo, observable
Sandbox evasionNone. Wants a representative sample.Skips cloud and analysis hostsNo, observable

Verdict: ignore names and destinations. Classify by what the code reads and when it executes, because those are the only two properties an author cannot disguise without changing what the code does. This is why we analyze what a package actually does instead of what its metadata or comments claim.

Socket's Telemetry alert (key telemetry, High severity) fires when package code tracks how the package is used. It sits beside separate capability alerts for network access, environment variable access, and install scripts, and the combination is the signal: a telemetry module that also reads environment variables in an install script deserves a human. In our Balanced default policy, Telemetry is set to Monitor. It moved from Block to Monitor on August 28, 2024, as part of a rework of the default policies aimed at reducing alert noise. Known malware stays on Block.

How do you disable telemetry in npm packages?

#

You disable telemetry in npm packages one tool at a time, using each tool's own environment variable or CLI command. No universal switch exists. The DO_NOT_TRACK=1 environment variable is the closest thing to a standard, and most of the JavaScript packages we checked ignore it.

We unpacked current releases and grepped their source for the variables they actually read, as of November 2026:

Package (version checked)Opt-outReads DO_NOT_TRACK?Default
next 16.4.5NEXT_TELEMETRY_DISABLED=1 or next telemetry disableNoOn
astro 7.4.7 (@astrojs/telemetry 3.4.2)ASTRO_TELEMETRY_DISABLED=1 or astro telemetry disableNoOn
@nuxt/telemetry 2.9.1NUXT_TELEMETRY_DISABLED=1YesOff until you accept a prompt
gatsby-telemetry 4.14.0GATSBY_TELEMETRY_DISABLED=1NoOn
storybook 10.6.6STORYBOOK_DISABLE_TELEMETRY=1NoOn
turbo 2.11.10TURBO_TELEMETRY_DISABLED=1 or turbo telemetry disableYes, per its bundled docsOn
Prisma (checkpoint-client 1.1.33)CHECKPOINT_DISABLE=1NoOn
@mui/x-telemetry 9.17.0MUI_X_TELEMETRY_DISABLED=true (framework prefixes also work)NoOn in development; hashes computed at install regardless
huggingface_hub 2.4.0 (PyPI)HF_HUB_DISABLE_TELEMETRY=1YesOn

Verdict: set DO_NOT_TRACK=1 everywhere, because it is free, and then set each tool's own variable, because DO_NOT_TRACK alone silences only three of the nine packages above. Env var opt-outs also stop at the boundary of what the author chose to gate. The MUI postinstall shows that "disabled" can mean "not sent" rather than "not collected."

Install-time collection needs a different control: do not run dependency install scripts. As of npm v12, that is the default. We installed @mui/x-telemetry with npm 12.2.2 and got:

Plain text
npm warn install-scripts 1 package had install scripts blocked because they are not covered by allowScripts:
npm warn install-scripts   @mui/x-telemetry@9.17.0 (postinstall: node ./postinstall/index.mjs)
npm warn install-scripts Run `npm install-scripts ls` to review, or `npm install-scripts approve <pkg>` to allow.

The generated context.mjs stayed at isInitialized: false, with no hashes. npm v12 made dependency lifecycle scripts opt-in in July 2026, and pnpm 10 blocked lifecycle scripts by default in January 2025. Default-on install scripts were a design mistake, and telemetry is one of the quieter things that mistake allowed. On older npm versions, --ignore-scripts or ignore-scripts=true in .npmrc gets the same effect, at the cost of breaking packages that genuinely need a build step.

Blocking scripts does not touch runtime telemetry. Next.js, Astro, and the MUI browser runtime all collect on use, and the Injective stealer ran on a function call. For CI, the reliable backstop is egress control: build jobs that can only reach your registry or package firewall cannot phone home to anyone, whatever the code decides. That breaks anything that downloads binaries during install, so it needs an allowlist, and it does nothing for developer laptops.

The deepest problem with dependency telemetry is that npm install is non-interactive by design. It runs in CI, in Docker builds, in editor background tasks, and increasingly inside coding agents. There is nobody to prompt, so "on by default with a notice" quietly becomes "on." Nuxt's consent prompt works because a human is at the dev server. Go's opt-in works because a human runs go telemetry on once, or answers an editor prompt, and the choice persists.

The package manager is the right place to fix this, and npm v12 already did half of it. Install scripts now need approval. The approval list says which packages have scripts and what command each runs, but not what the script does once it runs. A community RFC proposes a review mode that would surface whether a script makes network calls or reads environment variables. That is the missing half. A dependency that wants to measure you should have to say so in a place the package manager can enforce, and the honest default for code you never chose is that it measures nothing.