deadnix finds unused bindings in Nix files and can delete them
Scan Nix files for dead code
At a glance
- What is it?
- astro/deadnix parses .nix files and reports unused variable bindings, lambda arguments and inherit declarations, with a --edit mode that rewrites the source. It ships as a Rust binary usable through Nix flakes, pre-commit and GitHub Actions, under the GPL-3.0-or-later licence.
- Who is it for?
- deadnix is a good fit for a Nix shop that wants unused bindings flagged on every commit, especially if you already run pre-commit or use nix run github:astro/deadnix in CI. It is the wrong tool if your dead code lives in flake outputs rather than in .nix bindings, since its scope is variable bindings and nothing else.
- Can I use it commercially?
- Yes, with conditions. GPL-3.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
- Is it still maintained?
- Yes. The repository last received commits 22 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 24, 2026, and from our analysis. They are not legal advice.
Editorial analysis
What deadnix actually treats as dead code
The scope is narrower than the word dead code usually implies. deadnix scans .nix files for unused variable bindings, and its own help text calls itself a finder of dead code in .nix files. In practice the report covers four kinds of unused declaration: lambda arguments, lambda attrset pattern names, let bindings, and inherit declarations. The sample output in the documentation shows each of them, including an inherit that pulls unused_inherit from builtins, a let binding named unused, a shadowed binding, and a lambda argument that shadows an outer binding. It is a static reachability analysis of bindings, nothing more. It does not report unused flake outputs, unused packages in a package set, or unreferenced files. If your idea of dead code is a stale derivation in a flake, this tool will not find it.
The rnix and rowan foundation, and why the version numbers matter
The implementation is Rust, and the dependency list tells you the approach. rnix is pinned at 0.14, and rowan at 0.16 with an explicit comment that it matches the rnix version. rowan is a lossless syntax tree library, so the parser builds a concrete syntax tree that preserves every token, comments and formatting included. That is the enabling detail for a tool like this. If the analyzer had to work on a normal AST with spans and trivia discarded, the report could not reliably point back at exact source locations, and an edit mode that rewrites files would risk destroying formatting. With a lossless tree, the tool can attach an inline annotation to the precise character range that contains the unused binding. The remaining dependencies are ordinary: clap 4 for the command line, walkdir 2 to walk directories, and ariadne 0.6 for the diagnostic rendering that produces the caret style output in the example report. Serde and serde_json are optional and pulled in by the json-out feature, which is enabled by default, so JSON output is available out of the box.
Running deadnix for the first time with nix run
The documented invocation goes through a Nix flake, so no installation step is described separately:
nix run github:astro/deadnix -- --helpThe help output lists the options that decide how the scan behaves. FILE_PATHS accepts .nix files or directories containing them and defaults to the current directory. The analysis switches are -l/--no-lambda-arg to skip lambda parameter arguments, -L/--no-lambda-pattern-names to skip lambda attrset pattern names, and the -_ flag to skip bindings that a naming convention marks as intentionally unused. Output control comes from -q/--quiet, which suppresses the report, -o/--output-format with the values human-readable and json, and --exclude for paths to leave out of the analysis. Two flags change what happens next: -e/--edit removes the unused code and writes it back to the source file, and -f/--fail exits with status 1 when unused code is found, which is what you want in CI.
To scan a single file:
nix run github:astro/deadnix example.nixEach finding is reported with a file, line and column, so the output points straight at the offending binding.
Reading a report without drowning in ANSI escapes
A report looks like this, with the caret markers showing which token is at fault:
Warning: Unused declarations were found.
╭─[example.nix:1:1]
1 │unusedArgs@{ unusedArg, usedArg, ... }:
· │ ╰───── Unused lambda pattern: unusedArg
· ╰───────────────── Unused lambda pattern: unusedArgs
3 │ inherit (builtins) unused_inherit;
· ╰─────── Unused let binding: unused_inheritReports contain ANSI color escape codes unless the NO_COLOR environment variable is set, which matters when you pipe output into a log collector or a code review comment. The output format option also accepts json, and because serde and serde_json sit behind the default json-out feature, you can build tooling around the machine readable form without recompiling the tool. The --exclude parameter takes multiple paths, and the documentation notes the ordering rule: separate the excluded paths with -- so they are not consumed as FILE_PATHS arguments. That detail is easy to get wrong on the first run.
The edit mode, and the nixpkgs callPackages trap
deadnix can remove what it finds. The documentation puts a warning above that section in bold: commit your changes into version control before running it. The command given is:
nix run github:astro/deadnix -- -eq test.nixAs written that string combines the edit flag with the quiet flag and a filename: -q/--quiet suppresses the dead code report, so the run rewrites the file silently. Use the flags the help output defines, -e/--edit for rewriting the file and -f/--fail if you also want a non-zero exit status. The real hazard is not the flag, it is the nixpkgs callPackages case explained in the documentation. callPackages guesses which packages to inject from the names in a package lambda attrset pattern. Some packages alias those names with @args and forward them to another import...nix args. In that arrangement the used arguments are only named in the imported file, so they look dead in the package source file that callPackages imports, and the file becomes unable to guess the dependencies needed to call the package. Removing them breaks the build. The -L/--no-lambda-pattern-names switch exists for exactly this case.
There is also an escape hatch for one declaration. As of 1.1.0, deadnix supports a pragma that skips the check for bindings on the following line:
# deadnix: skipWiring deadnix into pre-commit and GitHub Actions
The project documents two integrations. For pre-commit, the hook goes into your .pre-commit-config.yaml:
repos:
- repo: https://github.com/astro/deadnix
rev: ID # frozen: VERSION
hooks:
- id: deadnix
#args: [--edit] # Uncomment to automatically modify files
stages: [commit]The rev placeholder is meant to be replaced with a version tag and the matching commit ID, and the documentation gives a concrete example of the frozen form:
rev: da39a3ee5e6b4b0d3255bfef95601890afd80709 # frozen: v1.2.3The hook runs at the commit stage by default, and the --edit argument is left commented out, which means the hook reports rather than rewrites. The repository also ships a .pre-commit-hooks.yaml at the top level, which is what makes the deadnix hook id resolvable. For GitHub Actions, the README points to a separate repository, astro/deadnix-action, rather than describing an action inline, so there is no workflow YAML to copy from this repository. Either way, the point of integration is the same: the -f/--fail exit code is what turns a finding into a failed job, and without that flag a CI step will pass while printing warnings nobody reads.
Licence, versioning and the cost of tracking this tool
The licence is GPL-3.0-or-later, declared in the manifest as GPL-3.0-or-later and stated at the repository level as GPL-3.0. For a command line tool that developers install from a flake and run on their own machines, that choice rarely forces a change to a proprietary product, but it does mean any redistribution of a modified binary carries copyleft obligations, and it is worth reading the LICENSE file before vendoring the tool into a distribution you ship. The project also notes a commercial support offer, stating that the author can be hired to implement features or integrate the tool into a toolchain, which is the usual signal that a GPL project with a single maintainer wants to keep the door open for funded work. The current version is 1.3.2 in Cargo.toml, the crate edition is 2021, and the repository has no GitHub releases, so version tracking happens through tags referenced in the pre-commit configuration rather than through release notes. The last push was on 2026-10-03, so the code is still moving.
Editorial conclusion
deadnix is a good fit for a Nix shop that wants unused bindings flagged on every commit, especially if you already run pre-commit or use nix run github:astro/deadnix in CI. It is the wrong tool if your dead code lives in flake outputs rather than in .nix bindings, since its scope is variable bindings and nothing else. Before enabling --edit across a repository, run deadnix without it first and read the report, because the nixpkgs callPackages interaction described in the documentation is exactly the case where automatic removal breaks a build.
Frequently asked questions
What kinds of unused code does deadnix detect?
It reports unused variable bindings, which covers lambda arguments, lambda attrset pattern names, let bindings and inherit declarations. Each finding includes the file, line and column of the unused declaration.
How do I install deadnix?
The documented route is through Nix, for example nix run github:astro/deadnix -- --help. The binary is a Rust crate named deadnix at version 1.3.2, and it is also exposed as a pre-commit hook and through the astro/deadnix-action repository for GitHub Actions.
Can deadnix remove the unused code automatically?
Yes, the -e/--edit flag removes unused code and writes it back to the source file. The documentation warns you to commit your changes into version control before using it, and the -f/--fail flag makes the tool exit with status 1 when unused code is found.
Why does deadnix report arguments that are clearly used?
The nixpkgs callPackages case is the known reason. callPackages injects packages by the names in a lambda attrset pattern, and some packages alias those names with @args and forward them to another import, so the used arguments are only named in the imported file. The -L/--no-lambda-pattern-names option disables that check.
How do I skip a specific declaration in deadnix?
As of 1.1.0, deadnix supports a pragma that skips the check for bindings on the following line. The pragma line is placed directly above the binding in the .nix file.
Official sources
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.
[](https://hysenlabs.com/projects/astro-deadnix)Was this useful?