# npins: dependency pinning for Nix, with channels and PyPI in the same file

> npins is a Rust command line tool that keeps a Nix project's dependencies in a sources.json file, pinning git branches, release tags, Nix channels and PyPI packages with hashes you can verify later. It is inspired by Niv and adds import from flake.lock and lon.lock, a lockfile mode and a verify command.

**andir/npins** — Nix dependency pinning. Very similar to Niv but has a few features that I personally wanted.

- Repository: https://github.com/andir/npins
- Stars: 582 · Forks: 49
- Language: Rust
- License: EUPL-1.2
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/andir-npins

## One pin file for channels, git, GitHub, GitLab and PyPI

npins is a simple tool for handling different types of dependencies in a Nix project, and the project description is candid that it is inspired by and comparable to Niv, with a few features the author wanted. The pin types it supports are the practical ones a Nix project runs into. It can track git branches and git release tags, where tags must roughly follow SemVer and where GitHub and GitLab releases are intentionally ignored. For repositories hosted on GitHub or GitLab it uses fetchTarball rather than fetchGit, which produces a smaller store path. It tracks Nix channels, and it tracks PyPI packages. The design is that all of these end up as entries in one sources.json file, and your Nix code imports that file rather than referring to channels or raw URLs.

## The channel pin is the feature that goes furthest from Niv

The features list calls out the channel behaviour specifically, and it is worth reading twice because the difference is not just cosmetic. Unlike tracking a channel from its git branch, this gives you access to the programs.sqlite database, and it can also track Nix channel artifacts such as live isos. The first point is the practical one. A channel tracked as a git branch gives you a nixpkgs expression, but a channel tracked as an artifact gives you the thing the channel actually published, metadata and all. The second point, tracking live isos, is a separate use of the same mechanism. The channel pin is therefore the clearest example of the author's stated intent, a feature the author personally wanted.

## Getting npins onto a machine, three ways

npins should readily be available in all sufficiently new nixpkgs, so the ordinary way to run it is a shell with the package in it:

```sh
nix-shell -p npins
```

If you want a nightly, the README notes that this requires the new style Nix commands:

```sh
nix shell -f https://github.com/andir/npins/archive/master.tar.gz
```

There is also an option to install it into your profile with nix-env, which the README marks as not recommended but potentially useful for bootstrapping:

```sh
nix-env -f https://github.com/andir/npins/archive/master.tar.gz -i
```

The licence is EUPL-1.2, which is unusual for a Rust tool, and the package metadata records the workspace version as 0.5.1 with edition 2024 and a Cargo resolver of 3. The dependency set is small, with serde_json, url, anyhow, log, tokio and clap, and there is a comment in the manifest about pinning clap because a later patch has a regression in the help output.

## npins init writes the directory and the first channel

The quickstart is a single command, and the output tells you what you get:

```console
$ npins init
```

Running it creates an npins folder containing a default.nix and a sources.json, and by default the nixpkgs-unstable channel is added as a pin. The init help text adds a detail worth knowing, that running it multiple times restores or upgrades the default.nix and never touches your sources.json, and that a --bare flag skips adding the initial nixpkgs entry. The directory name is configurable through the --directory option, which defaults to npins, and there is also a --lock-file option that activates lockfile mode, where no default.nix is generated and the directory option is ignored.

## Reading a pin: url, hash and the frozen flag

A pin entry is a small JSON object, and the show command prints it in a readable form with the name, the artifact, the url, the hash and whether it is frozen. The file itself is a pins object keyed by name with a version field, and the example in the README shows a channel entry carrying its type, name, artifact, url and hash. The attributes are exposed one to one, so sources.myPackage.version works for many pin types provided the pin actually tracks some version, with the caveat the README gives that the available attribute may change over time. The hash is the point of the whole exercise, because it is what lets a build fetch the same bytes months later, and the verify command exists to check that the recorded hashes still match what is actually out there.

## Freeze, update and verify, and what lockfile mode changes

The command list in the help output is the clearest description of the workflow. update moves all or the given pins to the latest version, verify checks that the pins still have correct hashes, and the help text describes verify as being like update with partial and dry-run flags and then checking that the diff is empty, which is a neat way to express a dry run as a first class command. freeze prevents a pin entry from being changed during an update, and unfreeze thaws it again. get-path evaluates the store path of a pin, fetching it if necessary, and the README warns not to forget to add a GC root, which is the classic footgun with any pinned store path. Lockfile mode, activated by --lock-file, changes the shape of the tool: no default.nix is generated and the directory option is ignored, so the sources file becomes the only artefact.

## Moving in from Niv, flake.lock or lon.lock

There are three import commands, and they are the practical reason to try the tool before committing to it. import-niv reads entries from a Niv sources file, and the README describes the migration as a drop-in replacement of the import./nix/sources.nix with import./npins. The warning matters, though: the import functionality is minimal and only preserves the information needed to identify the dependency, not the pinned values themselves, so a migration must always be followed by an update unless you do it by hand. import-flake tries to import entries from a flake.lock, and import-lon does the same for a lon.lock.

## Adding pins, and the state of the project

The add examples in the README show the shape of each pin type, and they are worth skimming because the arguments differ per type. A channel can be added by name or by artifact file, a git repository by URL with a branch, a GitHub repository by owner and name, a GitLab project by group and name with a --token option for private repositories and an --at option to pin an exact commit or tag, and a PyPI package by name with --at for an exact version or --upper-bound to stay within a range. The repository is a Cargo workspace with a libnpins library, a completions crate, tests, a justfile and a nix-build based integration test setup that runs against recorded snapshots, and the README is generated from a template through readme.nix. The last push was on 2026-10-19 and the recent releases are 0.4.2 and 0.5.1 on 2026-09-02, so the project is being worked on even though the version number is still below 1.0.

## Conclusion

npins fits a Nix project that wants its external dependencies written down as hashed pins in one file, and that wants to mix channels, git repositories and PyPI packages in the same mechanism. It does not fit a project that has already committed to flakes and lock files, since importing a flake.lock is a one-off convenience rather than a workflow. Before adopting it, read the pin type you actually need, because the tool makes deliberate differences between them, and check whether the channel pin behaviour matches how you consume channels, since the README singles that out as the feature it adds over Niv.

## FAQ

### What does npins pin?

Git branches, git release tags where tags must roughly follow SemVer, Nix channels including channel artifacts such as live isos, and PyPI packages. For GitHub and GitLab hosted repositories it uses fetchTarball instead of fetchGit, and GitHub and GitLab releases are intentionally ignored.

### How do I install npins?

It should be readily available in all sufficiently new nixpkgs, so nix-shell -p npins is the ordinary route. A nightly needs the new style Nix commands with nix shell, and nix-env -f with a GitHub archive URL is also possible, which the README marks as not recommended but useful for bootstrapping.

### Can I migrate an existing Niv setup to npins?

Yes, with npins import-niv nix/sources.json followed by npins update, and then replacing import ./nix/sources.nix with import ./npins in your configuration. The README notes the import only preserves enough to identify each dependency, not the pinned values, so an update is required.

### What does the verify command do in npins?

It verifies that all or the given pins still have correct hashes, which the help text describes as being like update with the partial and dry-run flags and then checking that the diff is empty. The freeze and unfreeze commands control whether a pin can be changed during an update at all.

## Sources

- [andir/npins on GitHub](https://github.com/andir/npins)
- [Issues](https://github.com/andir/npins/issues)
- [License: EUPL-1.2](https://github.com/andir/npins/blob/master/LICENSE)
- [README](https://github.com/andir/npins/blob/master/README.md)
- [Releases](https://github.com/andir/npins/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/andir-npins
