Skip to main content
Automatically remove unused exports and dependencies from your codebase. Fallow modifies your source files in place, so treat this like a refactoring tool.
Always commit your changes before running fallow fix. This gives you a clean rollback point if anything unexpected happens.
Use --dry-run first to preview exactly what will be removed before applying any changes.
fallow fix

Options

FlagDescription
--dry-runPreview changes without applying
--yes, --forceSkip confirmation (required in non-TTY)
--no-create-configRefuse to create a new .fallowrc.json when none exists (use in pre-commit hooks, CI bots, and fallow watch)
-f, --format <FORMAT>Output format: human (default), json

What gets fixed

  • Unused exports: the export keyword is removed, keeping the declaration. For exported enums where the enum itself is never referenced outside its body in the file, the entire enum block is deleted.
  • Unused dependencies: removed from package.json when they are not imported by another workspace
  • Unused enum members: removed from the enum declaration. When every member of an exported enum is unused, the whole declaration is deleted in one pass instead of leaving behind an empty export enum X {} shell. Importers in other files now reference a name that no longer exists, so run your TypeScript build to find and clean them up.
  • Unused pnpm catalog entries: removed from pnpm-workspace.yaml by line-aware deletion. Object-form entries such as react:\n specifier: ^18.2.0 are removed as one block. By default, fallow also removes a contiguous YAML comment block immediately above the entry when it clearly belongs to that entry; configure this with fix.catalog.deletePrecedingComments ("auto", "always", or "never"). Two escape hatches keep curated comments safe regardless of policy: a # fallow-keep marker on any line in the block preserves it (mirrors the inline-suppression convention), and the auto policy additionally preserves section-banner blocks whose comment body starts with three or more =, -, *, _, ~, +, or # characters (e.g. # === React 18 production pins ===). Other comments and stylistic choices in the file are preserved. When the last entry of a catalog group is removed, the header is rewritten to catalog: {} (or <name>: {}) so the file stays installable; bare catalog: with no value parses as null and pnpm errors at install time. Entries whose hardcoded_consumers is non-empty (a workspace package still pins a hardcoded version of the same package) are skipped to avoid breaking the next pnpm install; the skip is surfaced in stderr and in the JSON output ({"type": "remove_catalog_entry", "applied": false, "skipped": true, "skip_reason": "hardcoded_consumers", "consumers": [...]}). The JSON action carries both line (first deleted line, the leading comment when policy absorbs one) and entry_line (the catalog entry’s original 1-based line) so CI annotators can pick whichever anchor they need. After a successful catalog edit fallow reminds you to run pnpm install so pnpm-lock.yaml stays in sync; the human-mode summary appends (+M catalog comment lines) to the fixed-issue count when at least one comment line was absorbed. The JSON envelope grows a top-level "skipped" count alongside "total_fixed" for partial-fix gating in CI.
  • Duplicate exports: an ignoreExports rule is appended to your fallow config file. Common in shadcn / Radix / bits-ui namespace-barrel codebases where dozens of components/<name>/index.ts files intentionally re-export the same short names. When no fallow config file exists, .fallowrc.json is created using the same scaffolding fallow init would emit (framework detection, $schema, entry, ignorePatterns, etc.) and the rules are layered on top, so the post-fix fallow check doesn’t start surfacing new findings.
Unused files and class members are not auto-fixed. Use fallow dead-code to find those.

Missing config file

