Options
| Flag | Description |
|---|---|
--dry-run | Preview changes without applying |
--yes, --force | Skip confirmation (required in non-TTY) |
--no-create-config | Refuse 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
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 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.yamlby line-aware deletion. Object-form entries such asreact:\n specifier: ^18.2.0are 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 withfix.catalog.deletePrecedingComments("auto","always", or"never"). Two escape hatches keep curated comments safe regardless of policy: a# fallow-keepmarker on any line in the block preserves it (mirrors the inline-suppression convention), and theautopolicy 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 tocatalog: {}(or<name>: {}) so the file stays installable; barecatalog:with no value parses as null and pnpm errors at install time. Entries whosehardcoded_consumersis non-empty (a workspace package still pins a hardcoded version of the same package) are skipped to avoid breaking the nextpnpm 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 bothline(first deleted line, the leading comment when policy absorbs one) andentry_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 runpnpm installsopnpm-lock.yamlstays 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
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,.fallowrc.jsonis created using the same scaffoldingfallow initwould emit (framework detection,$schema,entry,ignorePatterns, etc.) and the rules are layered on top, so the post-fixfallow checkdoesn’t start surfacing new findings.
fallow dead-code to find those.
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 (TypeScript / Storybook / Vitest / Jest / Playwright / React / Vue / Angular / Svelte detection, sensible defaults, plus the new ignoreExports rules). 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
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 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 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\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.