Dead code & unused dependencies (DCA)
How Snitch flags packages declared but never imported, and source files that no other file references.
The Snitch CLI and GitHub Action ship with a deterministic dead-code analysis pass alongside the AI code review and the SCA dependency scan. It looks for two cleanup signals:
- Unused dependencies — packages declared in your manifests but never imported anywhere in source
- Dead files — source files (JS/TS + Python) that no other file imports
It is on by default. No config, no extra subscription.
Why a security tool cares
This is not just code-quality nitpicking. Both signals are real attack surface:
- Unused dependencies still install. They still execute postinstall scripts. If a CVE lands in one, you ship the vuln for zero benefit. Removing them costs nothing and shrinks your supply chain.
- Dead files still ship to users in your bundle. If a vuln later lands in dead code (a dormant route handler, a lazy-loaded module a framework still discovers), attackers can hit it. Reviewers don't audit dead branches; auditors don't either.
What we cover
Unused dependencies — all 8 ecosystems:
| Ecosystem | Manifest parsed |
|---|---|
| npm | package.json (dependencies + devDependencies + peerDependencies) |
| PyPI | pyproject.toml (PEP 621 + Poetry), requirements.txt |
| Go | go.mod (direct only, skips // indirect) |
| Rust | Cargo.toml ([dependencies], [dev-dependencies]) |
| RubyGems | Gemfile (top-level gem declarations) |
| Maven | pom.xml (direct <dependency> blocks) |
| Packagist | composer.json (require + require-dev) |
| NuGet | *.csproj (<PackageReference>) |
We read the higher-level manifest, not the lockfile, so transitive deps don't pollute the report. You can only remove what you declared.
Dead files — JS/TS + Python only (for now):
For each source file, we extract the relative imports it makes, resolve them to other files in the repo, and build a directed graph. We then walk forward from entry points and any file not reached is flagged as dead.
Entry points include: index.{ts,tsx,js,jsx,mjs,cjs}, main.{ts,tsx,js,jsx}, __init__.py, __main__.py, main.py, setup.py, conftest.py, anything matching test patterns (*.test.*, *.spec.*, tests/, __tests__/, test_*.py, *_test.py), Next.js / TanStack route conventions (pages/, app/page.tsx, src/routes/), and any path declared in package.json main / bin / exports.
How findings render
In the sticky PR comment, a new 🧹 Dead code & unused dependencies section sits alongside the SCA + code-review sections. Inside:
- Unused dependencies — collapsible table grouped by ecosystem, one row per package + manifest path. Suggested fix: remove from the manifest.
- Dead files — collapsible list of files with no inbound imports. Suggested fix: delete or wire it up.
Severity is Low for both — dead code rarely breaks anything, but it expands attack surface for free.
In SARIF / GitHub Code Scanning, each surfaces as its own alert with the same severity and a fingerprint stable across runs.
Disabling DCA
Some teams already run knip, vulture, cargo-udeps, or framework-specific dead-code linters and don't want the duplication. Both surfaces have an opt-out:
GitHub Action — set the input to false:
- uses: snitchplugin/snitch-github-action@v1
with:
snitch-license-key: ${{ secrets.SNITCH_LICENSE_KEY }}
include-dead-code: false
CLI — pass --skip-dca on a single run, or set SNITCH_SKIP_DCA=1 in your environment to disable globally:
snitch scan --skip-dca
SNITCH_SKIP_DCA=1 snitch scan
What it doesn't catch (and why)
- Dynamic imports we can't resolve statically —
import(varName)or__import__(name)looks like a missing reference and we silently drop it from the graph. If a file is only imported dynamically by a string the analyzer can't see, it'll show up as dead. Confidence rating ismediumfor dead-files for this reason. - Framework auto-discovery not in our list — Django views, Rails controllers, Spring beans get discovered at runtime by path. We hardcode the common conventions (Next.js, TanStack, tests, package.json
main); if your framework uses a custom router we don't recognize, we'll flag legit files. The 50-finding cap protects against catastrophic false-positive runs. peerDependenciesdeclared but only resolved by hosts — npm peer deps are not actually installed at the declaring package; treating them as "unused" would be wrong. We currently include them in the source-of-truth list — likely a small false-positive source, low priority.- Test runners and build tools referenced only by config —
vitest,webpack,tsc,pytestaren'timported in source code; they're invoked by configs (vite.config.ts, package.json scripts). We treat package.json scripts as inputs to the import set so common cases work; some configs (e.g. tooling configs in YAML) we don't currently scan.
Plugin (skill mode)
The Plugin variant of Snitch (the one that drops into Claude Code, Cursor, Codex) doesn't make file-system walks across the repo, so the deterministic graph build isn't available. Instead, methodology category 70 instructs the AI to enumerate every entry in the project's package manifests and grep the source for matching imports — anything declared but never referenced is a v1 unused-dep candidate. Precision is lower than the deterministic CLI / Action path; if you need true dead-file analysis, run the CLI on the same repo.
Configuration TODO (not yet shipped)
- Per-repo
.snitch.ymlwith adead-code-ignoreglob list — coming soon. For now you can suppress individual files by importing them from a known entry point even minimally (e.g.,import "./legacy/x.ts";at the top ofsrc/index.tswill mark the file reachable but is admittedly a hack). - Per-finding inline ignore comments (
// snitch-allow: dead-file) — also coming soon.