jscpd v5 is a Rust engine shipped as a self-contained binary: no runtime required, same CLI flags, reporters and .jscpd.json config as the earlier Node.js versions.
It is distributed under three names:
| Package | Installs commands | Notes |
|---|---|---|
jscpd |
jscpd |
Default install; installs the jscpd command |
cpd |
cpd |
Lighter package, shorter command only |
jscpd (crates.io) |
jscpd and cpd |
Rust-native install; both binaries |
All three install the identical Rust binary and accept the same CLI options. Only the crates.io install exposes both command names from a single package.
A comparison against other copy/paste detectors (jscpd-rs, Duplo, Fallow, Simian, PMD CPD) on the repository's fixtures/ corpus — timing, detection counts, cross-format detection and AI-token efficiency — is in benchmark/BENCHMARK.md.
# npm — installs the jscpd command
npm install -g jscpd
jscpd /path/to/code
# npm — installs only the cpd command (lighter)
npm install -g cpd
cpd /path/to/code
# crates.io — Rust-native install (exposes both jscpd and cpd commands)
cargo install jscpd
jscpd /path/to/code
cpd /path/to/code
# Nix — run without installing
nix run github:kucherenko/jscpd -- /path/to/code
# Nix — install permanently
nix profile install github:kucherenko/jscpd
# Homebrew (macOS/Linux)
brew install jscpdThe npm packages ship prebuilt binaries for 8 platforms — no Node.js runtime is required, the binary is self-contained:
| Platform | npm package | Rust target |
|---|---|---|
| macOS arm64 | jscpd-darwin-arm64 |
aarch64-apple-darwin |
| macOS x64 | jscpd-darwin-x64 |
x86_64-apple-darwin |
| Linux arm64 (glibc) | jscpd-linux-arm64-gnu |
aarch64-unknown-linux-gnu |
| Linux arm64 (musl) | jscpd-linux-arm64-musl |
aarch64-unknown-linux-musl |
| Linux x64 (glibc) | jscpd-linux-x64-gnu |
x86_64-unknown-linux-gnu |
| Linux x64 (musl) | jscpd-linux-x64-musl |
x86_64-unknown-linux-musl |
| Windows arm64 | jscpd-windows-arm64-msvc |
aarch64-pc-windows-msvc |
| Windows x64 | jscpd-windows-x64-msvc |
x86_64-pc-windows-msvc |
The same binaries are attached to every GitHub Release as jscpd-<platform>.tar.gz with a checksums.txt and SLSA provenance, and packaged as a multi-arch Docker image at ghcr.io/kucherenko/jscpd (see CI docs).
The jscpd command is available after installing jscpd from npm; the cpd command is available after installing either cpd (npm) or jscpd (crates.io). Both commands accept the same options and are identical:
jscpd [OPTIONS] [PATH]...
cpd [OPTIONS] [PATH]...| Option | Short | Description | Default |
|---|---|---|---|
--min-tokens |
-k |
Minimum tokens in a clone | 50 |
--min-lines |
-l |
Minimum lines in a clone | 5 |
--max-lines |
-x |
Maximum source file lines | — |
--max-size |
-z |
Skip files larger than SIZE (e.g. 1kb, 1mb, 100kb) |
no limit |
--mode |
-m |
Detection mode: mild, weak, strict |
mild |
--ignore-pattern |
Comma-separated regular expressions; source text matched by a pattern is excluded from clone detection. See Ignoring source regions | — | |
--workers |
Number of worker threads for parallel tokenization/detection | auto (all CPU cores) | |
--no-colors |
Disable ANSI color output | off | |
--absolute |
-a |
Use absolute paths in reports | off |
--ignore-case |
Ignore case of symbols in code (experimental) | off | |
--ignore-identifiers |
Treat all identifiers as equal, so clones that differ only in variable, function or type names are found. See Type-2 clones | off | |
--ignore-literals |
Treat all string literals as equal and all numeric literals as equal | off | |
--ignore-annotations |
Skip annotations and decorators (@Name, @Name(...)) before detection |
off | |
--max-gap-lines |
Merge clones of one file pair separated by at most N unmatched lines in both files into one near-miss clone reported as similar. See Type-3 clones |
0 (off) | |
--similarity |
Report JavaScript/TypeScript function pairs whose syntax-tree similarity reaches RATIO, a number in (0, 1], as similar clones; 1 means exact matches only. See function similarity |
1 (off) | |
--formats-exts |
Custom format-to-extension mapping (e.g. javascript:es,es6;dart:dt) |
— | |
--formats-names |
Custom format-to-filename mapping | — | |
--cross-formats |
Detect clones across formats: ;-separated groups of ,-separated formats (e.g. javascript,typescript). Preset js-ts = javascript,jsx,typescript,tsx |
— | |
--list |
List all supported formats and exit | — | |
--skip-local |
Skip clones where both fragments are in the same directory | off | |
--skip-isolated |
Skip clones between different folders of the same isolation group: ,-separated groups of |-separated folders (e.g. packages/a|packages/b). Useful in monorepos where teams own separate packages |
— | |
--baseline |
Clone baseline file (e.g. .jscpd-baseline.json): clones whose fingerprint is absent from it are reported as new. See Baseline |
— | |
--update-baseline |
Rewrite the baseline file from the current run, creating it if missing (requires --baseline) |
off | |
--fail-on-new-clones |
Exit 1 when more than N new clones are found (--fail-on-new-clones alone means N=0; requires --baseline or --baseline-from-ref) |
— | |
--baseline-from-ref |
Compare against an ephemeral baseline built from a git ref's tree (e.g. origin/main). Conflicts with --baseline |
— | |
--sarif-error-tokens |
Report SARIF results as error for clones with at least this many tokens (smaller clones stay warning). When overall duplication exceeds --threshold, all SARIF results become error regardless of size. |
— (all warning) |
|
--min-duplicated-lines |
Minimum percentage of duplication to report (0-100) | 0 | |
--mcp |
Serve the Model Context Protocol over stdio: scan PATHs once, then expose check_duplication / get_statistics / check_current_directory tools to MCP clients |
off | |
--summary |
Print a codebase summary: top files and folders by tokens, lines, size, and a complexity estimate. See Summary | off | |
--summary-top |
Number of entries in each summary top list | 10 | |
--summary-by |
Summary sort metric: tokens, lines, size, complexity |
tokens |
|
--silent |
-s |
Suppress console output | off |
--no-tips |
Suppress tips and promotional messages | off | |
--version |
-V |
Print version | — |
--help |
-h |
Print help | — |
15 built-in reporters:
| Reporter | Output |
|---|---|
console |
Clone list + statistics table (default) |
console-full |
Clone list with source snippets; with --blame shows side-by-side author comparison |
json |
report/jscpd-report.json |
xml |
report/jscpd-report.xml |
csv |
report/jscpd-report.csv |
html |
report/jscpd-report.html |
markdown |
report/jscpd-report.md |
badge |
report/jscpd-badge.svg + report/jscpd-lines-badge.svg |
sarif |
report/jscpd-report.sarif (GitHub Code Scanning) |
codeclimate (alias gitlab) |
report/gl-code-quality-report.json — CodeClimate issue format, ready for GitLab artifacts:reports:codequality |
openmetrics |
report/jscpd-metrics.txt — OpenMetrics text format, ready for GitLab artifacts:reports:metrics |
ai |
Token-efficient output for LLM pipelines |
xcode |
Xcode-compatible warnings |
threshold |
Exit 1 if duplication percentage exceeds --threshold |
silent |
No console output |
File reporters write into the --output directory (default report/) using the jscpd-report.* prefix (e.g. jscpd-report.json, jscpd-report.sarif).
--summary appends a codebase summary to the run output — the statistics jscpd already collects while scanning, aggregated to answer "where should I refactor first":
Summary (by tokens; 321 files, 129 folders analyzed)
Top files:
TOKENS LINES SIZE CX DUP% PATH
2052 363 11.4K 80 0.0 files.ts
...
Top folders:
FILES TOKENS LINES SIZE CX PATH
8 5264 843 26.5K 15 src/core
...
- Top files lists the top
--summary-topfiles ranked by the--summary-bymetric. Every row carries all metrics, so re-ranking by another lens is a--summary-by size(orlines,complexity) away. - Top folders aggregates files into their direct parent directory (each file counted exactly once; no cumulative ancestor totals).
- CX is a language-agnostic cyclomatic-complexity estimate computed from the token stream: 1 + the number of decision-point tokens (
if,elif/elsif/elseif,unless,for,foreach,while,until,case,cond,when,catch,rescue,except,and,or,andalso,orelse,&&,||,?,??). Matching is case-insensitive, so uppercase-keyword languages (SQL, PL/SQL, Fortran, COBOL, BASIC) count too. For folders it is the per-file mean. Languages that branch without such keywords (SmalltalkifTrue:messages, Prolog clauses) stay at 1 — treat CX as a ranking signal, not an exact metric. - DUP% is the share of the file's lines covered by detected clone fragments (both fragments of a clone count toward their files; display is capped at 100%).
The summary is fully opt-in and computed after detection from data already in memory, so runs without --summary are unaffected. It integrates with:
console/console-full— the block shown aboveai— a compact, LLM-token-efficient variant (one line per file/folder)json— an additivesummarykey injscpd-report.json(absent when the flag is off, so the schema is unchanged for existing consumers)
Config file equivalents: "summary": true, "summaryTop": 10, "summaryBy": "tokens".
# Refactoring hotspots: biggest files by tokens plus duplication share
cpd ./src --summary
# Agent-friendly: compact clone list + compact summary
cpd ./src --summary --reporters ai --no-tips
# Focus on the most complex files, top 5 lists, machine-readable
cpd ./src --summary --summary-by complexity --summary-top 5 --reporters jsonGate CI on new duplication only, tolerating clones that already exist. A baseline file records a content-hash fingerprint per accepted clone (the same hash the SARIF reporter emits as partialFingerprints["jscpdCloneHash/v1"]); clones absent from it are reported as new.
# Create or refresh the committed baseline
jscpd --baseline .jscpd-baseline.json --update-baseline .
# Fail when new clones appear (independent of --threshold)
jscpd --baseline .jscpd-baseline.json --fail-on-new-clones .
# Allow up to 3 new clones
jscpd --baseline .jscpd-baseline.json --fail-on-new-clones 3 .
# Stateless variant for PR gates: compare against a git ref instead of a file
jscpd --baseline-from-ref origin/main --fail-on-new-clones .--baseline-from-ref checks the base ref's tree out into a temporary detached worktree, scans it with the same configuration, and compares fingerprints in memory. It costs a second scan of the corpus; the committed file needs only one. In CI, fetch the ref first (fetch-depth: 0 or git fetch origin main).
New-clone information flows through the reporters: [NEW] markers in console/console-full, per-clone isNew plus newClones / newDuplicatedLines statistics in json, level error in sarif, severity major in codeclimate, and jscpd_new_clones / jscpd_new_duplicated_lines gauges in openmetrics.
Config file keys: baseline, baselineFromRef, failOnNewClones.
With --blame --reporters console-full, clones are displayed with a side-by-side author comparison:
176 │ Andrii Kucherenko │ <= │ 196 │ Josh Soref │ ## TODO
177 │ Andrii Kucherenko │ <= │ 197 │ Josh Soref │
180 │ Andrii Kucherenko │ == │ 200 │ Andrii Kucherenko │ ## License
== means both lines were written by the same author; <= means different authors (potential copy).
# Scan a directory
jscpd /path/to/source
# or
cpd /path/to/source
# Tune sensitivity and pick reporters
cpd /path/to/source --min-tokens 30 --min-lines 3 --reporters console,json,html
# Git blame with side-by-side author comparison
cpd /path/to/source --blame --reporters console-full
# List supported formats
cpd --list
# Use multiple reporters with custom output
cpd ./src -r console,json,sarif -o ./reports
# Skip clones within the same directory
cpd --skip-local /path/to/source
# Monorepo: don't compare team-owned packages with each other
cpd . --skip-isolated "packages/team-a|packages/team-b"Options can also come from a .jscpd.json config file (camelCase keys; existing v4 config files work unchanged):
{
"path": ["./src"],
"reporters": ["console", "json"],
"minLines": 5,
"minTokens": 50,
"threshold": 0,
"format": ["javascript", "typescript"],
"ignore": ["**/node_modules/**"],
"ignorePattern": ["generated by .*"],
"ignoreIdentifiers": false,
"ignoreLiterals": false,
"ignoreAnnotations": false,
"maxGapLines": 0,
"gitignore": true,
"mode": "mild"
}Isolation groups use the nested-array form in the config file: "skipIsolated": [["packages/a", "packages/b"]].
Config discovery order: --config <path> → .jscpd.json → .config/jscpd.json (the dot-config convention, also accepts .config/.jscpd.json) → the jscpd key in package.json.
Use --ignore-pattern (or ignorePattern in .jscpd.json) when only part of a file should be excluded. Each regular expression is matched against the raw source text before tokenization, and tokens that overlap a match are omitted from clone detection. This matches the v4 behavior; v5 uses Rust regex syntax, which does not support look-around or backreferences. A pattern that fails to compile is skipped with a warning.
The CLI flag splits its value on commas, so a regular expression that itself contains a comma (a {1,3} repetition, a character class such as [,;]) must be set in the config file instead.
For a one-off region, place jscpd:ignore-start and jscpd:ignore-end in comments that are valid for the source language:
// jscpd:ignore-start
const generatedLookup = {
alpha: 1,
beta: 2,
};
// jscpd:ignore-endAll source between the markers is excluded during tokenization. This works in every detection mode.
License headers are a common use case in languages whose comments are tokenized: C#, Java, Go, Python and every other format handled by the generic tokenizer. JavaScript and TypeScript comments never produce tokens, so a header in those files is never part of a clone and needs no exception. Mark a header explicitly when only a few files need it:
// jscpd:ignore-start
/*
* Copyright 2026 Example Authors
* Licensed under the Example License
*/
// jscpd:ignore-endFor the same block-comment header across many files, configure one anchored, dot-all regular expression instead:
{
"ignorePattern": ["(?s)\\A/\\*.*?\\*/"]
}The JSON escaping above produces the Rust regular expression (?s)\A/\*.*?\*/, which matches the first block comment only when it starts at the beginning of the file. Adjust the expression to the comment style and exact license text used by the project.
--mode weak is a blunter alternative: it drops every comment from detection in every language, so duplicated comments elsewhere in the code are ignored too.
By default two blocks are a clone only when their tokens are identical. Three opt-in flags normalize token classes before hashing so that blocks which differ only in names or values match as well (Type-2 clones, the same idea as PMD CPD's --ignore-identifiers / --ignore-literals / --ignore-annotations):
| Flag | Config key | What is normalized |
|---|---|---|
--ignore-identifiers |
ignoreIdentifiers |
Every identifier hashes as $id. Keywords are kept, so if never matches while: the JavaScript/TypeScript tokenizer classifies keywords itself, other languages use a built-in list of common keywords |
--ignore-literals |
ignoreLiterals |
String literals hash as $str, numeric literals as $num; true, null and regex literals keep their value |
--ignore-annotations |
ignoreAnnotations |
@Name, @a.b.Name and @Name(...) runs are dropped in Java, Kotlin, Scala, Groovy, Python, Dart, Swift, JavaScript and TypeScript. Formats where @ prefixes a variable or directive (Ruby, Perl, Razor, T-SQL, CSS) are untouched |
Positions in reports still point at the original source. Each clone carries a kind: exact when the raw tokens of both fragments are identical, renamed when they match only after normalization. The console prints Clone found (javascript, renamed), the ai reporter appends (renamed), the JSON report adds "kind": "renamed" to each duplicate, SARIF reports renamed clones under the rule jscpd/similar-code (exact ones stay jscpd/duplicate-code), and Code Climate uses the same two check_name values. A run without these flags reports only exact clones and its output is unchanged.
jscpd --ignore-identifiers src/ # function a(x) {…} matches function b(y) {…}
jscpd --ignore-identifiers --ignore-literals src/ # …and 10 matches 25, 'dev' matches 'prod'
jscpd --ignore-annotations src/main/java/ # @Override / @Deprecated no longer split a cloneNormalized runs find more and longer clones than exact runs, so their snippet fingerprints (jscpdCloneHash/v1) differ from an exact run's. Keep a separate --baseline file per configuration. See fixtures/type2-demo for a runnable example of each flag.
A copy with a line inserted, removed or changed in the middle shows up as two shorter exact clones with a gap between them, each of which must clear --min-tokens and --min-lines on its own. --max-gap-lines N (config key maxGapLines) merges clones of the same file pair whose fragments follow each other in both files with at most N unmatched lines in between, and reports the result as one clone of kind similar:
jscpd src/ # a.js 1-4 ↔ b.js 1-4, a.js 4-9 ↔ b.js 5-10: two exact clones
jscpd --max-gap-lines 1 src/ # Clone found (javascript, similar (gap) ~0.85): a.js 1-9 ↔ b.js 1-10A merged clone's tokens is the number of matched tokens and similarity is that number divided by the tokens of the longer merged span, so a single inserted line in a 60-token block gives roughly 0.9. Chains of matches merge transitively, and the merge is applied after every other filter (--min-lines, --skip-local, --skip-isolated). Because merging only ever joins clones the exact run already reported, it cannot introduce a match that was not there; it removes fragmentation. It applies to every language.
Reporting: the console prints Clone found (javascript, similar (gap) ~0.85), the ai reporter appends [~0.85 gap], the JSON report adds "kind": "similar", a "similarity" value and "method": "gap", SARIF files merged clones under jscpd/near-miss-code with similarity and similarity_method properties, and Code Climate uses the same check_name. The method is shown because the two near-miss mechanisms score on different scales: gap is matched tokens over the merged span, ast (from --similarity, below) is structural overlap of whole functions. Duplicated-line statistics count the merged span, gap lines included. With the default 0 the merge pass is skipped entirely and output is identical to earlier releases. See fixtures/type3-demo for a runnable example.
Edits spread through a function rather than concentrated in one gap still escape a token window. --similarity RATIO (config key similarity, a number in (0, 1]; the default 1 means exact matches only, so the pass never runs until you set a lower value) compares whole functions instead: every function declaration, function expression, method and arrow function in a JavaScript, TypeScript, JSX or TSX file is summarized by the bag of 4-grams over the pre-order sequence of its syntax-tree node types, and two functions are reported as one similar clone when the weighted Jaccard index of their bags reaches RATIO. Names and literal values are not part of the summary, so a renamed copy scores 1.0; one inserted line scores about 0.9; two inserted statements plus renames score about 0.75. Candidates come from a MinHash index, so the search stays close to linear in the number of functions.
jscpd --similarity 0.85 src/ # near-identical structure: renames, literal changes, a one-line edit
jscpd --similarity 0.7 src/ # looser: a couple of added or removed statementsFunctions must clear --min-tokens and --min-lines on their own, nested functions are never paired with their parent, and a pair that an exact, renamed or merged clone already covers is not reported again. Reporting is the same as for merged clones except for the method: the console prints Clone found (javascript, similar (ast) ~0.75), the ai reporter [~0.75 ast], JSON carries "method": "ast" and SARIF similarity_method; tokens is the smaller function's token count and the fragments span the whole functions. Values outside (0, 1] print a warning and fall back to 1.
Scoring needs a syntax tree, and today only JavaScript/TypeScript have one (oxc). Each language plugs in through the FunctionExtractor trait in cpd-tokenizer (functions.rs): a grammar id, the formats it serves, and a walk that opens a function at every function-like node and records the node-type sequence inside it. Signatures carry their grammar id and are only compared within one grammar, so a tree-sitter-backed extractor for another language is a self-contained addition; the scoring, CLI, MCP tool and reporters need no change. Formats without an extractor are a silent no-op. The MCP check_duplication tool accepts the same similarity argument and returns the structurally similar project functions for each function in the snippet.
jscpd supports 224 formats. Use cpd --list to see the full list, or see FORMATS.md for names, file extensions and descriptions.
Vue SFC (.vue), Svelte (.svelte), Astro (.astro), and Markdown (.md) files are tokenized per-block/per-section, enabling duplicate detection across file types.
By default every format is compared in its own isolated pool, so a TypeScript file never matches a near-identical JavaScript file. --cross-formats declares format equivalence groups that share one comparison pool — useful for finding leftover .js copies during a TypeScript migration:
cpd --cross-formats "javascript,typescript" ./src
cpd --cross-formats js-ts ./src # preset: javascript,jsx,typescript,tsx
cpd --cross-formats "js-ts;css,scss" ./src # multiple groupsWhen a group mixes TypeScript (typescript/tsx) with JavaScript (javascript/jsx), TypeScript files are compared with erasable type syntax stripped from the detection token stream — type annotations, generics, interface/type declarations, as/satisfies, ?/! markers, access modifiers, implements clauses, type-only imports/exports, overload signatures, and declare statements. Reported clone positions always reference the original sources.
Config file equivalents (all three shapes are accepted):
{ "crossFormats": "javascript,typescript;css,scss" }
{ "crossFormats": ["javascript,typescript", "css,scss"] }
{ "crossFormats": [["javascript", "typescript"], ["css", "scss"]] }Notes:
- TypeScript syntax with runtime semantics is not erased and will not cross-match:
enum, non-declarenamespace, parameter properties (constructor(private x)),import x = require(),export =. - A cross-format clone is attributed to one member format in the per-format statistics.
- Overlapping groups are merged; groups with fewer than two formats are ignored.
jscpd v4 (TypeScript engine) is maintained on the master-v4 branch and published as jscpd@4. Moving to v5 needs no changes to flags or config in most projects; the differences:
| Feature | jscpd v4 (Node.js) | jscpd v5 (Rust) |
|---|---|---|
--blame |
Calls git CLI for each file |
Same output (==/<= markers), calls git blame --porcelain per file |
--store (LevelDB/Redis) |
Persistent store for large repos | Not supported; the flag is ignored with a warning. Available on the master-v4 line. |
--formats-exts |
Custom format-to-extension mapping | Same flag name, same behavior |
--formats-names |
Custom format-to-filename mapping | Same flag name, same behavior |
| Programming API | jscpd() Promise API, detectClones() |
Rust API via cpd-finder crate; no Node.js API |
| Config file | .jscpd.json with camelCase keys |
Same — .jscpd.json with camelCase keys |
| Cross-format detection | Vue SFC, Svelte, Astro, Markdown | Same — per-block tokenization |
| Token counts | Varies by tokenizer | May differ by 1-2% due to Rust tokenizer; clone detection matches |
--reporters |
All v4 reporters | All v4 reporters except full (use console-full) |
--no-gitignore |
Default respects .gitignore |
Same behavior, same flag name |
--workers |
Not available | Available — control parallelism for file tokenization/detection |
| Output filenames | jscpd-report.json, html/ directory |
jscpd-report.json, jscpd-report.html, jscpd-report.sarif, jscpd-report.csv, jscpd-report.md, jscpd-badge.svg, jscpd-lines-badge.svg |
For integration in Rust applications:
use cpd_finder::orchestrate::{RunConfig, run};
let config = RunConfig {
paths: vec!["./src".into()],
min_tokens: 50,
..Default::default()
};
let result = run(&config).unwrap();
println!("Found {} clones", result.clones.len());
println!("Analyzed {} files", result.statistics.total.sources);cpd (CLI binary)
├── cpd-core — Detection algorithm (Rabin-Karp rolling hash)
├── cpd-tokenizer — Language tokenization (224 formats)
├── cpd-finder — File walking, orchestration, git blame
└── cpd-reporter — Output formatting (15 reporters)