Skip to content
Fallow home
All docs pages

fallow dead-code

Find the files, exports, dependencies, and types that nothing uses, so you can delete them. Fallow dead-code starts at your entry points, traces reachable exports, and reports the rest.

fallow dead-code finds the code that nothing uses, so you can delete it and keep the codebase smaller. Fallow starts at your entry points, traces all reachable exports, and reports each file, export, dependency, and type that is not used.

Bare fallow runs all analyses (dead code, duplication, and health). To run only the dead code analysis, use fallow dead-code. fallow check is still a hidden alias for fallow dead-code.

fallow dead-code
fallow check      # Hidden alias for fallow dead-code

Options

Output

FlagDescription
-f, --format <FORMAT>Output format: human (default), json, sarif, compact, markdown, codeclimate, gitlab-codequality, pr-comment-github, pr-comment-gitlab, review-github, review-gitlab, github-annotations, github-summary
-q, --quietSuppress progress output
--fail-on-issuesExit with code 1 if fallow finds issues
--sarif-file <PATH>Write SARIF output to a file, in addition to --format
--ciCI mode: sets the format to SARIF, turns on fail-on-issues, and hides progress. Individual flags can still override these settings.
--explainAdd metric explanations. In human format, prints a Description: line under each section header. In JSON format, adds a _meta object with metric descriptions and docs links.
--group-by <MODE>Group output by owner (CODEOWNERS), directory (first path component), package (workspace package), or section (GitLab CODEOWNERS [Section] headers, with owners metadata). See global flags.
--summaryIn human output, show only the issue counts per category, without the individual items. JSON output does not change: it always has a summary counts object and the individual items.

Filtering

FlagDescription
--unused-filesReport only unused files
--unused-exportsReport only unused exports
--unused-typesReport only unused types
--private-type-leaksOpt in to private type leak findings (API hygiene) and report only that issue type
--deprecated-exports-in-useOpt in to deprecated exports in use (@deprecated exports that reachable files still use) and report only that issue type
--unused-depsReport only dependency findings: unused dependencies, dev dependencies, and optional dependencies, plus type-only, test-only, and dev-in-production dependencies
--unused-enum-membersReport only unused enum members
--unused-class-membersReport only unused class members
--unused-store-membersReport only unused Pinia store members
--unprovided-injectsReport only unprovided injects
--unrendered-componentsReport only unrendered components
--unused-component-propsReport only unused component props
--unused-component-emitsReport only unused component emits
--unused-component-inputsReport only unused component inputs
--unused-component-outputsReport only unused component outputs
--unused-svelte-eventsReport only unused Svelte dispatched events
--unused-server-actionsReport only unused server actions
--unused-load-data-keysReport only unused SvelteKit load() data keys
--unresolved-importsReport only unresolved imports
--unlisted-depsReport only unlisted dependencies
--duplicate-exportsReport only duplicate exports
--circular-depsReport only circular dependencies
--re-export-cyclesReport only re-export cycles (barrel files that re-export from each other in a loop, or self-loops). See explanations/dead-code#re-export-cycles
--package-cyclesReport only package cycles (workspace packages that import each other in a loop). See explanations/dead-code#package-cycles
--boundary-violationsReport only boundary violations
--policy-violationsReport only rule-pack policy violations (banned calls and banned imports that you declare in the rulePacks config key). See explanations/dead-code#policy-violations
--stale-suppressionsReport only stale suppression comments and @expected-unused JSDoc tags
--unused-catalog-entriesReport only unused pnpm catalog entries
--empty-catalog-groupsReport only empty named pnpm catalog groups
--unresolved-catalog-referencesReport only package references to missing pnpm catalog entries
--unused-dependency-overridesReport only unused pnpm dependency overrides
--misconfigured-dependency-overridesReport only malformed pnpm dependency overrides
--include-dupesAlso run duplication analysis and link its results to dead code findings
--finding-id <ID>Report only the findings with this finding_id. Repeat the flag or pass a comma-separated list. The JSON output adds a finding_id_query answer. A malformed id exits 2. See finding_id_query.

Scoping

