Mollify, Agents, and Integration
For most of the last decade, Python tooling moved in one direction, and that direction was modular. The Unix philosophy won, small sharp tools proliferated, and answering a simple question about a codebase became an exercise in assembly. If you wanted to know what was unused you reached for vulture. Lint was ruff. Dependency hygiene was deptry, import boundaries were tach, complexity was radon, duplication was jscpd, and security was bandit. Each of these does its one job well, which is exactly why the arrangement felt like progress rather than fragmentation.
The cost of that arrangement is easy to miss because it is not in any single tool. It is in the seams. Seven tools means seven configuration files, seven output formats, seven notions of severity, and no shared answer to the most basic question of all, which is what counts as a finding. A human absorbs that overhead without complaining, because a human is good at squinting at seven kinds of output and building one mental model out of them. The overhead only becomes visible when the reader is no longer a human.
That is the argument I want to make here, and Favio and I built Mollify around it. Modular tool stacks are the right answer right up until the moment the job they are being asked to do gets harder than snapping modules together can satisfy. Clayton Christensen made this point about hardware and Ben Thompson has made it about software for years: when a product is not yet good enough for what is being asked of it, the integrated solution wins, because integration is what lets you optimize across the seams that modularity leaves exposed. Coding agents are the thing that just made the job harder, and the seams are where they fall down.
The integrated pass
Mollify is a Rust engine that runs the equivalent of that whole stack in a single deterministic pass, with one configuration file and one output contract. In practice there is one command worth memorizing:
mollify audit --path /your/python/project
audit runs every engine and prints a quality score from 0 to 100 on top of the findings:
Mollify audit — /your/project
Quality score: 84/100
12 finding(s) across 47 file(s) — 0 error, 12 warn
src/app.py:6 [warn/certain] unused-export — function `_legacy` has no reachable references (unused-export:931a82e6d41f07c3)
src/api.py:88 [warn/likely] high-complexity — function `handle` is complex (cyclomatic 14, cognitive 19) (high-complexity:1aa9…)
src/db.py:1 [warn/certain] circular-dependency — import cycle: db → models → db (circular-dependency:7c…)
pyproject.toml:1 [warn/likely] unused-dependency — declared dependency `rich` is never imported (unused-dependency:93…)
Installing it is deliberately boring, which is part of the point, because every channel ships the same self-contained binary:
uvx mollify audit # one-off via uv, no install
uv tool install mollify # persistent, on your PATH
pip install mollify # or the pip route
cargo install mollify-cli # from crates.io, binary is `mollify`
The audit command is really a front end over a set of engines you can also run individually, and each maps to one of eight categories. The full surface is wider than this, but the shape of it is:
| Area | Command | A sample of the rules |
|---|---|---|
| Dead code | mollify dead-code | unused-file, unused-export, unused-import, unused-method, unreachable-code, commented-code |
| Dependency hygiene | mollify deps | unused-dependency, missing-dependency, transitive-dependency, misplaced-dev-dependency |
| Architecture | mollify arch | circular-dependency, layer-violation, forbidden-import, private-import, custom policies |
| Complexity | mollify complexity | high-complexity, hotspot (churn times complexity), low-cohesion |
| Duplication | mollify dupes | duplication, via an exact suffix-array clone finder |
| Type health | mollify types | untyped-function, private-type-leak |
| Security | mollify security | eval, shell, SQL injection, weak crypto, secrets, each carrying a CWE id |
None of these individual capabilities is novel. vulture already finds dead code and bandit already finds security candidates. What is different is that they arrive as one evidence stream, ordered deterministically, under one contract, which turns out to matter far more than any single rule.
Why the discipline comes first
There is a failure mode that integrated tools fall into, and it is worth naming because avoiding it is most of the work. When you unify eight kinds of analysis, you also unify eight opportunities to be confidently wrong, and a tool that cries wolf across eight categories at once is more annoying than seven honest tools that each cry wolf in their own corner. So the governing rule of the project is that no analysis, and specifically no AI, is allowed to invent a finding. Every result is a piece of deterministic evidence with a stable fingerprint, a confidence tier, and a reason a person can read.
This is not modesty for its own sake. Dead-code detection in Python is undecidable in the general case, because getattr, eval, and importlib can conjure a reference out of a string at runtime, and pretending otherwise is precisely how static analyzers earn their reputation for noise. Mollify tiers every verdict instead of hiding the uncertainty. A finding marked certain is provable, a private symbol with no reachable reference and no dynamic dispatch in scope, and only those are ever auto-fixed. A likely finding is a strong static signal with some residual runtime risk. An uncertain finding sits on a public surface or near dynamic dispatch, and is reported for a human to judge, nothing more. The quality score then weights each finding by its confidence, so a report full of low-confidence review items does not get scored like one full of proven defects.
The practical upshot is that you can act on the tool at whatever level of trust you want:
mollify audit --min-confidence likely # drop the review-only candidates
mollify fix # preview safe removals of certain dead code
mollify fix --apply # actually write them
Framework awareness is part of the same discipline. Mollify understands Flask, FastAPI, Django, Celery, pytest, click, and Pydantic decorators, which means a route handler decorated with @app.route is not reported as dead just because nothing in the project calls it by name. That single piece of understanding removes the most common false positive that makes people turn dead-code tools off, and it is only possible because the analysis is integrated enough to reason about reachability and decorators together.
The contract is the product
Here is where integration stops being a convenience and becomes the actual value. Every Mollify command emits the same kind-discriminated envelope, so a client, whether that is a shell script, a CI job, or an agent, switches on kind and walks findings[] without caring which engine produced them:
{
"kind": "audit",
"schema_version": "0.1",
"quality_score": 84,
"summary": { "total": 12, "errors": 0, "warnings": 12, "files_analyzed": 47 },
"findings": [
{
"fingerprint": "unused-export:931a82e6d41f07c3",
"rule": "unused-export",
"category": "dead-code",
"severity": "warn",
"confidence": "certain",
"reason": "function `_legacy` has no reachable references in the project",
"location": { "path": "src/app.py", "line": 6, "end_line": 7 },
"actions": [
{
"type": "remove-symbol",
"description": "Delete unused function `_legacy`",
"auto_fixable": true,
"suppression_comment": "# mollify: ignore[unused-export]"
}
]
}
]
}
Because the output is deterministic, identical input produces byte-identical output, which is the property that makes the contract worth building on. You can diff two reports, cache one, or hand one to a program and trust that nothing reordered itself between runs. A modular stack cannot offer this, not because any one tool is careless, but because there is no single artifact to make deterministic in the first place.
Configuration follows the same logic of one file rather than seven. A .mollifyrc.json at the project root sets severities, thresholds, and architecture in one place:
{
"severity": { "dead-code": "error", "duplication": "warn", "unused-dependency": "off" },
"ignore": ["tests/", "migrations/"],
"max_cyclomatic": 10,
"architecture": { "layers": ["api", "service", "domain", "infra"] },
"policies": [{ "id": "no-requests-in-domain", "forbid_import": "requests", "in_paths": ["domain/"], "severity": "error" }]
}
Raising a rule to error is what makes CI block, and the policies block lets you encode a rule like “the domain layer stays free of I/O” as a deterministic check rather than a comment someone leaves on a pull request and everyone forgets.
CI is where the single contract earns back the most time. The flag that matters is --gate new-only, which attributes findings to changed files against a base branch and reports only what the current change introduced, so existing debt does not block every merge:
name: mollify
on: [pull_request]
permissions:
contents: read
security-events: write
jobs:
mollify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with: { fetch-depth: 0 }
- uses: dtolnay/rust-toolchain@stable
- run: cargo install mollify-cli
- run: mollify audit --gate new-only --base origin/$
- if: always()
run: mollify audit --format sarif > mollify.sarif
- uses: github/codeql-action/upload-sarif@v4
if: always()
with: { sarif_file: mollify.sarif }
If you would rather not lean on git for the diff, regression baselines do the same job off a content-derived snapshot, and because the fingerprints are derived from content rather than position, a baseline survives file moves and renames:
mollify audit --save-baseline .mollify/baseline.json # once, on a clean main
mollify audit --baseline .mollify/baseline.json --fail-on-regression # in CI
The new reader
Everything above would still be a reasonable argument if coding agents did not exist. It would be the familiar case that integration is a bit more convenient than assembly. What actually raises the stakes is that the reader has changed, and this is the part I find most interesting.
A coding agent spends a remarkable share of its effort reconstructing facts that a static analyzer already knows for certain. Is this function used anywhere. What imports this module. Is this dependency actually needed. The agent answers those questions by grepping and inferring, and grep is exactly the wrong instrument, because it misses dynamic dispatch, re-exports, and framework wiring, and then acts on its own confident guess by deleting something that was load-bearing. The agent is not the problem. The problem is that it is being forced to rebuild ground truth from text every single time, and text is a lossy source.
This is the demand that makes an integrated, deterministic tool valuable in a way it might not have been five years ago. An agent does not want to run seven tools and reconcile seven formats any more than you do, and it has far less patience for a source of truth that reorders itself between runs. It wants one call that returns structured, tiered evidence it can act on. So Mollify exposes exactly that, an MCP server, started with a single command:
mollify mcp # stdio MCP server for coding agents
You rarely run that yourself. The more useful entry point scaffolds version-matched skills, rules, hooks, and MCP configuration straight into a repository, in whatever form the agent expects:
mollify init --agent claude # or cursor, gemini, codex, cascade
mollify init --all # every supported agent at once
For Claude Code that writes an .mcp.json registering the server as a tool, a .claude/skills/mollify/ skill that teaches the agent the JSON contract and the meaning of the confidence tiers, slash commands under .claude/commands/, and hooks in .claude/settings.json so an audit can run automatically and block when your rules say error. Cursor gets .cursor/rules/mollify.mdc and its own mcp.json, Gemini gets GEMINI.md and .gemini/settings.json, Codex gets AGENTS.md and .codex/config.toml, and Devin and Cascade get their respective skills and workflows. The important detail is that these files are meant to be committed, which means every teammate’s agent reads the same deterministic ground truth rather than each one improvising its own from grep.
The confidence tiers do real work in this setting too. Because only certain findings are auto-fixable, an agent with an auto-fix hook can remove provably dead code and leave the likely and uncertain candidates for a person, which is the correct division of labor between a machine that should never guess and a human who is allowed to.
The wedge
It is worth being precise about the claim, because the temptation is to overstate it. Mollify is not a better dead-code detector than vulture, and it is not trying to be. The individual tools each do their piece well and will keep doing so. The wedge is the unified deterministic pass under one contract, which is a different product category than any of them, and the reason that category is worth building now rather than in 2018 is that a new kind of consumer showed up who genuinely needs it.
| vulture | ruff | deptry | tach | radon | jscpd | bandit | Mollify | |
|---|---|---|---|---|---|---|---|---|
| Whole-project dead code | yes | yes | ||||||
| Dependency hygiene | yes | yes | ||||||
| Circular deps and boundaries | yes | yes | ||||||
| Complexity and hotspots | ~ | yes | yes | |||||
| Duplication | yes | yes | ||||||
| Security candidates with CWE | ~ | yes | yes | |||||
| One deterministic pass and MCP contract | yes |
Mollify is early but real. It is published on PyPI and crates.io, it carries more than 210 tests, and it runs against itself in CI. Under the hood it is a Cargo workspace that flows from parse to graph to engines to report, built on Astral’s ruff_python_parser so the same distribution builds everywhere, with real LEGB scope resolution so a shadowing local never masks a dead top-level symbol, and a linear-time suffix array for exact duplication rather than hash-collision guessing. Those are the details that let the integrated pass stay honest.
If you work on a Python codebase of any size, uvx mollify audit is a thirty-second look at what it finds. And if there is an agent anywhere in your workflow, mollify init --agent is the more interesting experiment, because it is a small bet on the same thesis the whole tool is built on, which is that the codebase should hand its truth to the machine instead of making the machine guess. The code, and the docs these examples came from, are at github.com/FavioVazquez/mollify.
Enjoy Reading This Article?
Here are some more articles you might like to read next: