Dead code 3 unused files, 12 unused exports, 2 unused deps 18ms
Duplication 4 clone groups (2.1% of codebase) 31ms
Complexity 7 functions exceed thresholds 4ms
Total 26 issues across 847 files 53ms
90 framework plugins. No Node.js runtime. No config file needed.
Install
Commands
Dead code
Finds unused files, exports, dependencies, types, enum members, class members, unresolved imports, unlisted dependencies, duplicate exports, circular dependencies (including cross-package cycles in monorepos), boundary violations, type-only dependencies, test-only production dependencies, and stale suppression comments. Entry points are auto-detected from package.json fields, framework conventions, and plugin patterns. Arrow-wrapped dynamic imports (React.lazy, loadable, defineAsyncComponent) are tracked as references. Script multiplexers (concurrently, npm-run-all) are analyzed to discover transitive script dependencies. JSDoc tags (@public, @internal, @beta, @alpha, @expected-unused) control export visibility.
Duplication
Finds copy-pasted code blocks across your codebase. Suffix-array algorithm -- no quadratic pairwise comparison.
Four detection modes: strict (exact tokens), mild (default, AST-based), weak (different string literals), semantic (renamed variables and literals).
Complexity
Surfaces the most complex functions in your codebase and identifies where to spend refactoring effort.
Paid production coverage
Production coverage answers a different question than static reachability: which functions actually execute in production. Fallow can merge V8 coverage dumps (NODE_V8_COVERAGE=...) and Istanbul coverage-final.json files into the health report, classify cold functions, and surface hot paths.
Static coverage_gaps and paid production_coverage are separate layers in the same health surface:
coverage_gapsis graph-based and answers which runtime files or exports have no test dependency pathproduction_coverageis runtime-based and answers which functions actually executed in production-like coverage inputcoverage_gapscan appear either because you passed--coverage-gaps, or because top-levelhealthenabled it from config severity when no narrower section flags were selected
| Surface | Flag | Input | Answers | License |
|---|---|---|---|---|
| Static test reachability | --coverage-gaps |
none | which runtime files/exports have no test dependency path | no |
| Exact CRAP scoring | --coverage |
Istanbul JSON file or coverage-final.json directory |
how covered each function is for CRAP computation | no |
| Runtime production coverage | --production-coverage |
V8 directory, V8 JSON file, or Istanbul JSON file | which functions actually executed, which stayed cold, which are hot | yes |
fallow license activate --trial --email ...starts a trial and stores the signed license locallyfallow license refreshrefreshes the stored license before the hard-fail windowfallow coverage setupdetects your framework and package manager, installs the sidecar if needed, writes a collection recipe, and resumes from the current setup state on re-run- The sidecar can be installed globally or as a project devDependency; fallow resolves
FALLOW_COV_BIN, project-local shims, package-manager bin lookups,~/.fallow/bin/fallow-cov, andPATH fallow health --production-coverage <path>accepts a V8 directory, a single V8 JSON file, or a single Istanbul coverage map JSON file (commonlycoverage-final.json)fallow health --coverage <path>accepts a single Istanbul coverage map JSON file or a directory containingcoverage-final.json--coverage-root <path>rebases Istanbul file paths before CRAP matching. Use it when coverage was generated in CI or Docker with a different checkout root, for examplefallow health --coverage artifacts/coverage-final.json --coverage-root /home/runner/work/myapp- V8 dumps that include Node's
source-map-cacheare remapped through supported source-map paths before analysis, including file paths, relative paths,webpack://..., andvite://...; unsupported virtual schemes safely fall back to raw V8 handling fallow health --changed-since <ref> --production-coverage <path>promotes touched hot paths to ahot-path-changes-neededverdict during change review
Production coverage is merged into the same human, JSON, SARIF, compact, markdown, and CodeClimate outputs as the rest of the health report.
Audit
Quality gate for AI-generated code and PRs. Combines dead code + complexity + duplication scoped to changed files.
Returns a verdict: pass (exit 0), warn (exit 0, warn-severity only), or fail (exit 1). JSON output includes a verdict field for CI and agent integration.
CI integration
# GitHub Action
- uses: fallow-rs/fallow@v2
# GitLab CI — include the template and extend
include:
- remote: 'https://raw.githubusercontent.com/fallow-rs/fallow/main/ci/gitlab-ci.yml'
fallow:
extends: .fallow
# Or run directly on any CI
- run: npx fallow --ci
--ci enables SARIF output, quiet mode, and non-zero exit on issues. Also supports:
--group-by owner|directory|package-- group output by CODEOWNERS ownership, directory, or workspace package for team-level triage--summary-- show only category counts (no individual issues)--changed-since main-- analyze only files touched in a PR--baseline/--save-baseline-- fail only on new issues--fail-on-regression/--tolerance 2%-- fail only if issues grew beyond tolerance--format sarif-- upload to GitHub Code Scanning--format codeclimate-- GitLab Code Quality inline MR annotations--format annotations-- GitHub Actions inline PR annotations (no Action required)--format json/--format markdown-- for custom workflows (JSON includes machine-actionableactionsper issue)--format badge-- shields.io-compatible SVG health badge (fallow health --format badge > badge.svg)
Both the GitHub Action and GitLab CI template auto-detect your package manager (npm/pnpm/yarn) from lock files, so install/uninstall commands in review comments match your project.
Adopt incrementally -- surface issues without blocking CI, then promote when ready:
{ "rules": { "unused-files": "error", "unused-exports": "warn", "circular-dependencies": "off" } }
GitLab CI rich MR comments
The GitLab CI template can post rich comments directly on merge requests -- summary comments with collapsible sections and inline review discussions with suggestion blocks.
| Variable | Default | Description |
|---|---|---|
FALLOW_COMMENT |
"false" |
Post a summary comment on the MR with collapsible sections per analysis |
FALLOW_REVIEW |
"false" |
Post inline MR discussions at the relevant lines, with suggestion blocks for unused exports |
FALLOW_MAX_COMMENTS |
"50" |
Maximum number of inline review comments |
In MR pipelines, --changed-since is set automatically to scope analysis to changed files. Previous fallow comments are cleaned up on re-runs.
The comment merging pipeline groups unused exports per file and deduplicates clone reports, keeping MR threads readable.
A GITLAB_TOKEN (PAT with api scope) is recommended for full features (suggestion blocks, cleanup of previous comments). CI_JOB_TOKEN works for posting but cannot delete comments from prior runs.
# .gitlab-ci.yml — full example with rich MR comments
include:
- remote: 'https://raw.githubusercontent.com/fallow-rs/fallow/main/ci/gitlab-ci.yml'
fallow:
extends: .fallow
variables:
FALLOW_COMMENT: "true" # Summary comment with collapsible sections
FALLOW_REVIEW: "true" # Inline discussions with suggestion blocks
FALLOW_MAX_COMMENTS: "30" # Cap inline comments (default: 50)
FALLOW_FAIL_ON_ISSUES: "true"
Configuration
Works out of the box. When you need to customize, create .fallowrc.json or run fallow init:
// .fallowrc.json
{
"$schema": "https://raw.githubusercontent.com/fallow-rs/fallow/main/schema.json",
"entry": ["src/workers/*.ts", "scripts/*.ts"],
"ignorePatterns": ["**/*.generated.ts"],
"ignoreDependencies": ["autoprefixer"],
"rules": {
"unused-files": "error",
"unused-exports": "warn",
"unused-types": "off"
},
"health": {
"maxCyclomatic": 20,
"maxCognitive": 15
}
}
Architecture boundary presets enforce import rules between layers with zero manual config:
{ "boundaries": { "preset": "bulletproof" } } // or: layered, hexagonal, feature-sliced
Run fallow list --boundaries to inspect the expanded rules. TOML also supported (fallow init --toml). The init command auto-detects your project structure (monorepo layout, frameworks, existing config) and generates a tailored config. It also adds .fallow/ to your .gitignore (cache and local data). Scaffold a pre-commit hook with fallow init --hooks. Migrating from knip or jscpd? Run fallow migrate.
See the full configuration reference for all options.
Framework plugins
90 built-in plugins detect entry points, convention exports, config-defined aliases, and template-visible usage for your framework automatically.
| Category | Plugins |
|---|---|
| Frameworks | Next.js, Nuxt, Remix, Qwik, SvelteKit, Gatsby, Astro, Angular, NestJS, Expo, Expo Router, Electron, and more |
| Bundlers | Vite, Webpack, Rspack, Rsbuild, Rollup, Rolldown, Tsup, Tsdown, Parcel |
| Testing | Vitest, Jest, Playwright, Cypress, Storybook, Mocha, Ava |
| CSS | Tailwind, PostCSS, UnoCSS |
| Databases & Backend | Prisma, Drizzle, Knex, TypeORM, Kysely, Convex |
| Blockchain | Hardhat |
| Monorepos | Turborepo, Nx, Changesets, Syncpack, pnpm |
Full plugin list -- missing one? Add a custom plugin or open an issue.
Editor & AI support
- VS Code extension -- tree views, status bar, one-click fixes, auto-download LSP binary (Marketplace)
- LSP server -- real-time diagnostics, hover info, code actions, Code Lens with reference counts
- MCP server -- AI agent integration for Claude Code, Cursor, Windsurf (fallow-skills)
- JSON
actionsarray -- every issue in--format jsonoutput includes fix suggestions withauto_fixableflag, so agents can self-correct
Fallow vs linters
Linters enforce style. Formatters enforce consistency. Fallow enforces relevance.
ESLint, Biome, and oxlint analyze one file at a time. They catch bad patterns within a file boundary. Fallow builds a module dependency graph across the entire project and finds issues that only appear when you see the whole picture.
| What | Linter | Fallow |
|---|---|---|
| Unused variable in a function | yes | no |
| Unused export that nothing imports | no | yes |
| File that nothing imports | no | yes |
| Circular dependency across modules | no | yes |
| Duplicate code blocks across files | no | yes |
| Dependency in package.json never imported | no | yes |
They're complementary -- run your linter on every save, fallow on every commit.
Full comparison: fallow vs ESLint, Biome, knip, ts-prune
Performance
Benchmarked on real open-source projects (median of 5 runs, Apple M5).
Dead code: fallow vs knip
| Project | Files | fallow | knip v5 | knip v6 | vs v5 | vs v6 |
|---|---|---|---|---|---|---|
| zod | 174 | 17ms | 577ms | 300ms | 34x | 18x |
| fastify | 286 | 19ms | 791ms | 232ms | 41x | 12x |
| preact | 244 | 20ms | 767ms | 2.02s | 39x | 103x |
| TanStack/query | 901 | 170ms | 2.50s | 1.28s | 15x | 8x |
| svelte | 3,337 | 359ms | 1.73s | 749ms | 5x | 2x |
| next.js | 20,416 | 1.66s | -- | -- | -- | -- |
knip errors out on next.js. fallow completes in under 2 seconds.
Duplication: fallow vs jscpd
| Project | Files | fallow | jscpd | Speedup |
|---|---|---|---|---|
| fastify | 286 | 76ms | 1.96s | 26x |
| vue/core | 522 | 124ms | 3.11s | 25x |
| next.js | 20,416 | 2.89s | 24.37s | 8x |
No TypeScript compiler, no Node.js runtime. How it works | Reproduce benchmarks
Suppressing findings
// fallow-ignore-next-line unused-export
export const keepThis = 1;
// fallow-ignore-file
// Suppress all issues in this file
Also supports JSDoc visibility tags (/** @public */, /** @internal */, /** @beta */, /** @alpha */) to suppress unused export reports for library APIs consumed externally.
Limitations
fallow uses syntactic analysis -- no type information. This is what makes it fast, but type-level dead code is out of scope. Use inline suppression comments or ignoreExports for edge cases.
Documentation
- Getting started
- Configuration reference
- CI integration guide
- Migrating from knip
- Plugin authoring guide
Contributing
Missing a framework plugin? Found a false positive? Open an issue.
&&
License
MIT