Skip to main content

On 25 September a Codex user filed an issue with a title that reads like a bug report and lands like an audit finding: a persistent, self-updating background daemon should not be the default. By 12 October it was near the top of Hacker News with 542 points. The complaint was specific. Opening an interactive session started a daemon that outlived every window, copied the whole platform package out of node_modules into the home directory, and launched a detached loop that checked for updates five minutes later and hourly thereafter.

The interesting part is not the daemon. It is what the daemon implies: the version your package manager installed and the version actually executing your code had quietly become different things. So we went and measured the whole category. We audited 22 agent CLIs distributed on npm, accounting for 46,631,891 installs in a single week, and asked one question of each: of the bytes that end up on a developer’s machine, how many can anyone actually read before running them?

TL;DR

  • Across the nine agent CLIs that ship a compiled payload through npm platform packages, the package you type is 0.0202% of the installed bytes. 405,885 bytes of readable launcher, 2,011,015,620 bytes of opaque binary.
  • npm i -g @openai/codex installs 13,206 bytes. The Linux payload is 452,474,258 bytes, a ratio of 34,263 to 1, and the dependency name recorded in your lockfile returns 404 on the registry.
  • OpenAI’s own source sets daemon_auto_start to default_enabled: true at Stage::Stable. Cline’s published postinstall script carries a comment confessing the same architecture, and the same failure.
  • Three of 22 fetch their real payload from outside the registry entirely, so the integrity hash in your lockfile covers the installer and nothing else. JetBrains Junie verifies a SHA-256 when it updates itself and verifies nothing at all when npm first installs it.
  • Two of 22 let an ordinary environment variable redirect the host the agent binary is downloaded from.
  • One computes a SHA-256 of a downloaded archive, prints it to your console, and compares it to nothing.

How we built the population

We started from the npm registry search API across seven agent-tooling queries, which returned 353 candidate packages, and pulled last-week download counts for every one. The long tail is noise: dozens of near-identical forks sharing a single boilerplate description. The head is not. We kept 22 packages that are a vendor’s documented terminal agent with a real install base, then downloaded every tarball, extracted it, and read the manifest, the lifecycle scripts and the launcher by hand.

Combined, those 22 were installed 46,631,891 times in the week ending 11 October 2026. Codex alone accounts for 23,862,567 of them.

Finding 1: the package you name is a business card

Nine of the 22 use the npm platform-package pattern. The package you install contains a launcher and a manifest; the executable arrives through optionalDependencies, one per operating system and architecture, and npm installs only the one that matches your machine.

Package Named package Linux x64 payload Ratio
@openai/codex 13,206 B 452,474,258 B 34,263x
opencode-ai 7,852 B 185,747,721 B 23,656x
@ampcode/cli 7,965 B 134,363,224 B 16,869x
@github/copilot 12,971 B 185,015,020 B 14,264x
droid 25,819 B 291,658,511 B 11,296x
@kilocode/cli 26,484 B 293,524,297 B 11,083x
cline 48,236 B 137,361,429 B 2,848x
@anthropic-ai/claude-code 188,981 B 257,068,802 B 1,360x

Summed across all nine: 405,885 readable bytes against 2,011,015,620 opaque ones. The thing you named, reviewed, and approved in a pull request is 0.0202% of what landed.

This is not a scandal on its own. Shipping a Rust or Go binary through npm is a legitimate, well-trodden pattern, and esbuild popularised it for good reasons. The problem is what it does to the audit trail everyone assumes they have. Your SCA scanner reads manifests. Your licence checker reads manifests. Your review process reads a diff. None of them see 2 GB of compiled code, and none of them say so.

Finding 2: the dependency in your lockfile does not exist

Codex goes a step further than its peers. Its optionalDependencies are not sibling packages. They are npm aliases:

"@openai/codex-linux-x64": "npm:@openai/codex@0.162.1-linux-x64"

The payload is published as a prerelease version string of the same package name. We queried the registry for @openai/codex-linux-x64 and it returns HTTP 404. The name is real only inside your dependency tree. Anyone auditing a lockfile by resolving names against the registry, a reasonable and common thing for a tool to do, finds nothing there and has to decide whether that is an error or a ghost.

Finding 3: the daemon is on by default, and it is not just Codex

We did not take the Hacker News thread’s word for the daemon. We read OpenAI’s source. In codex-rs/features/src/lib.rs, the feature spec is unambiguous:

FeatureSpec {
    id: Feature::DaemonAutoStart,
    key: "daemon_auto_start",
    stage: Stage::Stable,
    default_enabled: true,
},

Supporting modules exist at codex-rs/app-server-daemon/src/managed_install.rs and backend/pid.rs, and pid-update-loop appears in the CLI entry point and both shell installers. Stable, on by default, shipped.

Then we found the second vendor, and we found it in their own published code. Cline’s postinstall.mjs, in the tarball, on the registry, right now, opens with this:

“CLI versions <= 3.0.54 restart the hub daemon after a background auto-update even while it is serving live sessions, killing those sessions mid-turn — and their build-fingerprint check then rejects every replacement hub, bricking the running TUI.”

That is a vendor documenting, in a file every user installs, that its background auto-update killed live sessions and bricked the interface. The surrounding function exists purely to shield a running daemon from the install that is happening around it. Two independent vendors, same architecture, same failure mode, found two different ways.

Finding 4: three payloads never touch the registry

