PHP Architect logo

Want to check out an issue? Sign up to receive a special offer.

I Made My AI Agent Fix Its Own Copy-Paste (jscpd + Mago)

Posted by on August 15, 2026

Video version: https://youtu.be/ubhVleOpLQ0

It’s so easy to ask an AI agent to build a feature, and have it hand you 5,000 lines of code. It might even feel like a win until you start reading it so see the same validation block in four controllers, the same database-fetch boilerplate in six models, and that efvery method wraps its logic in the same try/catch with the same log message. It “works,” but you now own a maintenance nightmare, and reviewing it drains the life out of your afternoon.

LLMs are great at turning two sentences into thousands of lines of code, but that doesn’t mean it’s all unique code. A lot of the time it’s the same 100 lines repeated with tiny (if any) variations. The fix is to give the agent a fast feedback loop so it catches the duplication itself, before you ever see the diff. Today we’re pairing two near-instant tools: jscpd for finding copy/paste, and Mago for static analysis.

What Is jscpd?

jscpd (JavaScript Copy Paste Detector) is a fast, language-agnostic tool that scans your code for duplicated blocks. Despite the name, it handles PHP, Python, Go, and dozens of other languages by working with tokens rather than parsing each language’s full syntax.

You can run it without installing anything permanent:

npx jscpd ./app

Or install it globally if you’ll use it often:

npm install -g jscpd
jscpd ./app

Point it at a directory, and it reports back how much of your code is duplicated and exactly where the clones live:

Clone found (php)
 - ./app/Types/Strings/NonBlank.php [43:35 - 67:23] (25 lines, 198 tokens)
   ./app/Types/Strings/Url.php [61:23 - 85:23]
[snip]
Clone found (php)
 - ./app/View/Components/EpisodeStatus.php [59:5 - 66:21] (8 lines, 84 tokens)
   ./app/View/Components/ShareEpisodeButton.php [25:5 - 32:21]
┌────────┬────────────────┬─────────────┬──────────────┬──────────────┬──────────────────┬───────────────────┐
│ Format │ Files analyzed │ Total lines │ Total tokens │ Clones found │ Duplicated lines │ Duplicated tokens │
├────────┼────────────────┼─────────────┼──────────────┼──────────────┼──────────────────┼───────────────────┤
│ php    │ 437339723017024514082 (12.02%)    │ 40538 (13.44%)    │
├────────┼────────────────┼─────────────┼──────────────┼──────────────┼──────────────────┼───────────────────┤
│ Total: │ 437339723017024514082 (12.02%)    │ 40538 (13.44%)    │
└────────┴────────────────┴─────────────┴──────────────┴──────────────┴──────────────────┴───────────────────┘

Found 451 clones.
time: 53.662ms

The “Duplicated tokens” percentage is the one to watch, and those file and line pairs point you straight at the copies.

Failing the Build on Too Much Duplication

A report is useful. A report that fails your build is a guardrail. The --threshold flag tells jscpd the maximum duplication percentage you’ll tolerate, and it exits with a non-zero code when you cross it:

jscpd ./app --threshold 5

If duplication comes back at 13.44% against a threshold of 5, the process exits non-zero, which is exactly what an agent or a CI pipeline needs to know something went wrong.

To make sure you’re using the same settings for each run, create a .jscpd.json in your project root so the settings live with the code:

{
    "threshold": 5,
    "reporters": ["console", "html"],
    "ignore": ["**/vendor/**", "**/tests/**"],
    "minTokens": 60
}

The reporters option controls output. console prints the table you saw, html writes a browsable report you can click through, and json gives you machine-readable data for other tools.

A Real PHP Clone

Duplication from an AI agent can look like the following example. You have two controllers, each fetching a record and guarding against a missing result:

<?php
// app/Http/Controllers/InvoiceController.php
public function show(int $id): JsonResponse
{
    $invoice = Invoice::find($id);

    if ($invoice === null) {
        Log::warning("Record not found", ["id" => $id]);
        return response()->json(["error" => "Not found"], 404);
    }

    return response()->json($invoice);
}
<?php
// app/Http/Controllers/OrderController.php
public function show(int $id): JsonResponse
{
    $order = Order::find($id);

    if ($order === null) {
        Log::warning("Record not found", ["id" => $id]);
        return response()->json(["error" => "Not found"], 404);
    }

    return response()->json($order);
}

