jscpd

Duplicate code detector for 220+ languages — plus dead code, complexity hotspots, duplication and health trends over git history, and one health score for the whole codebase. Rust engine, self-contained binary, AI-ready with an MCP server and a token-efficient reporter.
Documentation: https://jscpd.dev
jscpd tokenizes each of its 224 supported formats the way that language defines it — its own comment and string rules, not generic text — then finds duplicated token sequences across files with a rolling Rabin-Karp hash. Opt-in passes catch copies that differ only in names or values (Type-2) or that have a few edited lines (Type-3), and an experimental one uses a code embedding model for functions that do the same thing written differently (Type-4). See How detection works for the full mechanism, and Supported formats for the full list.
Beyond duplicates, jscpd also finds dead code (--dead-code), ranks files by complexity (--complexity), tracks duplication and health over git history (--history), and rolls it all into one health score (--health) — see Features below.
Quick Start
# macOS / Linux
curl -fsSL https://jscpd.dev/install.sh | bash
# Windows (PowerShell)
irm https://jscpd.dev/install.ps1 | iex
# No install — run once with npx (Node.js)
npx jscpd .
Then scan a project:
jscpd /path/to/code
Other install methods
| Method | Command | Notes |
|---|
| npm | npm install -g jscpd | Installs the jscpd command; prebuilt binary, no Node.js at runtime |
npm (cpd command) | npm install -g cpd | Same binary, exposed as cpd |
| PyPI | pip install jscpd | Platform wheels with both commands; also pipx install jscpd, uv tool install jscpd, or uvx jscpd . to run without installing |
| Cargo | cargo install jscpd | Builds from crates.io; installs both jscpd and cpd |
| Homebrew | brew install jscpd | macOS / Linux |
| Nix | nix run github:kucherenko/jscpd -- /path/to/code | Or nix profile install github:kucherenko/jscpd |
| Docker | docker run --rm -v "$PWD:/src" ghcr.io/kucherenko/jscpd . | Multi-arch image built from the release binaries |
GitHub Action
- uses: kucherenko/jscpd@v5
with:
threshold: 5
Uploads SARIF results to GitHub Code Scanning by default. See CI & Pre-Commit Hooks for all inputs and outputs.
Documentation
| Document | Description |
|---|
| Rust engine | Installation, CLI reference, reporters, baseline, summary, complexity, dashboard, blame, config file |
| AI-Ready | AI reporter, agent skills, MCP server |
| Editors | jscpd --lsp, the language server: setup for Neovim, Helix, Sublime Text, Emacs and JetBrains IDEs |
| Programming API | Rust API (cpd-finder crate) |
| CI & Pre-Commit Hooks | GitHub Action, Docker image, pre-commit hooks |
| Packages | npm packages and crates that make up a release |
| Supported formats | All 224 formats with their file extensions |
| Runnable demos | One fixtures/<feature>-demo/ directory per feature, each README lists the commands with their expected output |
Features
jscpd v5 is a Rust engine that ships as a self-contained binary — no runtime required — under two npm names (jscpd installs the jscpd command, cpd installs cpd), on PyPI, crates.io, Homebrew, Nix, Docker, and as a GitHub Action.
Duplicate detection
- Language-aware tokenization — per-format comment and string syntax for all 224 formats, the oxc parser for JavaScript/TypeScript/JSX/TSX, embedded-language extraction for Vue, Svelte, Astro, Markdown and Razor, and keyword/identifier/literal classification, so a clone is a repeated sequence of language tokens, never a repeated run of text (see How detection works)
- 224 language formats, with cross-format detection (Vue SFC, Svelte, Astro, Markdown) and
--cross-formats groups to match clones across JavaScript and TypeScript
- Type-2 clones —
--ignore-identifiers, --ignore-literals and --ignore-annotations find blocks that differ only in names, literal values or annotations, reported as renamed. A name must stand for one name on the other side, so x = x + x does not match d = a + b; --ignore-identifiers=blind matches any names, as 5.4 did (see docs)
- Type-3 near-miss clones —
--max-gap-lines N merges a copy with a few inserted or changed lines into one similar clone with a similarity score; --similarity compares whole functions of JavaScript, TypeScript, Python, Java, Kotlin, Scala, C#, Go, Rust, C, C++, PHP, Ruby, Swift and Clojure by the subtrees of their normalized syntax trees, in code blocks of Markdown and components as well: renamed locals (each standing for one name on the other side) and other literals match, other called functions or operators do not, and test files are left out; the edn reporter lists every pair with its score (see docs)
- Type-4 semantic clones (experimental) —
--semantic embeds the functions of JavaScript, TypeScript, Vue, Svelte, Astro, Python, Rust, Go, Java, Kotlin, C#, C, C++, PHP, Ruby, Scala and Swift files with a code embedding model, either one jscpd runs itself (after jscpd --semantic-download once) or any OpenAI-compatible API, and reports functions that do the same thing written differently: the same feature implemented twice in one language, or a rule your Rust backend enforces and your Svelte frontend repeats (see Semantic clones below)
- Port progress and parity (experimental):
jscpd --compare python/ typescript/ pairs the functions of two folders with the same model and lists which functions of each have a counterpart in the other: what is left to port to another language or platform, or what the Android version of an app has that the iOS one lacks (see Comparing two codebases below)
- Boundaries: rules in
.jscpd.json keep the code of a module, a folder, a file or a method inside its boundary, such as money that only the backend may calculate. A copy of it in a place a rule keeps it out of fails the run, whether someone copied it, edited the copy or wrote the logic again in another language (see Boundaries below)
- Clone kinds everywhere —
exact, renamed or similar in the console, JSON (kind, similarity, method), XML, HTML, Xcode, SARIF (jscpd/duplicate-code, jscpd/renamed-code, jscpd/similar-code, jscpd/similar-function, jscpd/semantic-code) and Code Climate output; default runs still report only exact clones
--kind — keep only the clone kinds you care about: --kind renamed, or --kind gap,ast for near-miss clones only. Statistics and --threshold follow the filter; a kind whose detector is off warns, an unknown kind errors (see docs)
- 15 reporters:
console, console-full, json, xml, csv, html, markdown, badge, sarif, codeclimate, openmetrics, ai, xcode, threshold, silent
- Clone baseline — gate CI on new duplication only.
--baseline .jscpd-baseline.json --fail-on-new-clones[=N] tolerates legacy clones and fails the build on regressions; --baseline-from-ref origin/main does the same without a committed file (see docs)
- Only the files you changed —
jscpd --changed . reports the clones of the files git status lists, matched against the whole project, and marks the ones HEAD didn't have as new; --changed-only compares the changed files only with one another. The baseline of HEAD is saved on the first run and built again after a commit (see docs)
- Exit codes you can gate on — an unknown
--format, a missing scan path or a reporter that can't write its file now exit 1 instead of passing with an empty report; --fail-on-empty fails a scan that analyzed no files (see Exit codes)
- GitLab-ready reporters —
codeclimate (gl-code-quality-report.json) and openmetrics (jscpd-metrics.txt) plug into artifacts:reports
- Git blame with side-by-side author comparison (
--blame --reporters console-full)
--skip-local — report only clones that cross the scan roots: jscpd packages/api packages/web --skip-local drops pairs inside either tree, keeping only api-to-web duplication
--skip-isolated — ignore duplication between monorepo folders owned by different teams (--skip-isolated "packages/team-a|packages/team-b")
Beyond duplication
--history — duplication and health trend over git history: jscpd --history scans the current folder over the last 30 commits of the branch, jscpd src --history v5.0.0..HEAD the commits of a range, a merged branch as its merge commit. It prints bar charts of the duplication percentage and the health score, a per-commit table with the change between points, the trends with the score of each health dimension at both ends, and how far --threshold could be tightened; --health and --dashboard print the series too (see docs)
--dead-code — find code nothing runs, not just code written twice: unused files, exports, declarations and imports across JavaScript, TypeScript and Python. Builds the import graph from your entry points (package.json, pyproject.toml, framework conventions) and walks it, so dead code cascades — a helper whose only caller is dead gets reported too, each finding with a confidence score and, below 100, why it might be wrong. Also ships standalone as basta (see docs)
--summary — refactoring hotspots straight from the scan: top files and folders by tokens, lines, size, and a complexity estimate (see docs)