FlagDescription
[PATH]Report only findings in this file or directory. Fallow still builds the full project graph. Default: the whole project.
--file <PATH>Scope output to specific files. Accepts multiple values. Fallow reports only issues in these files, and hides project-wide dependency issues. Warns on paths that do not exist. Use it with lint-staged.
--include-entry-exportsAlso report unused exports in entry files (package.json main and exports, framework pages, and others). Without this flag, fallow assumes that external code uses them. See global flags.

Type-aware TypeScript evidence

FlagDescription
--type-awareAdd exact evidence from the TypeScript checker for the project questions that fallow answers.
--type-aware-project <PATH>Select a TypeScript project. Repeat the flag for more projects.
--type-aware-require <MODE>best-effort gives advisory partial results. complete fails when the evidence is incomplete.
--symbol-impact <FILE:EXPORT>Return exact consumers, affected files, and targeted tests. Requires --type-aware.

Type-aware TypeScript analysis describes the safety contract, and what fallow checks compared with tsc and Oxlint.

Incremental

FlagDescription
--changed-since <REF>Check only files changed since a git ref
--baseline <PATH>Compare against a previously saved baseline file
--save-baseline <PATH>Save the current results as a baseline file. If the destination has a baseline of another kind, fallow refuses the save with exit 2. A destination with no kind gets one on the next save.
--fail-on-stale-baselineExit 1 when the loaded baseline has an entry that matched nothing in this run. See global flags
--fail-on-baseline-growthExit 1 when the loaded baseline has a key that the same file at the base ref does not have. Set the base ref with --baseline-base <REF>. See Baseline growth gate
--explain-skippedShow which built-in discovery ignore patterns removed source files, with counts
--productionProduction mode (exclude test, story, and dev files)
--top <N>Show only the top N items per issue category in human output

When a quarter or more of the saved baseline entries match no current issue, the run prints a partial-staleness warning on stderr with the command to save the baseline again. health --baseline works the same way. Some runs print no warning, because they cannot judge a whole-project baseline. These are runs narrowed by --file, --changed-since, --diff-file, --workspace, --changed-workspaces, a positional path, --production or an --unused-* filter. --fail-on-stale-baseline turns each stale entry into exit 1.

When the run loaded a baseline, the JSON output of the analysis run has a baseline_staleness object. fallow report --from renders a saved envelope again, and includes the object only if that envelope already had it. The object contains:

  • the counts
  • current_findings
  • change_scoped
  • the advisory result as warning
  • gate_trips, which applies the rule of --fail-on-stale-baseline, so a CI integration reads one boolean and does not need to repeat the rule

Read change_scoped before you divide the counts: a narrowed run can report matched_entries: 0 on a healthy baseline.

scope_reasons names what narrowed the run, so you do not have to guess which input caused it. It is present and not empty exactly when change_scoped is true. dead-code reads the flags and reports diff, changed-since, workspace, changed-workspaces, scope, file, issue-type-filter and production. issue-type-filter is an active --unused-* filter, which drops whole baseline categories before the comparison. Each command reports what it can see, so the names differ per command. Expect new names in the set.

Use scope_reasons to see if a repeat run can remove the scope. production, workspace and changed-workspaces describe what you consider the project to be, so a repeat run keeps them.

A run can load a baseline with entries and use only the inputs that a repeat run can remove: diff, changed-since, scope, file and issue-type-filter. Such a run also adds a recheck-baseline entry to next_steps[] with the unscoped command. That command reads the baseline again and reports. It never saves the baseline again.

  • A run in production mode, or a run scoped to workspaces, adds no entry. These settings can come from the config and the environment as well as from a flag, so the command in the entry would be just as narrow.
  • For the same reason, a diff-scoped run adds no entry while you export FALLOW_DIFF_FILE.
  • A bare fallow run follows the same rule and adds the same entry.

Each command writes its own baseline format, and a saved baseline names that format. Every file that --save-baseline writes has a top-level kind of dead-code, dupes or health. A baseline from an earlier release has no kind and still loads. For such a file, its keys decide the format.

A dead-code baseline also names its key form. --save-baseline writes a top-level "identity": "dc1" and one canonical key for each finding, for example unused-export:src/utils.ts:helper. A canonical key holds the rule, the root-relative path and the names. It never holds a line, a column or a suppression reason, so a line shift does not change the file. The file repeats a key once for each occurrence: N saved occurrences hide at most N current findings with that key.

A dead-code baseline from an earlier release has no identity and still loads. Each old entry matches only its exact old form. Some old forms hold a line, for example the keys of stale suppressions, so these entries can go stale after a line shift. A run that loads an old baseline prints a note on stderr in human output, and sets baseline_staleness.format to legacy in JSON. Neither fails the run. Run --save-baseline once to rewrite the file in the new form. An older fallow version reads a dc1 file without an error, but matches nothing in it, so save the baseline again if you downgrade. A baseline with an identity value that this version does not know exits 2.

fallow dead-code --baseline loads a baseline of another kind as zero entries, so no entry stays unmatched. Such a run prints a warning on stderr, with or without --quiet, that names both kinds and the path. It reports baseline_staleness.unrecognised_format: true and suppresses nothing. Next to it, saved_by names the command that saved the file: dead-code, dupes, or health. The saved_by field is absent for an empty file, for a baseline saved before files named their writer, and for a writer that this version does not know. The field is present only when it is true. An absent field means false. Read this field and not baseline_entries == 0, because a baseline from a project with nothing to record is correctly empty. The baseline gate trips on such a file, so --fail-on-stale-baseline exits 1. A run with no gate turned on keeps its exit code. Invalid JSON still exits 2. A dead-code baseline that has only some of its own keys also exits 2.

Regression detection

FlagDescription
--fail-on-regressionFail if the issue count grows by more than the tolerance, compared with a regression baseline
--tolerance <N>Allowed increase: "2%" (percentage) or "5" (absolute). Default: "0"
--regression-baseline <PATH>Path to regression baseline file (default: .fallow/regression-baseline.json)
--save-regression-baseline <PATH>Save the current issue counts as a regression baseline
Debugging flags
FlagDescription
--trace <FILE:EXPORT>Show where a specific export is used
--trace-file <PATH>Show all edges for a file
--trace-dependency <PACKAGE>Show where a dependency is used
--impact-closure <PATH>Show the files that a change to this file affects, through reverse dependencies and re-export chains
--performanceShow the time of each pipeline stage
# Why is formatDate reported as unused?
fallow dead-code --trace src/utils.ts:formatDate

# What imports/exports does this file have?
fallow dead-code --trace-file src/utils.ts

# Where is lodash used?
fallow dead-code --trace-dependency lodash

# Pipeline performance breakdown
fallow dead-code --performance

Examples

fallow dead-code

Regression detection

To stop the issue count from growing, save a regression baseline on your main branch. Then compare later runs against it, for example in PR checks.

  1. Save a regression baseline on main:

    fallow dead-code --save-regression-baseline

    This writes the issue counts to .fallow/regression-baseline.json (or a custom path).

  2. On each PR, compare against the baseline:

    fallow dead-code --fail-on-regression

    fallow exits with code 1 if the total issue count is more than the baseline plus the configured tolerance.

  3. Optional: set a tolerance to allow small changes:

    fallow dead-code --fail-on-regression --tolerance 2%   # percentage
    fallow dead-code --fail-on-regression --tolerance 5     # absolute

With --fail-on-regression and --format json, the output can include a regression object:

{
  "regression": {
    "status": "pass",
    "baseline_total": 42,
    "current_total": 45,
    "delta": 3,
    "tolerance": 2.0,
    "tolerance_kind": "percentage",
    "exceeded": false
  }
}
FieldDescription
statuspass, exceeded, or skipped
baseline_totalIssue count from the baseline file
current_totalIssue count from the current run
deltacurrent_total - baseline_total
toleranceConfigured tolerance value
tolerance_kindpercentage or absolute
exceededtrue when the delta is more than the tolerance
reasonPresent only when status is skipped (for example, baseline file not found)

These flags also work with fallow dupes and bare fallow (all analyses). The regression check compares the total issue counts of all enabled analyses.

Example output

unused-file:src/server/jobs/worker.ts
unused-file:src/features/savings/hooks/usePotGroups.ts
unused-file:src/server/jobs/cron.ts
unused-export:src/server/jobs/queue.ts:61:enqueueJobDelayed
unused-export:src/server/jobs/queue.ts:206:sweepStuckProcessingJobs
unused-export:src/components/Card/index.ts:1:CardFooter
re-export-cycle:src/api/index.ts:src/api/index.ts <-> src/api/internal/index.ts
re-export-cycle:src/utils/index.ts:src/utils/index.ts (self-loop)
package-cycle:packages/core/src/format.ts:3:@acme/core → @acme/utils → @acme/core

In --format json, re-export cycles have their own field, with the shape of each cycle:

{
  "kind": "dead-code",
  "schema_version": 9,
  "re_export_cycles": [
    {
      "files": ["src/api/index.ts", "src/api/internal/index.ts"],
      "kind": "multi-node",
      "actions": [
        {
          "type": "fix",
          "kind": "refactor-re-export-cycle",
          "auto_fixable": false,
          "description": "Remove one `export * from` (or `export { ... } from`) statement on any one member to break the cycle"
        },
        {
          "type": "suppress-file",
          "kind": "suppress-file",
          "auto_fixable": false,
          "comment": "// fallow-ignore-file re-export-cycle"
        }
      ]
    },
    {
      "files": ["src/utils/index.ts"],
      "kind": "self-loop",
      "actions": [...]
    }
  ]
}

files is sorted lexicographically. The multi-node shape has two or more entries, and the self-loop shape has exactly one. explanations/dead-code#re-export-cycles explains why the cycles have these shapes.

Fix suggestions in JSON output

With --format json, each issue has an actions array with fix and suppress hints that a script or agent can apply.

{
  "path": "src/utils.ts",
  "export_name": "helperFn",
  "line": 10,
  "actions": [
    {
      "type": "remove-export",
      "auto_fixable": true,
      "description": "Remove the unused export from the public API"
    },
    {
      "type": "suppress-line",
      "auto_fixable": false,
      "description": "Suppress with an inline comment above the line",
      "comment": "// fallow-ignore-next-line unused-export"
    }
  ]
}

Each action has these fields:

FieldDescription
typeFix action type in kebab-case (e.g. remove-export, remove-file, suppress-line, add-to-config)
auto_fixabletrue when fallow fix can apply this action automatically. Fallow sets it per finding, not per action type. The same type can be true on one finding and false on another. For example, remove-catalog-entry changes with hardcoded_consumers, and the primary dependency action changes between remove-dependency and move-dependency with used_in_workspaces. Filter on the auto_fixable value of each action, not on type. Auto-fix lists all these per-finding changes.
descriptionA readable explanation of the action
comment(optional) The inline suppression comment to add
note(optional) More context for items that fallow fix cannot fix
config_key(optional) The config key to modify, e.g. "ignoreDependencies" for dependency issues
value(optional) Value to write at config_key. Scalar for keys like ignoreDependencies; array of { file, exports } rule objects for ignoreExports
value_schema(optional) URL of the JSON Schema fragment that describes value. An agent can fetch this schema to validate the payload before it writes the payload into the config of a user
scope(optional) "per-location" on suppress actions that apply to one location of a finding, for example on duplicate_exports (which span more than one file) and stale_suppressions

Re-export findings include a warning note about the effect on the public API. Dependency issues use an add-to-config suppress action (not an inline comment) with the package name and config_key: "ignoreDependencies".

Additional JSON fields

With --format json, the output can include these top-level objects:

entry_points

Counts the resolved entry point files, in total and per source. The module graph starts from these files:

{
  "entry_points": {
    "total": 3,
    "sources": {
      "package.json": 1,
      "plugin": 2
    }
  }
}

summary

The JSON output always includes a summary counts object, with a count for each issue type. This excerpt shows some of the keys:

{
  "summary": {
    "unused_files": 3,
    "unused_exports": 12,
    "unused_types": 2,
    "private_type_leaks": 0,
    "unused_dependencies": 1,
    "unresolved_imports": 0,
    "circular_dependencies": 1,
    "total_issues": 19
  }
}

