A drop-in replacement for nixfmt: same binary name, same flags, byte-identical output — just faster and embeddable.
nixfmt v1.2.0 across all of
nixpkgs; swap the binary and nothing in your tree reformats.nixfmt-tree (benchmarks).#![forbid(unsafe_code)],
two dependencies) or in the browser via the
WebAssembly build.# Nix
nix run github:Mic92/nixfmt-rs -- --help
# Cargo
cargo install nixfmt_rs
# From source
nix develop -c cargo build --release # binary at target/release/nixfmt
Prebuilt static binaries are attached to each
GitHub release with a
SHA256SUMS file and Sigstore-backed provenance:
gh attestation verify ./nixfmt-x86_64-linux -R Mic92/nixfmt-rs
NixOS / home-manager (via flake input):
{
inputs.nixfmt-rs.url = "github:Mic92/nixfmt-rs";
outputs = { nixpkgs, nixfmt-rs, ... }: {
nixosConfigurations.host = nixpkgs.lib.nixosSystem {
modules = [
({ pkgs, ... }: {
environment.systemPackages = [ nixfmt-rs.packages.${pkgs.system}.default ];
# or, in home-manager:
# home.packages = [ nixfmt-rs.packages.${pkgs.system}.default ];
})
];
};
};
}
The binary is named nixfmt and is flag-compatible with upstream.
# stdin → stdout
echo '{a=1;}' | nixfmt -
# Format files / directories in place (recurses into *.nix, parallel)
nixfmt path/to/file.nix path/to/dir
# Check only (exit 1 if any file would change)
nixfmt -c path/to/dir
# Layout
nixfmt --width 80 --indent 4 file.nix
# Debugging modes (match `nixfmt --ast` / `nixfmt --ir` exactly)
echo '{a=1;}' | nixfmt --ast
echo '{a=1;}' | nixfmt --ir
Technical preview This might change based on how this feature will be implemented in nixfmt: https://github.com/NixOS/nixfmt/pull/388
Wrap a region in /*nixfmt:disable*/ and /*nixfmt:enable*/ to keep it
verbatim, e.g. for hand-aligned tables:
{
formatted = 1;
/*nixfmt:disable*/
aligned = 1;
table = 2;
here = 3;
/*nixfmt:enable*/
alsoFormatted = 2;
}
Each directive must be on its own line (only whitespace around it). An
unclosed /*nixfmt:disable*/ extends to the end of the file.
The binary is a drop-in for nixfmt, so with treefmt-nix just override the
package:
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
treefmt-nix.url = "github:numtide/treefmt-nix";
nixfmt-rs.url = "github:Mic92/nixfmt-rs";
};
outputs = { nixpkgs, treefmt-nix, nixfmt-rs, ... }:
let
forAllSystems = nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed;
in {
formatter = forAllSystems (system:
treefmt-nix.lib.mkWrapper nixpkgs.legacyPackages.${system} {
programs.nixfmt = {
enable = true;
package = nixfmt-rs.packages.${system}.default;
};
});
};
}
Or in a plain treefmt.toml:
[formatter.nixfmt]
command = "nixfmt" # the nixfmt-rs binary is also called `nixfmt`
includes = ["*.nix"]
Without treefmt at all, point nix fmt straight at the binary (it recurses
into directories and formats *.nix in place):
outputs = { nixpkgs, nixfmt-rs, ... }:
let
forAllSystems = nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed;
in {
formatter = forAllSystems (system: nixfmt-rs.packages.${system}.default);
};
The binary is named nixfmt and accepts the same flags and stdin/stdout
contract as upstream, so any existing nixfmt integration works unchanged once
this package is on $PATH (or pointed at explicitly).
Examples
VS Code (jnoortheen.nix-ide):
"nix.formatterPath": "nixfmt"
Neovim (conform.nvim):
require("conform").setup({ formatters_by_ft = { nix = { "nixfmt" } } })
Helix (languages.toml):
[[language]]
name = "nix"
formatter = { command = "nixfmt", args = ["-"] }
Emacs (apheleia): nixfmt
is built in; just ensure the binary resolves to this one.
Anything else: pipe the buffer through nixfmt - (reads stdin, writes
stdout, exit 1 on parse error).
Parse errors come with source snippets, related spans and fix-it hints:
Error[E001]: expected ';', found '='
┌─ config.nix:2:27
│
1 │ {
2 │ services.nginx.enable = true
│ ^^^^
3 │ networking.firewall.enable = false;
= note: missing semicolon after definition
= help: add a semicolon at the end of the previous line
Error[E002]: unclosed delimiter '{'
┌─ config.nix:5:1
│
3 │ bar = {
│ - unclosed delimiter opened here
4 │ baz = 2;
5 │ }
│ ^
= help: add closing '}'
Error[E005]: commas are not used to separate list elements in Nix
┌─ config.nix:1:4
│
1 │ [ 1, 2, 3 ]
│ ^
= help: use spaces to separate list elements: [1 2 3]
Run cargo run --example error_visualization to see the full catalogue
of diagnostics on intentionally broken inputs.
For editor / LSP integrations, --message-format=json writes one JSON
object per diagnostic to stderr instead of the rendered snippet. The shape
follows the LSP Diagnostic type (0-based range), with raw byteRange
offsets and the human rendered text alongside:
$ nixfmt --message-format=json --check . 2>&1 >/dev/null | jq -c .
{"file":"config.nix","severity":"error","code":"E002",
"message":"unclosed delimiter '{'",
"range":{"start":{"line":4,"character":0},"end":{"line":4,"character":0}},
"byteRange":{"start":42,"end":42},
"help":"add closing '}'",
"relatedInformation":[{"message":"unclosed delimiter opened here",
"range":{"start":{"line":2,"character":8},"end":{"line":2,"character":9}},
"byteRange":{"start":18,"end":19}}],
"rendered":"Error[E002]: unclosed delimiter '{'\n ┌─ config.nix:5:1\n..."}
{"file":"ok.nix","severity":"warning","message":"not formatted"}
In this mode every line on stderr is JSON (parse errors, --check
results, I/O failures), so wrappers can parse line-by-line without
special-casing.
--check over a full nixpkgs checkout (42 942 .nix files), AMD EPYC
7713P 64-core, nixfmt 1.2.0, treefmt 2.5.0. treefmt runs use --no-cache
so every file is actually processed:
| command | wall time | user time | vs nixfmt-rs |
|---|---|---|---|
nixfmt-rs --check . |
1.68 s | 9.34 s | 1.00× |
treefmt driving nixfmt-rs |
3.35 s | 10.14 s | 1.99× |
nixfmt-tree (treefmt + Haskell) |
38.89 s | 216.2 s | 23.2× |
Single large file (all-packages.nix, ~12 k lines): 36.8 ms vs 762.7 ms
(20.7×).
Reproduce with scripts/bench.sh; the dev shell
provides hyperfine and the script defaults to the nixpkgs revision
pinned in flake.lock, so nix develop -c scripts/bench.sh is
self-contained.
For parser micro-benchmarks via criterion (the bench feature gates the
criterion dependency so cargo test stays lean):
cargo bench --features bench
The formatter is also usable as a library. Disable default features to skip
the CLI-only dependencies (ignore, mimalloc):
[dependencies]
nixfmt_rs = { version = "0.1", default-features = false }
let formatted = nixfmt_rs::format("{foo=1;}")?;
let mut opts = nixfmt_rs::Options::default();
opts.width = 80;
let formatted = nixfmt_rs::format_with(src, &opts)?;
On parse failure, render the returned ParseError with source context via
nixfmt_rs::format_error. See the API docs.
For JavaScript/TypeScript, the WebAssembly build is on npm as
nixfmt-rs — see
wasm/README.md.
--ast, --ir and formatted output are
diffable byte-for-byte against the Haskell implementation, so any
divergence can be bisected mechanically.Nixfmt/Parser.hs directly,
which keeps error messages and trivia handling under our control.memchr and
compact_str; the binary adds ignore (parallel .nix walking) and
mimalloc.See docs/ARCHITECTURE.md for how the pieces fit
together.
cargo test # full suite
# differential check vs. reference `nixfmt` over a nixpkgs checkout
# modes: format | ir | ast; env: NIXPKGS, LIMIT, JOBS, MAX_BYTES, REF, OUT
LIMIT=0 cargo run --release --features sweep --example diff_sweep -- format
The test suite is layered (unit → regression → vendored fixtures →
properties); see tests/README.md for where to add
new cases.
$ claude mcp add nixfmt-rs \
-- python -m otcore.mcp_server <graph>