diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c963ad..33cd73d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ All notable changes to Diskern are documented here. The format follows ## [Unreleased] +### Added + +- npm download caches are now classified as safe package-manager cache files + ### Fixed - The CLI scan summary now says "1 file" instead of "1 files" when a diff --git a/crates/diskern-core/rules/base.json b/crates/diskern-core/rules/base.json index c6a5b57..689ad0f 100644 --- a/crates/diskern-core/rules/base.json +++ b/crates/diskern-core/rules/base.json @@ -57,6 +57,13 @@ "verdict": "safe", "description": "pip download cache. Packages are re-downloaded on demand." }, + { + "id": "npm-cache", + "patterns": ["**/.npm/_cacache/**", "**/appdata/local/npm-cache/**"], + "category": "package_manager_cache", + "verdict": "safe", + "description": "npm's download cache. Re-fetched on demand; removing it only makes the next install download packages again." + }, { "id": "unix-system-logs", "patterns": ["/var/log/**", "/private/var/log/**"], diff --git a/crates/diskern-core/src/rules.rs b/crates/diskern-core/src/rules.rs index 0dcf289..0c7ad32 100644 --- a/crates/diskern-core/src/rules.rs +++ b/crates/diskern-core/src/rules.rs @@ -431,6 +431,38 @@ mod tests { } } + #[test] + fn npm_cache_is_safe_to_remove() { + let db = RulesDb::embedded(); + for path in [ + "/home/u/.npm/_cacache/content-v2/sha512/aa/package", + "/Users/u/.npm/_cacache/index-v5/ab/cd", + "C:\\Users\\u\\AppData\\Local\\npm-cache\\_cacache\\content-v2\\sha512\\aa\\package", + ] { + let (cat, verdict, rule) = db.classify(std::path::Path::new(path)); + assert_eq!( + cat, + Category::PackageManagerCache, + "{path} matched {rule:?}" + ); + assert_eq!(verdict, Verdict::Safe, "{path} matched {rule:?}"); + } + } + + #[test] + fn npm_state_outside_the_download_cache_is_not_marked_safe() { + let db = RulesDb::embedded(); + for path in [ + "/home/u/.npmrc", + "/home/u/.npm/_logs/2026-09-12-debug.log", + "C:\\Users\\u\\AppData\\Roaming\\npm\\npmrc", + ] { + let (cat, verdict, rule) = db.classify(std::path::Path::new(path)); + assert_eq!(cat, Category::Unknown, "{path} matched {rule:?}"); + assert_ne!(verdict, Verdict::Safe, "{path} matched {rule:?}"); + } + } + /// An installed application's own repair binary is not a reclaimable /// download. `unknown` is the right answer: report::build drops those, /// so it never reaches the user as an actionable row.