AI agents copy. That is what they do.
Ask Claude Code for a new screen. It writes a fresh loading widget. It writes a fresh error card. It writes a fresh date formatter. All three already exist in your app. The agent never looked.
Do that for three months and your Flutter app carries four versions of the same thing. Fix a bug in one. The other three keep it.
I hit this on my own repos. So I went looking for a tool that reads Dart. I found Deslop.
What Deslop does
Deslop finds duplicate functions and repeated code. It reads nine languages: Dart, C#, Rust, Python, JavaScript, TypeScript/TSX, PHP, F# and Go.
Dart is on that list. That is rare. Most clone detectors stop at JavaScript and Python.
It ships three ways to use it. A CLI. A VS Code extension with live warnings. An MCP server your coding agent can call before it writes.
Nimblesite builds it. The code sits on GitHub.
It parses your code, it does not grep it
Most copy-paste detectors match text. They break the moment someone renames a variable.
Deslop parses. Each language ships a tree-sitter grammar. The parser builds an AST. The docs are blunt about it: "No source-level regex touches this pipeline — ever."
So these two Dart functions collapse to the same shape:
String formatUserLabel(User u) => '${u.firstName} ${u.lastName}';
String buildAuthorName(Author a) => '${a.firstName} ${a.lastName}';
Different names. Same code. Deslop catches it.
The three scores fuse into one. Pairs cross a FUSED_THRESHOLD of 0.85 or they die. Survivors group by transitive closure. If A matches B and B matches C, all three land in one cluster.
It tells you what to fix first
A raw list of 400 clones helps nobody. Deslop ranks them:
weight = clone_node_count × (cluster_size − 1) × log2(1 + spanned_bytes)
Bigger fragments score higher. More copies score higher. The log term keeps one huge generated file from swallowing the whole report.
You get the worst offenders at the top. Each cluster anchors to one copy so you can diff the others against it.
That is the part I care about. I do not want a score. I want the next five things to delete.
Run it on a Flutter project
Install the CLI:
brew install nimblesite/tap/deslop
On Windows, use Scoop:
scoop bucket add nimblesite https://github.com/Nimblesite/scoop-bucket
scoop install deslop
Then point it at your repo:
deslop .
It writes three reports into .deslop/: JSON, plain text, and an HTML page you can open in a browser. Add .deslop/ to your .gitignore.
For a Flutter app, exclude the generated files first. Otherwise your .g.dart and .freezed.dart files drown the report. Create .deslop.toml at the root:
[defaults]
exclude = ["**/build/**", "**/.dart_tool/**"]
report_hide = ["**/*.g.dart", "**/*.freezed.dart", "**/*.gr.dart"]
[threshold]
max_duplication_percent = 5.0
exclude skips the files. report_hide still counts them but keeps them out of your face.
If you want warnings while you type, install the extension instead:
code --install-extension nimblesite.deslop-live
It works in VS Code, Cursor, Windsurf, VSCodium and GitHub Codespaces.
Stop the copy before it lands
This is the part that matters if you code with an agent.
Deslop ships an MCP server. It exposes nine tools. One of them, find-similar, belongs in the writing loop. The agent calls it before it writes a function. If something close already exists, the agent reuses it.
Wire it into Claude Code:
claude mcp add deslop -s user -- deslop-mcp --root .
The same setup works for Claude Desktop, Cursor and any other MCP client. The analysis refreshes as you edit. No batch run.
The other eight tools are read-only report queries. top-offenders for the ranked list. cluster-by-id for one group. Handy for a cleanup session, not for the inner loop.
If MCP is not available, the agent falls back to the CLI. Same pipeline, same JSON.
I run Claude Code on my Flutter projects daily. I wrote about that workflow here. Adding a duplicate check to it changed the output. The agent stopped writing its fourth snackbar helper.
Gate it in CI
Local checks slip. Put a ceiling in the pipeline:
name: deslop
on: [push, pull_request]
jobs:
duplication-gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: Nimblesite/Deslop@v0.32.0
with:
fail-over: "5.0"
Cross 5% duplication and the step fails with exit code 3. The message names your measured percentage and your ceiling.
Two inputs earn their keep. only-changed reports clusters touched by the diff, so a legacy pile does not block every PR. upload-artifact pushes the HTML report as a build artifact.
The action posts no PR comment on its own. You read duplication-percent from the step outputs and post it yourself.
Pin the version. Do not float on a tag.
What it will not do
It finds duplicates. It does not fix them. You still read the diff and decide.
That is correct. Two functions can look identical and mean different things. A price formatter and a weight formatter share every line and must stay apart. A machine cannot judge that. You can.
It also flags generated code unless you tell it not to. In a Flutter repo that is most of your report on day one. Write the config first.
And on a first run over a large app, expect a long list. Take the top ten by weight. Ignore the tail.
Why I bothered
Duplicate code is not a style problem. It is a bug multiplier.
Every copy is a place where the fix did not land. You patch the auth retry in one repository class and the other two keep failing. You find out from a user.
Agents make this worse, fast. They write more code per hour than you ever did, and they have no memory of what your repo already holds. Volume goes up. Awareness goes down.
Deslop closes that gap. It gives the agent a way to ask "does this exist already?" and gives you a ranked list of what to delete.
Get it at deslop.live. The CLI and the extension are on GitHub, the docs are short, and it reads Dart.
If you want your Flutter codebase to start clean, ApparenceKit ships the structure that keeps it that way. One theme system, one router, one API layer. Fewer places for a fourth copy to hide.