Open-source project
andir/npins avatar
andir/npins

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

Nix dependency pinning. Very similar to Niv but has a few features that I personally wanted.

582 stars49 forksRustEUPL-1.2

At a glance

What is it?
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.
Who is it for?
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.
Can I use it commercially?
Yes, with conditions. EUPL-1.2 is a weak copyleft licence: you can use it inside commercial and closed-source software, but if you distribute changes to its own files, you must publish those changes under the same licence.
Is it still maintained?
Yes. The repository last received commits 4 days ago.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

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.

Editorial 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.

Frequently asked questions

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.

Official sources

  1. andir/npins on GitHub
  2. Issues
  3. License: EUPL-1.2
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/andir-npins.svg)](https://hysenlabs.com/projects/andir-npins)

Was this useful?