
# Configure a project

> A project pin tells jup which tool and version to run.

## Package-manager pins

The simplest pin lives in `package.json`:

```json
{
  "packageManager": "pnpm@12.0.0"
}
```

Do not write a bare name such as `"pnpm"`. A manifest pin must include a
version, range, tag, or supported URL form.

The easiest way to create a pin is:

```sh
jup use pnpm@^12
```

`use` accepts:

```sh
jup use pnpm             # current stable release, then write an exact pin
jup use pnpm@12.0.0      # exact release
jup use pnpm@^12         # keep this range and create jup.lock
jup use pnpm@latest      # resolve the tag now, then write an exact pin
```

Partial versions such as `12` and `12.1` are also accepted as ranges. The caret
form makes the intent clearer. jup keeps a range as typed
and records its current answer in `jup.lock`.

Automatic selection skips prereleases unless the selector asks for one or
`JUP_ENABLE_PRERELEASES=1` is set.

## Exact pins and digests

An exact pin is reproducible by itself. For a portable artifact, jup writes the
checked digest in the version suffix:

```json
{
  "devEngines": {
    "packageManager": {
      "name": "yarn",
      "version": "4.0.0+sha512.0123abcd..."
    }
  }
}
```

Do not calculate this value by hand. `jup use` writes the digest of the bytes it
verified.

Some tools publish different artifacts for each operating system and
architecture. Their manifest pin stays digest-free:

```json
{
  "devEngines": {
    "packageManager": {
      "name": "pnpm",
      "version": "12.0.0"
    }
  }
}
```

jup still verifies the current host's artifact. A single host-specific digest in
`package.json` would break the project on another host.

## Keep a range with `jup.lock`

A range records intent:

```json
{
  "devEngines": {
    "packageManager": {
      "name": "pnpm",
      "version": "^12"
    }
  }
}
```

Run this once to select and record a release:

```sh
jup use --lock pnpm@^12
```

jup keeps the range in `package.json` and creates `jup.lock` beside it. Commit
both files. Future runs use the recorded release without asking which matching
release is newest.

Without `--lock`, `jup use pnpm@^12` still writes the range and installs a
matching release; it just records the choice in the short-lived cache below
instead of creating a file in your tree. Once `jup.lock` exists, `use` and `up`
keep it up to date without the flag.

Update that decision when you choose:

```sh
jup up
```

For a range, `up` changes the `jup.lock` entry and leaves the range alone. Only
`use` and `up` change the committed lockfile.

`up` refreshes an existing `jup.lock`; it creates one only with `--lock`. On
a project with no committed lockfile it selects and installs the newest matching
release, records it in the short-lived cache below, and leaves the project
without a lockfile.

For tools with per-host artifacts, the lockfile can hold one integrity value per
host. The selected version is shared. A new host with no digest entry can still
use that version after registry signature verification; run `use` or `up` on
each target host if your policy requires every host digest to be committed.

Set `JUP_FROZEN_LOCKFILE=1` when a command must not create, refresh, or remove a
lockfile entry. It refuses only the commands that would change the file, so `jup
up` on a project with no `jup.lock` still runs.

Exact pins do not use the lockfile, so `--lock` has no effect on them.

To stop committing a lockfile, delete `jup.lock`. Later runs then resolve the
range through the short-lived cache again.

### The package manager's own lockfile

pnpm 12 and later record the version they ran for a `devEngines` range in
`pnpm-lock.yaml`, under the root importer:

```yaml
importers:

  .:
    packageManagerDependencies:
      pnpm:
        specifier: ^12
        version: 12.3.4
```

That file is committed, so jup reads it. When a range has no `jup.lock` answer,
the version recorded there is the version jup runs. Every checkout then runs the
same pnpm, and `pnpm install` stops rewriting the line on each machine.

jup only reads it, and only when the recorded specifier is the range your
manifest currently declares and the version still satisfies it. `jup.lock` wins
where both are present, and `jup up` still moves a range forward. Set
`JUP_ENABLE_PM_LOCKFILE=0` to ignore the file.

### A project with no spec at all

If your `package.json` names no package manager, jup normally runs its default
version. A `pnpm-lock.yaml` beside it can still say which major to run: the
`lockfileVersion` on its first line is the format pnpm wrote it in, and pnpm
changes that format on major boundaries.

| `lockfileVersion` | jup runs |
| --- | --- |
| `9.0` | its usual default — pnpm 9, 10, 11 and 12 all write this format |
| `6.0` or `6.1` | pnpm 8 |
| `5.4` | pnpm 7 |
| `5.3` | pnpm 6 |
| `5.2` | pnpm 5.10 or newer 5.x |
| `5.1` | pnpm 3.5 to 5.9 |
| `5` | pnpm 3.0 to 3.4 |

This matters because running the wrong major over an old lockfile is not a no-op:
a newer pnpm rewrites the file in its own format and re-resolves your pinned
versions while doing it, and `pnpm install --frozen-lockfile` fails outright. So
a fresh clone of an old project gets the pnpm that project's lockfile was written
for, without anyone editing anything.

It is a fallback and nothing more. Anything you actually declare —
`packageManager`, `devEngines.packageManager`, `jup.lock` — decides instead, the
guessed version is never recorded as your machine's default, and
`JUP_ENABLE_PM_LOCKFILE=0` switches it off along with the read above. To settle
the question for good, pin it:

```sh
jup use pnpm@8
```

### The short-lived resolution cache

If a range has no committed answer in either file, jup may remember its registry
answer in `node_modules/.jup/jup.lock` for 24 hours. This is a disposable cache, not a
project lockfile. It is only created when `node_modules` already exists.

When that entry expires and the registry has a temporary outage, jup can use the
stale version and print an advisory. It does not use stale data to bypass a
disabled network, authentication error, TLS failure, release-age policy, or
ordinary 4xx response.

## Registry tags

A tag in the manifest, such as `pnpm@latest`, changes as the registry changes.
Its short-lived answer may be cached, but it is not a committed decision and
`jup up` will not update it.

For repeatable builds, resolve it to an exact pin:

```sh
jup use pnpm@latest
```

Or replace it with a range and commit `jup.lock`.

## `devEngines.packageManager`

A project may use Node's structured form:

```json
{
  "devEngines": {
    "packageManager": {
      "name": "pnpm",
      "version": "^12",
      "onFail": "error"
    }
  }
}
```

`version` is a semver range. `onFail` can be:

- `error` — stop on a mismatch (also the default);
- `warn` — print a warning and continue;
- `ignore` — continue silently;
- `download` — continue silently, because jup has already downloaded and run the
  version this object declares. Where jup cannot do that — the object is
  malformed, or names no version, or its digest is unusable or disagrees with
  the one on `packageManager` — it warns instead.

`download` is pnpm's; npm documents only the first three.

This is where `jup use` and `jup up` write the pin, and where jup reads it
first.

When both this object and `packageManager` exist, the structured object wins as
long as it names a `version`; a disagreement is still reported according to
`onFail`. An object that names only a package manager has not said which release
to run, so the top-level `packageManager` answers instead.

Writing a pin consolidates the two. The pin goes into this object, and an
existing top-level `packageManager` is **removed** rather than updated beside it:
one pin, in the one field that can carry a range, an `onFail` and a digest at
once. jup never creates a `packageManager` field.

`jup use` replaces whatever the object declared, including a range: the object
is the pin, so a range left behind would be what the next run resolved. `jup up`
against a declared range does not touch the manifest at all — it refreshes the
recorded release in `jup.lock`.

A pin records the release digest. In this object, the digest is stored as SRI in
`integrity`, next to a clean semver `version`. In the top-level `packageManager`
string, it is stored in the `<version>+<algo>.<hex>` suffix. Both forms are read
the same way.

To commit the version alone:

```sh
jup use --no-integrity yarn@4.6.0
```

This also removes any existing digest from the manifest. The pin is still
resolved and installed, and the registry signature still verifies the download.

Tools that read only `packageManager` — Corepack, npm, Yarn — will not see a pin
jup wrote. If you need them to, keep a `packageManager` field by hand; note that
`jup use` and `jup up` remove it again the next time they write a pin, and that
it can only ever hold an exact version, never a range.

## Pin Node.js

Node.js is a runtime, not a package manager. Put it in `devEngines.runtime`:

```sh
jup use node@^22
```

```json
{
  "devEngines": {
    "runtime": {
      "name": "node",
      "version": "^22"
    }
  }
}
```

Because `^22` is a range, this also creates a `jup.lock` entry for the selected
Node release. Use an exact selector such as `node@22.1.0` when you want the
manifest pin itself to be exact.

A project can pin Node and a package manager at the same time. The two settings
do not conflict. `jup use node@...` writes the runtime pin and does not run a
package-manager install afterward.

Never put Node in `packageManager`; jup rejects that and points to
`devEngines.runtime`.

### bun, deno, and nub

These three are package managers *and* runtimes. They are pinned like any other
package manager — `packageManager` or `devEngines.packageManager` — and `jup use
bun@1.4.0` writes that pin.

Because they are also runtimes, another manager's pin never blocks them: in a
pnpm project, `bun server.ts` and `deno run main.ts` run the requested tool at
its own version rather than failing with "This project is configured to use
pnpm". Note that this covers every invocation, `bun install` included, so a
project that wants installs to go through its pinned manager should rely on the
manager's own lockfile rather than on jup rejecting the command.

### Use `.nvmrc`

When `devEngines.runtime` is absent, jup reads the nearest `.nvmrc`. It accepts
numeric versions and ranges, with an optional leading `v`. `node` and `stable`
mean the latest release. Comments, blank lines, and nvm-style `key=value` lines
are ignored.

jup does not interpret local nvm state. Aliases such as `system`, `default`,
`iojs`, `lts/*`, LTS codenames, and user aliases cannot be resolved: jup warns,
ignores the file, and runs its default Node version. Replace the alias with a
version/range or add `devEngines.runtime` to say what it meant.

A `.nvmrc` that carries no version at all — empty, or two of them — is an error
rather than a warning, because nvm rejects it too.

jup only reads `.nvmrc`; it never edits it. A manifest runtime declaration wins.

## Workspaces and `--here`

Read-only commands walk upward and let nested packages inherit a declaration.
Commands that write stop at a workspace root (a manifest with `workspaces` or a
directory with `pnpm-workspace.yaml`). jup prints every path it changes.

Target only the current directory with:

```sh
jup use --here pnpm@^12
jup up --here
```

`use --here` can create `package.json`. `up --here` needs an existing usable pin.

## Project env files

jup can load the nearest `.jup.env`; if that name is absent in the same
directory, it tries `.corepack.env`. The search stops at the project boundary.
The real process environment always wins.

Only approved `JUP_`/compatible `COREPACK_` variables are accepted. Project env
files cannot choose the store, shim directory, credentials, trust keys, TLS
weakening, a spec override, host runtimes, or other protected settings.
See [Settings](./settings) for the exact list.
