scarno

scarno: Cut the Dead Wood Before It Catches Fire
Back in March I wrote about dependency pruning and tree shaking, and I closed that post with a wager: pick your three highest-value applications, run a dependency analysis, and I’d bet at least 10% of the declared dependencies have zero references in your source code. Several of you took the bet, ran mvn dependency:analyze or pipreqs, and discovered the reason nobody trusts those tools: they flagged dependencies as unused that were wired in via Spring @Autowired, loaded through Class.forName(), or activated by declarative config. You’d been burned before. You ignored the output. The dead wood stayed.
That post described the tool I wanted to exist. Today it does, and it’s yours: scarno is open source under Apache-2.0, on PyPI, and on the GitHub Marketplace.
pip install scarno
Key takeaways
- scarno identifies genuinely unused dependencies across Python, Java/Kotlin, JavaScript/TypeScript, Go, C#/.NET, and CSS, without the framework-blind false positives that make existing tools untrustworthy.
- Every dependency is classified with evidence as SAFE, UNCERTAIN, IN_USE, or UNDECLARED; scarno never guesses and never silently deletes.
- The entry-point surface report shows which of a dependency’s public symbols you actually call, and how often, so a 200-class library used for one utility method has nowhere to hide.
- A JVM cross-version ABI diff catches diamond dependencies where a symbol your code calls differs between the declared and resolved versions, before production finds out for you.
- Every release ships with SLSA Build Level 3 provenance. Verify it; don’t trust me.
Why Another Dependency Tool?
Because the existing ones make a structural mistake: they scan for imports, and modern frameworks don’t work through imports alone. Dependency injection, reflection, annotation processing, and declarative configuration all consume dependencies invisibly to surface-level scanning. The result is a report full of false “unused” findings, and a tool that cries wolf gets ignored, which means the genuinely unused dependencies (each one CVE surface, CI time, and cognitive load) never get removed.

scarno approaches it the other way around: reachability in reverse. It performs static and bytecode analysis for direct, reflective, and annotated usage; it understands Spring @Autowired, @Bean, and @Component, Guice @Inject, and the Python equivalents; and where it sees dynamic loading (Class.forName(), importlib, __import__()) it does something most tools refuse to do: it admits uncertainty.
Four States, Not Two
Every dependency in the report lands in one of four states, each with the evidence attached:
SAFE means no reference of any kind was found: not a call, not an annotation, not a reflective hint. These are your removal candidates. IN_USE means scarno can show you exactly where and how the dependency is consumed, down to per-symbol usage counts. UNCERTAIN means dynamic loading or reflection is in play and static analysis cannot honestly resolve it, so scarno tells you that rather than guessing in either direction. UNDECLARED is the one that surprises people: imports that resolve to installed packages which appear in no dependency file at all, the phantom dependencies that work on your machine and break in a clean build.

The four-state model is the trust mechanism. A tool that only says “unused” or “used” has to gamble on the hard cases, and every wrong gamble costs it your confidence. scarno’s UNCERTAIN state is a design decision: the tool would rather be honestly incomplete than confidently wrong. For the same reason, scarno never modifies anything. It reports; you decide.
Here’s what that looks like in the Markdown report’s dependency tree, rendered with diff colouring so the verdicts are visible at a glance (red for safe to remove, orange for uncertain or undeclared, plain for in use):

The One-Method Dependency
My favourite report scarno produces is the entry-point surface area. For every direct dependency it lists the public symbols the package exposes and marks which ones your code actually calls, with a tally: requests.get used 23x. Which means you can finally see the 200-class library you pulled in for a single utility method, or the dependency subtree that exists to serve one call site. Sometimes the right answer is removal. Sometimes it’s replacement with something narrower. Quite often it’s the discovery that the same method already exists in another library or framework you’re already using, or in the standard library itself, and the dependency was pure duplication all along. And sometimes it’s a shrug and a documented decision. But for the first time the decision is informed by evidence rather than archaeology.
Diamond Dependencies and the ABI Diff
There’s one analysis in scarno that goes beyond pruning, and it’s the one I’d run first on any mature JVM estate. You probably know the diamond dependency problem: somewhere upstream a version gets pinned, never unpinned, and two versions of the same library end up resolving into your build. The usual framing is that this causes maddening runtime errors. The security framing is worse: a caught NoSuchMethodError sends execution down a fallback path that no single-version analysis ever examined.