private_type_leaks is always in the JSON output, so the schema stays stable, but the rule is off by default. Fallow fills it only when you enable it with --private-type-leaks, or set private-type-leaks to warn or error in the configuration.

deprecated_exports_in_use works the same way: the array and the summary.deprecated_exports_in_use count are always in the JSON output. Fallow fills the array only when you enable the rule with --deprecated-exports-in-use or in rules. An unused_exports entry for an export with a @deprecated tag has deprecated: true and, when the tag has a message, deprecated_reason. Both keys are absent when the export is not deprecated.

Each finding has an optional effective_severity field (error or warn) with its rule severity, after overrides[].rules. The CI formats read it. See Levels in CI formats.

used_in_workspaces

An unused dependency finding can include used_in_workspaces. This happens when the workspace that declares the package does not use it, but another workspace in the monorepo imports it:

{
  "unused_dependencies": [
    {
      "package_name": "lodash-es",
      "location": "dependencies",
      "path": "packages/shared/package.json",
      "line": 5,
      "used_in_workspaces": ["packages/consumer"]
    }
  ]
}

In human, markdown, GitHub, and GitLab output, this shows as "imported in" or as an "Imported elsewhere" column. The dependency is in the wrong workspace. Move it to the workspace that imports it, and do not remove it automatically.

finding_id

Each dead-code finding has an optional finding_id, stale suppressions included. Use it to track one finding across runs, or to join a finding to its SARIF result, editor diagnostic or CI review thread.

{
  "unused_exports": [
    {
      "path": "src/utils.ts",
      "export_name": "helper",
      "line": 12,
      "finding_id": "dc1:unused-export:81a349a3b9ea3b15"
    }
  ]
}

The form is dc1:<rule>:<16 hex digits>. Fallow computes the id from the rule and the subject of the finding: the root-relative path and the symbol name. The line and the column are not inputs, so these changes keep the id:

  • you add lines above the finding
  • you reformat the file
  • you reorder the declarations

A rename of the file or the symbol gives a new id. Another issue type also gives a new id. When two findings of one type have the same subject, for example a static and an instance member with one name, the first keeps the base id and the next one gets the suffix ~1.

Fallow computes the ids on the full result set, before the filters run. Workspace scope, --changed-since, ignoreFindings and baselines therefore never change the id of a finding that stays in the report. A finding that is absent from a scoped run, or from a run with other config, is unknown in that run. It is not resolved. To ask if a finding still exists, use --finding-id.

The field is optional in the JSON schema, so schema_version does not change. Security findings keep their own finding_id form. See fallow security.

finding_id_query

--finding-id reports only the findings that you ask for. Repeat the flag or pass a comma-separated list:

fallow dead-code --finding-id dc1:unused-export:81a349a3b9ea3b15 --format json --quiet
fallow dead-code --finding-id dc1:unused-file:0123456789abcdef,dc1:unused-export:81a349a3b9ea3b15 --format json --quiet

The filter runs after every other filter and after the baseline, and it does not change the ids. The JSON output then has a finding_id_query object:

{
  "finding_id_query": {
    "requested": ["dc1:unused-file:0123456789abcdef", "dc1:unused-export:81a349a3b9ea3b15"],
    "found": ["dc1:unused-export:81a349a3b9ea3b15"],
    "missing": ["dc1:unused-file:0123456789abcdef"],
    "filtered": [],
    "conclusive": true,
    "inconclusive_reasons": [],
    "analysis_fingerprint": "af1:0123456789abcdef"
  }
}
FieldDescription
requestedThe requested ids, without duplicates, in the order of the arguments
foundThe requested ids that the report contains
missingThe requested ids that the report does not contain
filteredThe requested ids that the analysis found before a filter of this run removed them. These findings still exist.
conclusivetrue when no option of this run can hide a finding that still exists, and no requested id was filtered
inconclusive_reasonsWhy the answer is not conclusive, sorted. Empty exactly when conclusive is true.
analysis_fingerprintA hash (af1:<16 hex digits>) of the inputs that decide which findings the run reports, other than the source code

