Options
What gets fixed
- Unused exports: the
exportkeyword is removed, keeping the declaration. For exported enums where the enum itself is never referenced outside its body in the file, the entireenumblock is deleted. - Unused dependencies: removed from
package.jsonwhen 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 then 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. See Catalog entries. - Duplicate exports: an
ignoreExportsrule is appended to your fallow config file. Common in shadcn / Radix / bits-ui namespace-barrel codebases where dozens ofcomponents/<name>/index.tsfiles intentionally re-export the same short names. When no fallow config file exists, one is created; see Missing config file.
fallow dead-code to find those.
Catalog entries
Catalog entries are removed frompnpm-workspace.yaml by line-aware deletion. Object-form entries such as react:\n specifier: ^18.2.0 are removed as one block. 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. Other comments and stylistic choices in the file are preserved. After a successful catalog edit fallow reminds you to run pnpm install so pnpm-lock.yaml stays in sync.
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 ===). The human-mode summary appends (+M catalog comment lines) to the fixed-issue count when at least one comment line was absorbed.
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 on 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. The JSON envelope carries a top-level "skipped" count alongside "total_fixed" for partial-fix gating in CI.
Missing config file
Whenfallow fix finds duplicate-export issues and no fallow config exists, it creates .fallowrc.json automatically, scaffolded the same way as fallow init (framework and test-runner detection, $schema, entry, ignorePatterns) with the ignoreExports rules layered on top, so the post-fix fallow dead-code does not start surfacing new findings. The created file is reported on stderr:
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:
fallow watch where silently materialising a new top-level file would surprise the user, pass --no-create-config:
On-disk drift protection
If a parallel editor save, CI rebase, or other tool changes a source file between analysis and the write,fallow fix skips that file instead of applying stale line offsets. The skip is reported on stderr (Skipping <path>: file content changed since fallow dead-code 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 tallies catalog / YAML guard skips only).
Each per-file rewrite is staged into a sibling temp file, and every stage is promoted to its final path only after all stages succeed. 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 a re-run is safe. package.json and pnpm-workspace.yaml are re-read at fix time instead, so a dependency or catalog entry that has already disappeared is a no-op.
Low-confidence export removals
Removing anexport 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, orgolden(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.
{"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 dead-code, so you can confirm it is truly unused and remove it by hand. High-confidence exports in normal source files are removed.
Incomplete source evidence
Findings with nonemptyreachability_caveats remain reported, but their removals are not applied automatically. This includes missing references caused by parse errors, unreadable files, and skipped source files. workspace_diagnostics identifies the source of the incomplete evidence. Type-aware approval of a class method does not override this guard.
The fix entry has skipped: true and skip_reason: "low_confidence_incomplete_analysis". The JSON envelope reports separate counters for skipped_low_confidence_exports, skipped_low_confidence_dependencies, and skipped_low_confidence_members. They are distinct from the general skipped counter. These evidence-based skips do not change the exit code; inspect them even when the command succeeds.
File encoding contract
fallow fix is UTF-8 only. Two encoding shapes are 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 do not 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\nand bare-LF line endings (common after cross-platform edits withoutcore.autocrlf) are skipped instead of silently rewritten to the wrong offsets. The stderr message names the remediation:The JSON envelope carries a per-file entry ({"type": "skipped", "path": "...", "skipped": true, "skip_reason": "mixed_line_endings"}) and a top-level counterskipped_mixed_line_endings: number(always present) disjoint fromskipped_content_changed. Any non-zero mixed-EOL count exits the run with code 2. The skip is not self-healing: re-runningfallow fixagainst 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):
--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
Example output
$ fallow fix
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.