Your Clippy Config Should Be Stricter
“If it compiles, it works.” This feeling is one of the main things Rust engineers love most about Rust, and a reason why using it with coding agents is especially nice. After debugging some code that compiled but mysteriously stopped in production, I realized that it’s useful to enable more Clippy lints to catch bugs that the compiler won't prevent by itself. It's especially useful as guardrails for coding agents, but stricter linting can make your code safer, whether or not you’re coding with LLMs.
Motivating Bug: UTF-8-Oblivious String Slicing
Scour is the personalized content feed that I work on. Every Friday, Scour sends an email digest to each user with the top posts that matched their interests. On a recent Friday, the email sending job mysteriously stopped. This was puzzling because I had already put in place multiple type system-level safeguards and tests to ensure that it would continue with a log on all types of errors.
After digging into the logs, I found the culprit to be thread 'tokio-runtime-worker' panicked... byte index 200 is not a char boundary. A function naively truncated article summaries without checking for UTF-8 character boundaries, which caused a panic and stopped the Tokio worker thread running the email sending loop.
The solution for this particular bug was a safer method for truncating article summaries that respects UTF-8 character boundaries. However, this problem was reminiscent enough of the 2025 Cloudflare unwrap bug that "broke the internet" that I wanted some more general solution.
Rust's compiler prevents many types of bugs but there are still production problems it can't catch. Panics will either crash your program or quietly kill Tokio worker threads. Deadlocks and dropped futures can make work silently stop. And plenty of numeric operations can silently cause incorrect behavior.
We can stave off many of these types of bugs by making Clippy even stricter than it already is.
This is especially relevant in the age of coding agents. A seasoned Rust engineer might naturally avoid patterns that could cause problems. An agent or a junior colleague might not. Stricter Clippy rules make it easier to rely on code you didn't personally write. Also, enabling new lints on an existing codebase is tedious, and exactly the kind of task that is good to hand to a coding agent.
Enabling More Clippy Lints
Clippy ships with hundreds of lints that are disabled by default. Some are disabled because they might have false positives and some are style choices which you might reasonably not want.
Which lints should we enable to help us get back the "if it compiles [and passes Clippy], it works" feeling?
Why Not Enable Lint Categories?
Clippy's lints are grouped into categories: Correctness, Suspicious, Complexity, Perf, Style, Pedantic, Restriction, Cargo, Nursery, and Deprecated.
Unfortunately, none of these categories cleanly map onto "don't let this panic or do the wrong thing in production".
In fact, the Clippy docs say that "The restriction category should, emphatically, not be enabled as a whole." Clippy even includes a dedicated lint, blanket_clippy_restriction_lints, to discourage you from enabling this category. While the restriction category includes many useful lints, it also includes some that directly contradict one another. For example, it contains lints to enforce both big_endian_bytes and little_endian_bytes.
The docs say "Lints should be considered on a case-by-case basis before enabling". Of course, you can enable whole categories like pedantic and restriction and then allow specific ones you want to disable, but I'm outlining a selective opt-in here.
Lints That Don't Fire Are Still Useful
Even if you don't use a certain pattern in your code base today, it's not bad to enable the lint anyway. Inapplicable lints serve as cheap tripwires in case the given pattern is ever added later, whether by you, a colleague, or a coding agent.
My Lints
Every project is different and you should look through the available lints to see which ones make sense for your project.
Also, check when lints landed in stable if your Minimum Supported Rust Version predates 1.95, as some of these may have been added after your MSRV.
With those caveats out of the way, here are the lints I enabled, roughly categorized by what kind of behavior they prevent. You can skip to the bottom if you just want to copy my config.
Don't Panic
This group prevents panics from unwraps and unsafe slicing or indexing into arrays and strings.
Note that some of these, like string_slice and indexing_slicing may produce many warnings throughout your code base. That may be annoying to fix. However, using safe methods like .get() and iterators instead of slicing prevents pretty severe footguns, so I would argue that it's worth it.
string_slice-&s[a..b]on&str(UTF-8 boundary panic). This would have caught my initial bug.indexing_slicing-arr[i]/&arr[a..b]unwrap_used-Option::unwrap/Result::unwrappanic-panic!()callstodo/unimplemented/unreachable- placeholder-panic macrosget_unwrap-vec.get(i).unwrap()unwrap_in_result-.unwrap()inside functions that return aResultunchecked_time_subtraction-Instant - Instantpanics if the second is largerpanic_in_result_fn-panic!/assert!inside a function that returns aResult
You might or might not want to enable expect_used. Calling .expect on an Option or Result can result in a panic. However, the message you pass to expect should already document why that thing shouldn't happen. Enabling the lint and then selectively disabling it throughout your code with #[expect(expect_used, reason = "...")] may end up duplicating the same rationale for using it in the first place.
Another lint that is a real judgement call is arithmetic_side_effects. This can prevent overflows and division by zero. However, it will cause Clippy to warn you about every place you use math operators: +, -, *, <<, /, and %. I tried enabling it in my code base and would estimate that around 15% of the warnings caught real issues and 85% was just noise.
Don't Fail Silently
let_underscore_future-let _ = futuredrops without awaitinglet_underscore_must_use-let _ = result_returning()swallows errorsunused_result_ok-result.ok();silently dropsErrmap_err_ignore-.map_err(|_| MyErr)loses source errorassertions_on_result_states-assert!(r.is_ok())discards the error message
Don't Do Bad Async Stuff
These prevent various concurrency bugs and deadlocks:
await_holding_lock-MutexGuardacross.awaitawait_holding_refcell_ref-RefCell::borrow_mutacross.awaitif_let_mutex(only relevant if you're using an earlier edition than 2024) -if let _ = mutex.lock() { other_lock() }deadlock pattern. The scoping was fixed in the 2024 edition so this is no longer an issue.large_futures- aFuturethat is too large can cause a stack overflow
Don't Do Unsafe Things with Memory
mem_forget-mem::forgetleaksundocumented_unsafe_blocks- everyunsafe {}needs a// SAFETY:commentmultiple_unsafe_ops_per_block- one unsafe op per block (one comment per op)unnecessary_safety_doc/unnecessary_safety_comment- only document safety where it belongs
Don't Do Potentially Incorrect Things with Numbers
float_cmp-a == bon floatsfloat_cmp_const- stricter, also flags comparisons against constantslossy_float_literal- silently-rounded float literals (16_777_217.0_f32)cast_sign_loss-(-1_i8) as u64wraps tou64::MAXinvalid_upcast_comparisons-(x: i32 as i64) > i32::MAX as i64always false
The lints cast_possible_wrap, cast_precision_loss, cast_possible_truncation effectively force you to document invariants when doing lossy casts between numeric types. You might or might not find that useful.
Don't Do Bad Things That are Easy to Avoid
rc_mutex-Rc<Mutex<_>>(Rcis single-threaded)debug_assert_with_mut_call-debug_assert!(stack.pop().is_some())differs in debug vs releaseiter_not_returning_iterator- method namediterreturning non-Iteratorexpl_impl_clone_on_copy- manualCloneimpl that disagrees withCopyinfallible_try_from-TryFromimpl whose error isInfallibleshould beFromdbg_macro-dbg!calls should be removed after debugging
Don't allow Your Way Around These Lints
These two are especially useful if you're using a coding agent. Instead of letting the agent write #[allow(lint_we_wanted_to_enable)], it should provide a reason wherever it's disabling a lint.
allow_attributes- every#[allow]becomes#[expect(..., reason = "…")]allow_attributes_without_reason- every#[expect]requires a reason
Workaround for Workspace Inheritance
If you're using a Cargo workspace, you'll want to enable these lints in the workspace Cargo.toml. Unfortunately, each workspace crate needs to opt in to inheriting lints with lints.workspace = true, rather than inheriting the lints by default. On nightly, there's a missing_lints_inheritance lint that specifically checks for this.
If you're using stable Rust, you can use cargo-workspace-lints or a simple shell script run on CI to make sure you don't forget to make a workspace crate inherit the lints.
Warn or Deny?
When enabling lints, you can either set Clippy to warn or deny them. Either works but I personally prefer setting these to warn and running Clippy with -D warnings before committing and on CI. This makes local iteration marginally easier because you can compile your code initially without fixing all the lints right away.
My Configs
# Workspace Cargo.toml
[workspace.lints.clippy]
# Don't Panic - prevent panics from unwraps and unsafe slicing or indexing
string_slice = "warn"
indexing_slicing = "warn"
unwrap_used = "warn"
panic = "warn"
todo = "warn"
unimplemented = "warn"
unreachable = "warn"
get_unwrap = "warn"
unwrap_in_result = "warn"
unchecked_time_subtraction = "warn"
panic_in_result_fn = "warn"
# Optional - see post for caveats
# expect_used = "warn"
# arithmetic_side_effects = "warn"
# Don't Fail Silently - prevent dropped futures and swallowed errors
let_underscore_future = "warn"
let_underscore_must_use = "warn"
unused_result_ok = "warn"
map_err_ignore = "warn"
assertions_on_result_states = "warn"
# Don't Do Bad Async Stuff - prevent deadlocks and concurrency bugs
await_holding_lock = "warn"
await_holding_refcell_ref = "warn"
if_let_mutex = "warn" # only relevant on editions before 2024
large_futures = "warn"
# Don't Do Unsafe Things with Memory
mem_forget = "warn"
undocumented_unsafe_blocks = "warn"
multiple_unsafe_ops_per_block = "warn"
unnecessary_safety_doc = "warn"
unnecessary_safety_comment = "warn"
# Don't Do Potentially Incorrect Things with Numbers
float_cmp = "warn"
float_cmp_const = "warn"
lossy_float_literal = "warn"
cast_sign_loss = "warn"
invalid_upcast_comparisons = "warn"
# Optional - these effectively force you to document numeric invariants
# cast_possible_wrap = "warn"
# cast_precision_loss = "warn"
# cast_possible_truncation = "warn"
# Don't Do Bad Things That are Easy to Avoid
rc_mutex = "warn"
debug_assert_with_mut_call = "warn"
iter_not_returning_iterator = "warn"
expl_impl_clone_on_copy = "warn"
infallible_try_from = "warn"
dbg_macro = "warn"
# Don't `allow` Your Way Around These Lints - every suppression must be
# a deliberate #[expect(..., reason = "…")] rather than a silent #[allow]
allow_attributes = "warn"
allow_attributes_without_reason = "warn"
# Workspace clippy.toml
allow-indexing-slicing-in-tests = true
allow-panic-in-tests = true
allow-unwrap-in-tests = true
allow-expect-in-tests = true
allow-dbg-in-tests = true
Conclusion
Ultimately, as Clippy's docs say, "You can choose how much Clippy is supposed to annoy help you." But especially in the age of coding agents, I think it's worth tightening the guardrails so you end up with even fewer mysterious bugs in production and more code where you can say "if it compiles and lints, it should work."