When fallow fix finds duplicate-export issues and no fallow config exists, it creates .fallowrc.json automatically, scaffolded the same way as fallow init (TypeScript / Storybook / Vitest / Jest / Playwright / React / Vue / Angular / Svelte detection, sensible defaults, plus the new ignoreExports rules). The created file is reported on stderr:
Created .fallowrc.json with 2 ignoreExports rule(s). Add it to git.
In monorepo subpackages (one of pnpm-workspace.yaml, package.json#workspaces, turbo.json, lerna.json, rush.json above the invocation directory), the create-fallback refuses to avoid fragmenting one config file across N sub-packages. Instead it emits a directive pointing at the workspace root:
Skipped duplicate-export config fix: no fallow config file at packages/ui and the
directory is inside a monorepo (workspace root: /repo). Run `fallow init` at the
workspace root, or invoke `fallow fix` from /repo instead of from a subpackage.
For pre-commit hooks, CI bots, and fallow watch where silently materialising a new top-level file would surprise the user, pass --no-create-config:
# Pre-commit hook
fallow fix --yes --no-create-config

# CI: never create configs as a side effect
fallow fix --yes --no-create-config --format json
The duplicate-export config-add path is skipped with an explanatory message; source-file edits (unused exports, dependencies, enum members, catalog entries) proceed normally.

On-disk drift protection

fallow fix re-runs analysis in-process and then writes each per-file rewrite. If a parallel editor save, CI rebase, or other tool mutates a target file between the analysis read and the write, the line offsets computed during analysis no longer line up with what is currently on disk. fallow now captures every parsed source file’s xxh3 content hash during analysis (the same hash the extract cache uses), recomputes it at fix time, and skips files whose content drifted. The diagnostic surfaces on stderr (Skipping <path>: file content changed since fallow check ran. Re-run fallow fix to refresh the analysis first.) and in the JSON output ({"type": "skipped", "path": "...", "skipped": true, "skip_reason": "content_changed"}). A run with any content-changed skip exits with code 2 so CI does not treat the partial run as a clean no-op. The JSON envelope’s top-level skipped_content_changed: number (always present) carries the count separately from skipped (which still tallies catalog / YAML guard skips only). fallow fix also batches its writes: each per-file rewrite is staged into a sibling temp file, and the orchestrator promotes every stage to its final path only after every stage has succeeded. A stage failure (read-only directory, full disk) leaves every target file at its original content. A rename failure mid-batch (rare; the per-file rename is atomic but POSIX has no atomic multi-rename primitive) reports per-path so the user can re-run with confidence. Hash precondition covers source files (TS, JS, Vue, Svelte, Astro, MDX); package.json and pnpm-workspace.yaml are not in the captured hash map because the extract layer does not parse them, but the dep and catalog fixers re-parse those files at fix time as the natural safety net (a key-lookup miss is a no-op, not a corruption).

Low-confidence export removals

Removing an export keyword is only safe when fallow can see every consumer of that symbol. Some consumers are invisible to static analysis: Vitest mock aliases (test.alias pointing at a __mocks__ file), off-workspace end-to-end suites that import shared helpers, and fixture or golden harnesses wired up by a build step. Stripping an export that one of these actually uses turns an analysis false positive into a tsc error and a broken build. fallow fix therefore withholds export removals in two low-confidence cases:
  • Off-graph consumer directories. The file sits under any of __mocks__, __fixtures__, fixtures, e2e, e2e-tests, cypress, playwright, examples, evals, or golden (matched on any path segment). Plain test directories (test, tests, __tests__) are intentionally not on the list, because genuinely-dead test helpers are common and should still auto-remove.
  • Files with an unresolved import. The file itself imports something fallow could not resolve, so its local usage graph is incomplete and the “this export is unused” conclusion is lower confidence.
In both cases the per-file entry is emitted as {"type": "skipped", "path": "...", "skipped": true, "skip_reason": "low_confidence_off_graph"} (or "low_confidence_unresolved_imports"), and the JSON envelope’s top-level skipped_low_confidence_exports: number (always present) carries the combined count, disjoint from skipped. Unlike the drift and encoding skips, this is an intentional, conservative decision rather than a recoverable failure: it does not change the exit code. The export is still reported by fallow check, so you can confirm it is truly unused and remove it by hand. High-confidence exports in normal source files are removed exactly as before.

File encoding contract

fallow fix is UTF-8 only. Two encoding shapes that previously caused silent corruption are now handled explicitly:
  • UTF-8 BOM round-trip. Files with a leading UTF-8 byte-order mark (EF BB BF, common on Windows-authored TypeScript when Notepad / older VS settings or some IDE plugins are involved) are read with the BOM stripped before line-offset computation and parsing, so reported line numbers no longer shift by the BOM codepoint, and the BOM is re-prepended on write so the file’s encoding is preserved on round-trip. fallow neither adds nor removes a BOM; if your input has one, the output has one.
  • Mixed CRLF / LF rejection. Files containing both \r\n and bare-LF line endings (common after cross-platform edits without core.autocrlf) are skipped instead of silently rewritten to the wrong offsets. The stderr message names the remediation:
    Skipping src/mixed.ts: file has mixed CRLF/LF line endings.
    Normalize with `dos2unix` or set `git config core.autocrlf input`, then re-run `fallow fix`.
    
    The JSON envelope carries a per-file entry ({"type": "skipped", "path": "...", "skipped": true, "skip_reason": "mixed_line_endings"}) and a top-level counter skipped_mixed_line_endings: number (always present) disjoint from skipped_content_changed. Any non-zero mixed-EOL count exits the run with code 2. The skip is not self-healing: re-running fallow fix against the same file produces the same skip; you (or your CI / AI agent) must normalize the file’s line endings before fallow can act on it. When the same file carries findings for multiple fixers (an unused export AND an unused enum member), the skip is reported once per file, not once per fixer.

Dry-run config diff

fallow fix --dry-run against duplicate-export findings prints the proposed config write as a unified diff so you can audit the exact bytes that would land before passing --yes. For the create-fallback case the diff shows the full proposed file (BEFORE is empty):
Would create .fallowrc.json with 2 ignoreExports rule(s):
--- .fallowrc.json (does not exist)
+++ .fallowrc.json (proposed)
@@ -0,0 +1,35 @@
+{
+  "$schema": "https://raw.githubusercontent.com/fallow-rs/fallow/main/schema.json",
+  "entry": ["src/index.{ts,tsx,js,jsx}", "src/main.{ts,tsx,js,jsx}"],
+  "ignorePatterns": [".storybook/**"],
+  ...
+  "ignoreExports": [
+    { "file": "src/components/Button/index.ts", "exports": ["*"] },
+    { "file": "src/components/Card/index.ts", "exports": ["*"] }
+  ]
+}
For the edit case (existing config), the diff shows context lines around the proposed additions. JSON output (--dry-run --format json) includes the same diff under a proposed_diff field on the fix entry, plus a created_files: [".fallowrc.json"] array on the create-fallback path so agents can detect file-creation side effects programmatically.

Examples

# See what would be removed
fallow fix --dry-run

# JSON output for scripting
fallow fix --dry-run --format json

Example output

$ fallow fix
Would remove export from src/components/Card/index.ts:1 `CardFooter`
Would remove export from src/providers/trpc-provider/index.tsx:12 `TRPCProvider`
Would remove export from src/server/jobs/queue.ts:61 `enqueueJobDelayed`
Would remove export from src/server/jobs/queue.ts:206 `sweepStuckProcessingJobs`
Would remove export from src/server/jobs/queue.ts:276 `getDeadLetterJobs`
Would remove `@trpc/react-query` from dependencies

6 changes to apply. Proceed? [y/N] y

 Applied 6 fixes (5 exports, 1 dependency)

See also

Auto-fix

Details on what fallow can and cannot auto-fix.

Dead code analysis

Understand the full range of dead code fallow detects.