A missing id means "resolved" only when conclusive is true. The finding is then fixed, suppressed with an inline comment, or ignored by config such as ignoreFindings. When conclusive is false, the state of a missing id is unknown. These options make the answer not conclusive, because each one can hide a finding that still exists:

  • a diff, --changed-since, --workspace, --changed-workspaces, a positional path or --file
  • an issue-type filter such as --unused-exports
  • production mode or includeEntryExports, from the flag or from the config
  • --baseline
  • a rule set to off for the rule of a missing id, in rules or overrides[].rules (rule-off)
  • a requested id in filtered

inconclusive_reasons is an open set of kebab-case names. Read a name that you do not know as "not conclusive". A project with production mode in its config never gets a conclusive answer.

analysis_fingerprint hashes the fallow version, the merged config, the loaded plugins and rule packs, the detection options, the ignore files, the package.json, tsconfig and jsconfig files and the plugin config files (for example vite.config.ts). Store it with your verdict. When a later query gives another fingerprint, treat a missing id as unknown, also when conclusive is true. An edit to a manifest or a tsconfig changes the fingerprint, also when the edit fixes a dependency finding.

The exit code follows the normal rule for the filtered report: 1 when a reported finding has error severity, 0 when every requested id is missing. A malformed id exits 2, so a typo never reads as "resolved". Regression detection and --save-baseline see the findings before the id filter. In human output, a note on stderr gives the counts and says if the answer is conclusive.

The MCP analyze tool (finding_ids) and the Node bindings (findingIds on detectDeadCode) take the same option and return the same finding_id_query.

baseline

With --baseline, a baseline object shows how many entries the baseline file has, and how many of them matched a current issue and were filtered out:

{
  "baseline": {
    "entries": 19,
    "matched": 14
  }
}

The baseline_staleness object has the details. See Incremental.

Unmatched config patterns in workspace_diagnostics

When an ignoreDependencies glob matches no declared dependency, or an ignoreFindings pattern matches no finding, workspace_diagnostics[] has one entry for each such pattern:

{
  "workspace_diagnostics": [
    {
      "path": ".",
      "kind": "ignore-findings-pattern-unmatched",
      "pattern": "src/legcy/**",
      "message": "ignoreFindings pattern 'src/legcy/**' matched no finding in this run, so it has no effect. A pattern is a glob relative to the project root. Fix the pattern, or remove it from the config."
    }
  ]
}

The other output formats name the same patterns: SARIF as toolConfigurationNotifications, Markdown, the job summary, the PR comment and the review summary body as a section, and the other formats as a note on stderr. See Unmatched config patterns.

Stale suppression detection

When you fix an issue, its suppression comment can stay behind. Fallow finds // fallow-ignore comments and /** @expected-unused */ JSDoc tags that no longer match an issue, so you can remove them.

# Only report stale suppressions
fallow dead-code --stale-suppressions

# JSON output includes stale_suppressions array
fallow dead-code --format json --stale-suppressions

Fallow detects two types of stale suppressions:

OriginWhen it's stale
// fallow-ignore-next-line / // fallow-ignore-fileThe suppression no longer matches an issue on the target line or in the file
/** @expected-unused */ JSDoc tagThe tagged export is now imported by another module

The stale-suppressions rule defaults to warn. To enforce it in CI, set it to error:

{
  "rules": {
    "stale-suppressions": "error"
  }
}

With --format json, stale suppressions are in the stale_suppressions array:

{
  "stale_suppressions": [
    {
      "path": "src/utils.ts",
      "line": 5,
      "col": 0,
      "origin": {
        "type": "comment",
        "issue_kind": "unused-export",
        "is_file_level": false
      }
    },
    {
      "path": "src/lib.ts",
      "line": 10,
      "col": 0,
      "origin": {
        "type": "jsdoc_tag",
        "export_name": "createWidget"
      }
    }
  ]
}

Use /** @expected-unused */ and not // fallow-ignore-next-line when you want fallow to tell you if a suppressed export becomes used later. Fallow checks both for staleness, but @expected-unused shows your intent more clearly.

See also