jscpd flags these two blocks as a clone because they are the same shape with one model name swapped. To fix this, we can pull the shared behavior into one place, and the duplication disappears:

<?php
// app/Support/RecordResponder.php
final class RecordResponder
{
    public static function respond(?Model $record, int $id): JsonResponse
    {
        if ($record === null) {
            Log::warning("Record not found", ["id" => $id]);
            return response()->json(["error" => "Not found"], 404);
        }

        return response()->json($record);
    }
}

Each controller now calls RecordResponder::respond(Invoice::find($id), $id) and when your users eventually find a bug in this code the fix happens once instead of six times.

We’ll have more after this word from our partners.

Mago Handles Correctness

So jscpd tells you if your code is repeated, but it says nothing about whether the code is right. That’s why we reach for Mago. Mago is a static analysis toolchain for PHP written in Rust, which makes it fast enough to run on every single edit. I covered it in depth in a separate article, so here’s the short version.

mago lint checks style and consistency, mago analyze builds a semantic model of your code and catches type errors, dead code, and logic that can’t work. When your AI-generated function claims to return an int but hands back a string, Mago catches it right away:

error[invalid-return-statement]: expected `int`, but found `string`.
  ┌─ app/Cart.php:8:128return $cartCount . " items";
  │            ^^^^^^^^^^^^^^^^^^^^^ This has type `string`

Together they cover two failure modes: jscpd for duplication, Mago for correctness and style.

Wiring It Into the Agentic Loop

Where it gets nice is integrating it with your agentic workflow. Inside a loop with Claude Code, Cursor, or any agent that runs shell commands, you want the agent to check its own work after every write. We can create the following “check.sh” that can be used

#!/usr/bin/env bash
set -e

jscpd app
mago lint
mago analyze

set -e makes the script exit on the first failure, so any of the three failing commands sends a non-zero code back to the agent. Wire it as a post-edit hook or add it to the agent’s instructions: “run ./check.sh after every change and fix what it reports.” The agent sees the duplication percentage climb past 5%, extracts the repeated block, and moves on, all before you look at a single line.

For Claude Code, Claude adds the following section to “.claude/settings.json”:

    "PostToolUse": [
      {
        "matcher": "Write|Edit|MultiEdit|NotebookEdit",
        "hooks": [
          {
            "type": "command",
            "command": "bash \".claude/support-scripts/hooks/run-checks.sh\""
          }
        ]
      }
    ],

Why Fast Matters

None of this works if the tools are slow. A check that takes two minutes is a check you’ll run once a day, if that, and by then the agent has buried the problem under another 3,000 lines. jscpd and mago scans in seconds. Because they finish before you notice they started, you can run them on every iteration, and running them on every iteration is what keeps the AI on track.

Gotchas

A few things to keep in mind.

jscpd will flag clones that are coincidental, thinks like two short constructors that happen to line up. To fix this bump minTokens or minLines until it stops crying about trivial matches.

Not all duplication is bad. Two functions that look alike today might need to diverge tomorrow, and forcing them into one abstraction now creates a worse tangle than the copy did. The tools only see in black and white, so you might need to override something if you know it’s necessary. Then again if you’re not reviewing the code created by you agent you may not care.

Thresholds are per-project. A greenfield service might hold at 3% while a legacy codebase starts at 15% and you ratchet it down over months. Pick a number you can actually pass, then tighten it.

These are guardrails, not a replacement for review. jscpd and mago catch mechanical problems fast, which frees you to spend your review energy on the things only a human notices: whether the feature is the right feature at all.

What You Need To Know

  1. jscpd is a fast, language-agnostic copy/paste detector that reports duplication percentage and clone locations.
  2. Use --threshold and a .jscpd.json to fail the build when duplication gets too high.
  3. Extract flagged clones into a shared method, trait, or service so fixes happen once.
  4. Pair jscpd with Mago so you cover duplication and correctness together.
  5. A check.sh the agent runs after every edit turns both tools into an immediate feedback loop.

 

Leave a comment

Use the form below to leave a comment:

 

Our Partners

Collaborating with industry leaders to bring you the best PHP resources and expertise