jpm

A fast, small, secure-by-default package manager for JavaScript

curl -fsSL https://getjpm.sh | sh

Every line written by Claude Code. Directed by one engineer. Checked against everyone else's tests.

  • ~2 MBone binary, no runtime to install
  • 1.22 scold nuxt install on GitHub CI
  • 34 MBmemory used to install nuxt on GitHub CI

Speed

Fast where your CI spends its time

Three real projects, four install phases, eight package managers, against the live npm registry. On GitHub CI jpm is the fastest in 9 of 12 cells; the other three are shown as measured.

Machine
Phase
Project

No cache, no lockfile, no node_modules.

Install wall time, shorter is fasterGitHub CI, Linux · nuxt · 591 packages
GitHub CI, Linux, cold, nuxt · 591 packages
Package managerWall time
jpm1.22 s
bun 1.4.21.28 s
pnpm 12.8.11.72 s
aube 2.6.03.00 s
upm 1.3.13.52 s
deno 2.9.67.64 s
yarn 4.18.19.63 s
npm 12.1.018.54 s

jpm is the fastest here, 15× faster than npm.

Wall time, medians: ubuntu-latest with 4 cores (5 runs) and Windows 11 with Defender on (3 runs), 2026-09-30. Full tables, with CPU time, in docs/benchmarks.md.

Get started

Switch in one command

Run jpm install in your project. jpm imports the lockfile you have into its own jpm.lock, with the same versions.

  • package-lock.json and npm-shrinkwrap.json (npm 7+), pnpm-lock.yaml (pnpm 9+) and bun.lock are carried over as they are: the same versions and the same tree, with no registry lookups.
  • yarn.lock, from yarn 1 or yarn 2 and later, keeps every version yarn picked. jpm asks the registry only for what yarn.lock doesn't record: peers, platforms and bins.
  • Your old lockfile is left untouched. Delete it when you're ready.
  • CI keeps working during the switch: jpm ci and jpm install --frozen-lockfile install from the old lockfile as it is until jpm.lock is committed.
  • It also reads workspaces, pnpm-workspace.yaml, catalogs, overrides and resolutions, and patches.

Older formats, out-of-date lockfiles and the rest: docs/migrating.md.

$ cd your-project
$ jpm install   # reads package-lock.json / pnpm-lock.yaml / yarn.lock / bun.lock → writes jpm.lock

Lean

Small enough to forget it's there

One binary with nothing behind it, an install that holds little in memory, and a store that keeps each file once.

Not every number goes jpm's way: npm and yarn keep compressed tarballs, so their caches are smaller. jpm keeps its files unpacked, ready to link.

GitHub CI, Linux, 2026-09-30. Binary sizes are the README's round figures.

Binary size, approximate
Binary size, approximate
jpm~2 MB
pnpm~60 MB
bun~80 MB
aube~150 MB
Peak memory, nuxt ci install
Peak memory, nuxt ci install
jpm34 MB
bun89 MB
pnpm184 MB
npm387 MB
Disk after a cold nuxt install, node_modules and store
Disk after a cold nuxt install, node_modules and store
jpm244 MB
bun261 MB
pnpm290 MB
npm443 MB
CI cache after a cold next install
CI cache after a cold next install
npm194 MB
yarn328 MB
jpm347 MB
pnpm433 MB
bun466 MB

Storage

Each file stored once. Each package built once.

jpm keeps one store per machine and builds each package's directory once, for every project that uses it. Installing again is mostly making links.

  1. A content-addressed store

    Each tarball is checked against its integrity, then unpacked once into ~/.jpm/store. Every file is kept once, by its hash, read-only.

  2. A global virtual store

    Each package is built once as an entry: name@version, plus a short digest of its dependency graph when it has dependencies. Its files are hardlinked from the store, and its dependencies are linked beside it, so it can import only what it declared.

  3. Your node_modules

    A project's node_modules links to those entries. A warm install makes only these few links: 4 ms for nitro on GitHub CI.

  4. Every other project

    The next project on the machine links to the same entries. When nothing changed, a repeat install checks a few links and is done in 1–2 ms.

~/.jpm/storefiles, by hash3fa19c07e4b271d80b5ec6a3hardlinks~/.jpm/store/v1/linksentries, built once[email protected]node_modules/debugnode_modules/ms[email protected]node_modules/msapp-one/app-two/node_modules/debugnode_modules/debuglinks

Next.js and Nuxt: inside the project

Next's Turbopack compiles nothing outside the project, and Nuxt imports packages it doesn't declare. For a project that depends on next or nuxt, jpm builds the entries in node_modules/.jpm instead, still hardlinked from the store.

node_modules/next -> .jpm/[email protected]…/node_modules/next
node_modules/.jpm/[email protected]…/node_modules/next/
node_modules/.jpm/[email protected]/node_modules/react/

The full picture: docs/how-it-works.md.

Security

Secure by default

The safe setting is the one you get without asking. Loosening one is your call, made in your own settings, not a package's or a cloned repository's.

  • Install scripts wait for you

    A dependency's install scripts, the way most npm malware runs, don't run until you jpm approve that package and version. A new version waits for approval again.

  • New versions wait a day

    A version published in the last day isn't picked (min-release-age), even when a package deep in the tree pins it exactly, so a hijacked release can't ride in on a pin.

  • No surprise sources

    A published package can't pull in git or tarball dependencies, or paths outside itself. Only your project and its workspaces choose where code comes from.

  • Checked before it's visible

    Every package is checked against its lockfile integrity before any of its files are visible to your project.

  • No secrets in the lockfile

    Credentials never land in jpm.lock: tokens stay in your .npmrc, and a git URL carrying a password is refused.

  • A cloned repo can't lower the bar

    A project's own .npmrc can't turn off TLS checks, swap the CA or proxy, or loosen the release age. jpm ignores those there and names them in a warning.

  • Its own TLS, no OpenSSL

    jpm speaks TLS with its own code, tested against Project Wycheproof and the RFCs' vectors, and checks certificates against Mozilla's roots and your system's.

  • Runtimes checked by signature

    A Node.js runtime a package asks for is checked against the release's SHASUMS256.txt, trusted only once its signature matches Node's release keys.

  • Less to attack

    One ~2 MB binary, with no runtime and no dependency tree of its own to install: less code between you and the registry.

Secure by default isn't a sandbox: a script you approve runs as you. The details: docs/install-scripts.md and docs/configuration.md.

The story

Written by Claude Code. Checked by everyone else's tests.

It started with an episode of the Syntax podcast that held up upm as faster and smaller than the rest. Small, because it runs on Node.js. So: what if it were ported to Rust and made small for real? And what would it take to turn an AI's one-shot into something you'd trust in CI?

Claude Code, running Claude Opus 5.5, wrote every line, its own TLS and crypto included. JT Turner, an engineer of 27 years, made the calls. When Claude pushed back on writing our own TLS, the condition was: test it like we didn't trust it. So every "it works" is checked against someone else's tests: Wycheproof, the RFCs, and the suites of npm, pnpm, yarn and bun.

The numbers decide. An HTTP/2 client was built, benchmarked on GitHub's runners and a desktop, found slower and hungrier for CPU, and deleted.

Is jpm perfect? No. Perfect doesn't exist. There's good enough, there's great, and there's tested enough to know which one you have.

JT Turner

Read the whole story

Try jpm in your CI. Then go build the thing you didn't think you had time for.

Install jpm