scarno’s --deep-inspection mode performs a cross-version ABI diff: it compares the declared and resolved versions of each dependency and flags when a symbol your code actually calls differs between them. Not “these versions differ”, which is trivia, but “these versions differ at a call site you exercise”, which is a finding. It’s cache-first against your local artefact store, and if you supply a package index it will fetch what’s missing and build a local cache, with every remote fetch disclosed and audited in the report.
That last part deserves a sentence more, because it’s a design position. Any finding whose verdict depended on a remotely fetched artefact is tagged provenance="remote" and is advisory by default: it will not fail your CI gate unless you explicitly opt in. If the bytes scarno analysed could have been influenced by an attacker (a compromised index, a typosquatted coordinate in a hostile manifest), then gating on the verdict would hand that attacker your build status. Deterministic where determinism is possible; clearly labelled inference where it isn’t. If you’ve read my writing on controls, you’ll recognise the shape.
Built for the Pipeline
scarno is CLI-first with four output formats: human-readable text, full JSON for programmatic consumption, a Markdown checklist format built for PR descriptions, and SARIF 2.1.0 so findings land in GitHub Code Scanning, SonarQube, or GitLab alongside your other security results. Exit code 3 signals HIGH or CRITICAL supply-chain findings (scarno also detects suspicious patterns like runtime pip install via subprocess, curl | sh in container recipes, and exec of network responses), so you can gate CI on supply-chain risk independently of cleanup.
And if you’d rather not touch the CLI at all, the GitHub Action wraps the whole thing: SARIF auto-upload, sticky PR comments, and workflow annotations.

The minimal setup is genuinely minimal, because the defaults do the right thing (SARIF format, upload to Code Scanning, sticky PR comment, gate on HIGH):
name: scarno
on: [pull_request]
permissions:
security-events: write
pull-requests: write
jobs:
scarno:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: BrettCrawley/scarno@v1
What It Doesn’t Do
In the spirit of every tool post I write: the limitations are documented first-class, in the repo, in a file called exactly what you’d expect. Type inference is deliberately shallow (direct annotations and declarations, not chained-call return types or generics). Reflective invocation flags as UNCERTAIN rather than resolved, by design. CDN-loaded browser dependencies get inventory but not per-symbol attribution. Auto-removal doesn’t exist and won’t until the analysis has earned that trust. Read LIMITATIONS.md before you rely on the output; that’s what it’s for.
Verify This Release: Don’t Take My Word for It
In my SLSA and provenance post I argued that provenance is one of the few supply-chain controls that is actually deterministic: you can verify it, rather than trusting a claim. It would be a poor show to argue that and then ship you an unattested tool.
Every scarno release is built with SLSA Build Level 3 provenance: the artefact you install was built by a hardened, ephemeral builder from a specific commit of the public repository, and you can prove it:
gh attestation verify scarno-1.0.4-py3-none-any.whl --owner BrettCrawley

If the command fails, don’t run the tool. That’s the whole point: the control is the verification, not my assurance.
One honest note: SBOM-Graph doesn’t have this yet. It’s a monorepo with five Python subprojects, three container images, and a Helm chart, so bringing every release target to the same standard is a bigger pipeline job, and it’s in progress. I’ll write that one up when it’s done, scars and all.
Where scarno Fits
Earlier this week I published the case for estate-graph analysis: a deterministic graph of your whole software estate answering four questions. What exists. What’s reachable. Where taint flows. What’s duplicated. scarno is the second question. SBOM-Graph answers the first, and the integration between them (estate-wide detection routing to repo-level confirmation, reachability annotations flowing back into the graph) is designed and on its way. The third question ships next, with data.
Note to engineering leaders: I’d weigh odds my bet from March holds up, and now the tool to settle it is free. Pick your three highest-value applications, point scarno at them, and count the SAFE classifications. Every one is CVE surface you’re carrying, CI time you’re spending, and a line in your SBOM you’re accountable for, in exchange for nothing. The dead wood doesn’t remove itself. Now you can see exactly which branches to cut.
Stay tuned!
Tags: scarno, supply-chain-security, dependency-management, SBOM, SAST, open-source, estate-graph-analysis, SLSA