Dylint runs Rust lints from libraries instead of a fixed set
Run Rust lints from dynamic libraries
At a glance
- What is it?
- Dylint is a Rust linting tool from Trail of Bits that, unlike Clippy's predetermined static lint set, loads lints from user-specified dynamic libraries, so a team can maintain its own lint collection as a separate crate. It is Apache-2.0 licensed and installs as cargo-dylint plus dylint-link.
- Who is it for?
- Dylint suits a Rust team that wants to own its lint rules as versioned code rather than contribute them upstream, since a lint library is an ordinary crate that is downloaded, built and loaded, and the rules can be shared across a workspace or an organisation through git references.
- Can I use it commercially?
- Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- 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 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The difference from Clippy is where the lints come from
Dylint is described as a Rust linting tool, similar to Clippy. But whereas Clippy runs a predetermined, static set of lints, Dylint runs lints from user-specified, dynamic libraries. Thus, Dylint allows developers to maintain their own personal lint collections.
That single difference explains the whole design. Clippy's lints are compiled into the Clippy binary, which means a new lint rule is a change to Clippy itself, reviewed upstream and released with a new Clippy version. Dylint moves the boundary: the lint is a separate artifact, a dynamic library, and Dylint loads it at run time.
The rule is a Rust library that implements a trait, and the README is direct about the effort involved. All you have to do is implement the LateLintPass trait and accommodate the symbols asking to be filled in. Writing a lint is therefore the same kind of work as writing any other Rust library, with a trait to implement rather than a plugin API to learn.
The project came out of Trail of Bits, and the homepage link is their EuroRust 2024 talk on writing Rust lints without forking Clippy, with slides and video. That framing matters: the talk is about the pain of forking Clippy to add one rule, and Dylint is the answer to that specific problem.
The tool is published on crates.io as cargo-dylint and dylint-link, with a repository that carries a LICENSE-APACHE and a LICENSE-MIT, and the release history is active, with v6.2.0 on 2026-10-16 after v6.1.1 and v6.1.0 in the weeks before.
Installing it and running the example lints
Installation is a cargo install of two crates:
cargo install cargo-dylint dylint-linkThe second one, dylint-link, is the piece that does the linking, and it is what lets a lint library be built against the same rustc that will run it.
Running Dylint against a workspace takes a git reference and a pattern:
cargo dylint --git https://github.com/trailofbits/dylint --pattern examples/generalIn that example the libraries are found via the command line. The README says that if you plan to run Dylint regularly, you should consider using workspace metadata instead, and that there are additional ways of finding libraries documented under How Dylint works.
Scaffolding a new lint library is one command:
cargo dylint new new_lint_name
cd new_lint_name
cargo build
cargo dylint list --path .That produces a loadable library right out of the box, and the list subcommand with a path is how you confirm the library was built and is visible to the tool. The pattern of the whole workflow is worth noting: a lint library is a normal Cargo project, it builds with cargo build, and the tool finds it the same way it finds any other dependency.
The repository carries a rich set of example lints under examples/, split into general, restriction, supplementary, experimental and testing directories, and the README links to resources for writing lints.
Workspace metadata, and why it beats the command line
The command line form works, but the project is clearly designed for the metadata form, and the README shows why.
A workspace can name the libraries it should be linted with in its Cargo.toml or dylint.toml file. Specifically, either file can contain a TOML array under workspace.metadata.dylint.libraries. Each array entry must have the form of a Cargo git or path dependency, with two differences: there is no leading package name, that is, no package =, and path entries can contain glob patterns.
The example:
[workspace.metadata.dylint]
libraries = [
{ git = "https://github.com/trailofbits/dylint", pattern = "examples/general" },
{ git = "https://github.com/trailofbits/dylint", pattern = "examples/restriction/try_io_result" },
]The pattern field indicates the subdirectories that contain Dylint libraries, and it can also be an array, in which case the pattern is considered to be the union of the array elements, so the same thing can be written as a table of tables with a pattern list.
The git field can be accompanied by a branch, tag, or rev option, which the README shows as three valid ways to load lints from a remote git repository, pinning to tag v5.0.0, tracking branch master, or pinning to a specific commit hash.
The mechanism behind it is that Dylint downloads and builds each entry, similar to how Cargo downloads and builds a dependency, and the resulting target/release directories are searched for files with names of the form that Dylint recognizes.
So the practical effect is that your lint configuration lives in your workspace manifest, is committed with your code, and is versioned with it. A colleague who clones the repository gets the same lints without knowing any command line flags.
Conditional compilation, the part that bites first
If a lint fires in code that does not enable the lint library, you get an unknown lint warning, and the README documents the mechanism for avoiding that.
For each library that Dylint uses to check a crate, Dylint passes the following to the Rust compiler:
--cfg=dylint_lib="LIBRARY_NAME"You can use this feature to allow a lint when Dylint is used, but also avoid an unknown lint warning when Dylint is not used:
#[cfg_attr(dylint_lib = "LIBRARY_NAME", allow(LINT_NAME))]There is a catch, and it is stated plainly. The approach does not work for pre-expansion lints. The only known workaround for pre-expansion lints is to allow the compiler's built-in unknown_lints lint, with the attribute pair shown in the README.
Then there is a second-order effect from a rustc change. As of nightly-2024-05-05, the names and values of every reachable #[cfg] are checked, which causes the compiler to produce warnings for the cfg_attr attributes described above. To suppress those warnings, you add a check-cfg entry to your packages' Cargo.toml files, and for a workspace the equivalent is a workspace.lints.rust.unexpected_cfgs table with level and check-cfg.
This is the part of the tool that requires attention from a team, and it is worth being honest about. The two lint kinds, late and pre-expansion, need different treatment, and the recent rustc change means the treatment is a Cargo manifest edit rather than a one line attribute. The repository's own Cargo.toml shows the workspace-wide form, with check-cfg listing cfg(coverage), cfg(dylint_lib, values(any())), cfg(nightly), cfg(__cargo_cli) and cfg(__library_packages), and the README links a commit showing the per-package lints setting that pairs with it.
Configurable libraries and the CI caching story
Not every lint can be fully expressed in code, and Dylint gives libraries a configuration channel for the parts that need data.
Libraries can be configured by including a dylint.toml file in a linted workspace's root directory. The file should encode a TOML table whose keys are library names, and a library determines how its value in the table, if any, is interpreted. The example given sets the non_local_effect_before_unhandled_error library's work_limit configuration to 1_000_000.
A work limit is a good example of what belongs in configuration rather than in code. A lint that explores a large search space needs a bound on how much work it may do, and that bound is a property of the deployment rather than of the rule.
The README has a Caching in CI section, which addresses the obvious objection to loading lints from git on every run: the lints are built like dependencies, and a CI cache for that build output is what makes repeated runs affordable.
The repository layout shows the scale of the machinery. There is cargo-dylint/ for the cargo subcommand, dylint/ for the tool itself, dylint-link/ for the linking step, driver/ and internal/ for the parts that need specific Rust components and are excluded from the main workspace, nightly/ and scheduled/ for the linting of the Rust toolchain itself, expensive/ for the slower checks, utils/ with a linting workspace, and fixtures/, scripts/, utils/ and a docs/ directory. The workspace.dependencies list is long, with compiletest_rs for the UI tests, rustc-stable-hash for hashing compiler artefacts, syntect for syntax highlighting, and supply_chain for dependency verification.
Where Dylint fits, and where it stops
The comparison worth making is with Clippy, and the difference is about who owns the rules rather than about what the rules do.
Against Clippy, Dylint wins on the case where a rule is specific to your codebase. A rule that forbids a particular pattern in your organisation, or encodes a convention that only makes sense in your project, is awkward in Clippy because the natural route is an upstream contribution, which means writing it in Clippy's style, arguing for it, and waiting for a release. In Dylint it is a crate in your own repository, versioned with your code, loaded by a config line.
The cost is real and it is in the dependencies and the toolchain coupling. The lints run against rustc internals through the LateLintPass trait and the pre-expansion mechanism, and the conditional compilation story above shows the seams. A lint library is bound to a rustc version in ways that a normal crate is not, so a lint collection is something you may have to rebuild and re-pin more often than a normal dependency.
The second limit is that this is a security research tool in origin, and the repository is organised accordingly. There is a CODEOWNERS file, a supply_chain dependency, a .dockerignore, a nightly/ directory for linting the toolchain and a scheduled/ directory. The nightly and scheduled directories exist because lints that check the Rust toolchain itself can only run against a specific toolchain build.
The third is that Dylint does not replace Clippy. The README's framing is that Dylint is similar to Clippy, and the example lints in the repository are meant to be run alongside it. The general-purpose example lints in examples/general are the ones you would think of as the common cases, and the restriction examples such as try_io_result are the ones that forbid something entirely, which is a category a general linter cannot express well because the restriction has to be known to the rule, not inferred from a pattern.
For a team that has decided a rule belongs in Clippy, this tool adds a layer rather than removing one. For a team that has a dozen internal conventions it wants enforced everywhere, it is the difference between a wiki page nobody reads and a check that fails the build.
Editorial conclusion
Dylint suits a Rust team that wants to own its lint rules as versioned code rather than contribute them upstream, since a lint library is an ordinary crate that is downloaded, built and loaded, and the rules can be shared across a workspace or an organisation through git references. It is the wrong tool if you only want the standard lints, because Clippy already provides those, and it is the wrong tool if you cannot accept a lint rule that depends on the nightly compiler internals, since the mechanism is tied to rustc internals. Before adopting it, write one lint with cargo dylint new and confirm it loads, then decide whether the lint libraries belong in the same repository as the code they check.
Frequently asked questions
How do I install Dylint?
Run cargo install cargo-dylint dylint-link. The dylint-link crate is the linking step that lets a lint library be built against the same rustc that will run it, and both are needed.
How do I run Dylint on a workspace?
Run cargo dylint --git https://github.com/trailofbits/dylint --pattern examples/general, which finds the lint libraries on the command line. For regular use, declare them in workspace.metadata.dylint.libraries in Cargo.toml or dylint.toml instead, and run cargo dylint --all.
How do I write a Dylint lint?
Scaffold one with cargo dylint new new_lint_name, which produces a loadable library, then implement the LateLintPass trait and accommodate the symbols asking to be filled in. Build it with cargo build and confirm it is found with cargo dylint list --path .
How do I stop Dylint lints from producing unknown lint warnings?
Dylint passes --cfg=dylint_lib="LIBRARY_NAME" to the compiler for each library in use, and you use it in a cfg_attr attribute so the allow only applies when the lint library is loaded. For pre-expansion lints the README says the only known workaround is to allow the compiler's built-in unknown_lints lint, and you also need a check-cfg entry for cfg(dylint_lib, values(any())) in Cargo.toml.
How do I configure a Dylint library?
Include a dylint.toml file in the root of the linted workspace, encoding a TOML table whose keys are library names. The README's example sets the non_local_effect_before_unhandled_error library's work_limit to 1_000_000, and the value in the table is interpreted according to the library.
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/trailofbits-dylint)Was this useful?