Crush, Junie and Codebuff fetch the real executable from somewhere npm has no visibility into. The integrity hash in your lockfile covers the installer that does the fetching. It covers nothing that runs. Two more, Just-Every Code and Factory’s droid, try the registry first and leave it when that path fails, which turns out to matter a great deal.

Credit where it is due: Charm’s Crush does this properly. Its GoReleaser-generated installer carries a per-platform SHA-256 in the manifest, verifies the downloaded archive against it, and fails loudly on a mismatch. Augment’s Auggie pins SHA-256 digests for every ripgrep binary it pulls from GitHub. That is how the pattern should look.

JetBrains Junie is the instructive one. Its postinstall builds a GitHub release URL, downloads a zip, extracts it to ~/.local/share/junie/versions/, and writes a shell shim into ~/.local/bin/junie. There is no checksum, no signature, no verification of any kind in that script. We grepped the whole file for every hashing primitive and found zero. Yet the shim it installs, which handles later self-updates, contains a careful SHA-256 validation routine with explicit handling for poisoned manifests. The update path is hardened. The bootstrap path that establishes trust in the first place is not.

Junie also installs outside node_modules entirely, into your home directory, with its own versions/, updates/ and current symlink. It is a second package manager, bootstrapped by the first. And the npm version and the tool version are separate fields in the manifest: your lockfile pins 3110.7.0 while junieVersion says 3110.7. Removing the npm package does not remove the tool.

Finding 5: hash theatre, and an environment variable that moves the supply chain

When iFlow’s CLI runs inside a JetBrains terminal, it downloads a plugin zip from cloud.iflow.cn and installs it into the plugins directory of every JetBrains IDE it finds on the machine. On the way, it computes a SHA-256 of the archive. The code comment says the hash is there to verify the download. It then logs the digest to the console and compares it to nothing. There is no expected value anywhere in the file. Hashing without comparison is not verification; it is a log line that looks like one.

Two packages let the download host be moved from outside the process. Codebuff builds its binary URL from process.env.NEXT_PUBLIC_CODEBUFF_APP_URL, defaulting to its own domain, then downloads, chmod 0755s and executes the result with no checksum anywhere in the launcher. Junie’s shim honours JUNIE_INSTALL_BASE_URL the same way. Anything that can set an environment variable (a CI job, a shared dotfile, a compromised shell profile) can choose where the agent binary comes from.

Codebuff is also worth noting because it does none of this at install time. The launcher resolves the latest version from the registry and downloads the binary on first run. npm install --ignore-scripts, the standard defence against hostile lifecycle scripts, does not stop it.

Which brings us to the sharpest finding in the set. Factory’s droid installs through platform packages normally, but its postinstall has a fallback for when optional dependencies are disabled. In that path it fetches the packument itself, reads dist.tarball, downloads, gunzips, extracts the binary and writes it to disk at mode 0755. The packument it just parsed contains dist.integrity. The word integrity appears zero times in the file. Running --omit=optional, which a security-conscious team does precisely to reduce unreviewed surface, converts a verified install into an unverified one.

The honest outlier

Antinomy’s forge ships 368,551,166 bytes in the package you name: nine binaries covering every supported platform, inside the tarball, under the lockfile’s integrity hash. Its postinstall only picks one and chmods it. No network call, nothing fetched, nothing to verify later. It is the least fashionable approach in the set and the only one where what npm verified is exactly what executes. Ten of the 22, including Gemini CLI, Qwen Code and the pi family, are similarly self-contained because they ship JavaScript rather than compiled binaries.

What to actually do about it

None of this means stop using agent CLIs. We use them daily. It means the install is a deployment, not a dependency, and should be treated like one.

  • Audit the payload, not the package. Resolve the platform package your lockfile actually pulls and record its size and hash. If your SBOM lists a 13 KB launcher as the agent, your SBOM is wrong.
  • Find the second install root. Check ~/.local/share, ~/.cache and the tool’s own dot-directory. Anything that survives npm uninstall -g is unmanaged software on a developer machine.
  • Treat auto-update as a change-management question. Disable it where you can, and where you cannot, know the update cadence and the endpoint. For Codex specifically that is codex features disable daemon_auto_start.
  • Pin the download host. If a tool reads its base URL from the environment, set it explicitly in CI rather than inheriting whatever the runner has.
  • Do not assume --ignore-scripts is a boundary. At least one of these fetches and executes on first run instead of on install.
  • Ask vendors for signed releases. Crush and Auggie prove it is a solved problem. The gap between those two and the rest is effort, not feasibility.

This is the same seam we have measured before from other directions: provenance attestations that only 293 of 880 packages can produce, licences that change without a version bump, and agent configurations that set no guard at all. The registry is a good distribution network and a poor assurance mechanism, and agent tooling has moved faster than anyone’s audit process.

At REPTILEHAUS we build and run agent infrastructure for clients, which means we own this problem on their behalf: supply-chain controls that survive contact with tools that install themselves, DevOps pipelines that pin what actually executes, and AI agent deployments where the version in the change log is the version on the box. If you are rolling agent CLIs out across a team and nobody has asked what arrives after npm install, that is the conversation to have. Get in touch.


Method: npm registry search API across seven queries (353 candidates), last-week download counts from the npm downloads API for the week ending 11 October 2026, tarballs and manifests pulled from the registry on 13 October 2026, and source verification against openai/codex on GitHub. Sizes are registry-reported unpackedSize for the linux-x64 artefact of each package’s current latest version.

📷 Photo by Arum Visuals on Unsplash