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
| Flag | Description |
|---|---|
-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, --quiet | Suppress progress output |
--fail-on-issues | Exit with code 1 if fallow finds issues |
--sarif-file <PATH> | Write SARIF output to a file, in addition to --format |
--ci | CI mode: sets the format to SARIF, turns on fail-on-issues, and hides progress. Individual flags can still override these settings. |
--explain | Add 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. |
--summary | In 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
| Flag | Description |
|---|---|
--unused-files | Report only unused files |
--unused-exports | Report only unused exports |
--unused-types | Report only unused types |
--private-type-leaks | Opt in to private type leak findings (API hygiene) and report only that issue type |
--deprecated-exports-in-use | Opt in to deprecated exports in use (@deprecated exports that reachable files still use) and report only that issue type |
--unused-deps | Report only dependency findings: unused dependencies, dev dependencies, and optional dependencies, plus type-only, test-only, and dev-in-production dependencies |
--unused-enum-members | Report only unused enum members |
--unused-class-members | Report only unused class members |
--unused-store-members | Report only unused Pinia store members |
--unprovided-injects | Report only unprovided injects |
--unrendered-components | Report only unrendered components |
--unused-component-props | Report only unused component props |
--unused-component-emits | Report only unused component emits |
--unused-component-inputs | Report only unused component inputs |
--unused-component-outputs | Report only unused component outputs |
--unused-svelte-events | Report only unused Svelte dispatched events |
--unused-server-actions | Report only unused server actions |
--unused-load-data-keys | Report only unused SvelteKit load() data keys |
--unresolved-imports | Report only unresolved imports |
--unlisted-deps | Report only unlisted dependencies |
--duplicate-exports | Report only duplicate exports |
--circular-deps | Report only circular dependencies |
--re-export-cycles | Report 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-cycles | Report only package cycles (workspace packages that import each other in a loop). See explanations/dead-code#package-cycles |
--boundary-violations | Report only boundary violations |
--policy-violations | Report 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-suppressions | Report only stale suppression comments and @expected-unused JSDoc tags |
--unused-catalog-entries | Report only unused pnpm catalog entries |
--empty-catalog-groups | Report only empty named pnpm catalog groups |
--unresolved-catalog-references | Report only package references to missing pnpm catalog entries |
--unused-dependency-overrides | Report only unused pnpm dependency overrides |
--misconfigured-dependency-overrides | Report only malformed pnpm dependency overrides |
--include-dupes | Also 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
| Flag | Description |
|---|---|
[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-exports | Also 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
| Flag | Description |
|---|---|
--type-aware | Add 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
| Flag | Description |
|---|---|
--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-baseline | Exit 1 when the loaded baseline has an entry that matched nothing in this run. See global flags |
--fail-on-baseline-growth | Exit 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-skipped | Show which built-in discovery ignore patterns removed source files, with counts |
--production | Production 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_findingschange_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
fallowrun 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
| Flag | Description |
|---|---|
--fail-on-regression | Fail 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
| Flag | Description |
|---|---|
--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 |
--performance | Show 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 --performanceExamples
fallow dead-codeRegression 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.
-
Save a regression baseline on
main:fallow dead-code --save-regression-baselineThis writes the issue counts to
.fallow/regression-baseline.json(or a custom path). -
On each PR, compare against the baseline:
fallow dead-code --fail-on-regressionfallow exits with code 1 if the total issue count is more than the baseline plus the configured tolerance.
-
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
}
}
| Field | Description |
|---|---|
status | pass, exceeded, or skipped |
baseline_total | Issue count from the baseline file |
current_total | Issue count from the current run |
delta | current_total - baseline_total |
tolerance | Configured tolerance value |
tolerance_kind | percentage or absolute |
exceeded | true when the delta is more than the tolerance |
reason | Present 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:
| Field | Description |
|---|---|
type | Fix action type in kebab-case (e.g. remove-export, remove-file, suppress-line, add-to-config) |
auto_fixable | true 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. |
description | A 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"
}
}
| Field | Description |
|---|---|
requested | The requested ids, without duplicates, in the order of the arguments |
found | The requested ids that the report contains |
missing | The requested ids that the report does not contain |
filtered | The requested ids that the analysis found before a filter of this run removed them. These findings still exist. |
conclusive | true when no option of this run can hide a finding that still exists, and no requested id was filtered |
inconclusive_reasons | Why the answer is not conclusive, sorted. Empty exactly when conclusive is true. |
analysis_fingerprint | A 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
offfor the rule of a missing id, inrulesoroverrides[].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:
| Origin | When it's stale |
|---|---|
// fallow-ignore-next-line / // fallow-ignore-file | The suppression no longer matches an issue on the target line or in the file |
/** @expected-unused */ JSDoc tag | The 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.