CLI reference for fallow audit. Combined dead code, complexity, and duplication analysis scoped to changed files, with a pass/warn/fail verdict for PR quality gates and AI-generated code review.
Audit changed files for dead code, complexity, and duplication. Returns a pass, warn, or fail verdict based on the severity of issues found. Scoping to changed files keeps signal-to-noise high: by default only issues introduced by the current PR or commit fail the gate; inherited findings in touched files are shown as context.
fallow audit is the PR-time gate. For the initial full-repo cleanup on an existing codebase, start with Adopt Fallow in an existing repo, which walks through fallow, fallow dead-code, fallow dupes, and fallow health, and hands the actual cleanup to an AI agent before turning audit on in CI.
fallow audit auto-detects your base branch. Run it without arguments for a zero-config quality check.
Git ref to compare against (e.g., main, HEAD~5, a commit SHA). Alias for --changed-since. When omitted, the base is the git merge-base against the branch’s upstream or the remote default (origin/main); set FALLOW_AUDIT_BASE to pin it without a flag.
Path to a pre-computed unified diff (e.g. git diff --unified=0 main...HEAD > /tmp/pr.diff). Narrows source-anchored findings to lines inside an added hunk: point findings drop when their source line is off-diff, range findings (complexity hotspots, clone families) drop when their span does not overlap the diff. Project-level findings (unused deps, catalog entries, dependency overrides) bypass the analysis filter. Sticky PR/MR summaries can set FALLOW_SUMMARY_SCOPE=diff to filter those project-level findings too. Also drives line-level scoping of the runtime-coverage hot-path-touched verdict. Pass - for stdin (gh pr diff | fallow audit --diff-file -) or use the --diff-stdin alias. Falls back to the FALLOW_DIFF_FILE env var. The bundled GitHub Action and GitLab CI template pre-compute the diff and pass it automatically; you only need this flag for non-PR pipelines or local invocations.
Default. Compare against the base ref and fail only on findings introduced by the current changeset. Inherited findings are reported in JSON attribution and annotated with introduced: false.
--gate all
Strict mode. Fail on every finding in changed files, including findings inherited from the base ref. Skips the extra base-snapshot attribution pass for lower latency.
The gate has three surface forms. They all accept the same values (new-only, all) and the precedence is CLI flag > config > MCP param > default.
Under --gate new-only, audit normally runs every analysis twice: once on the current tree and once on a temporary worktree at the base ref so it can attribute each finding as introduced or inherited. When every changed file is either a non-behavioral doc (.md, .markdown, .txt, .rst, .adoc) or token-equivalent at the base ref (a comment-only or whitespace-only edit on a .ts/.tsx/.js/.jsx/.mjs/.mts/.cjs/.cts source file), audit reuses the current run’s findings as the base snapshot, classifies everything as inherited, and skips the second worktree analysis entirely. Common case: docs-only PRs and formatter sweeps complete in roughly one analysis pass instead of two.Run fallow audit --base main --gate new-only --performance to see whether the fast path fired: the JSON output includes base_snapshot_skipped: true|false and the human output prints the same on stderr. The optimization activates automatically whenever every entry in the diff qualifies; there is no flag to opt in or out.
Render the deterministic review brief instead of the gating audit report. Answers “where do I look?” rather than “will CI block this?”, runs the same analysis, and always exits 0. Orthogonal to --format. Implied by fallow review. See Review brief and decision surface.
--max-decisions <N>
Cap on the number of consequential structural decisions surfaced in the brief’s decision surface (the working-memory limit). Default 4; clamped to a 3 to 5 band. Only consulted on the brief path.
--walkthrough-guide
Emit the agent-contract walkthrough guide: the current digest (brief + decision surface), the review direction, the JSON schema the agent must return, and a deterministic graph-snapshot hash pinned into the digest. The digest is built from the graph only (PR prose is never folded in, so it is injection-resistant). Implies the brief; always exits 0. See Agent walkthrough loop.
--walkthrough-file <PATH>
Ingest an agent’s judgment JSON and post-validate it against the live graph. Rejects any judgment whose signal_id fallow did not emit (anti-hallucination); refuses the whole payload as stale when the echoed graph-snapshot hash no longer matches (the tree moved). The verifier is the graph, not a second model. The agent’s free-text framing is fenced as non-deterministic and never gates. Implies the brief; always exits 0.
--show-deprioritized
Expand the de-prioritized units in the brief’s weighted focus map (“show me what you de-prioritized”). The deprioritized escape-hatch list is always present in --format json regardless; this flag only re-expands the collapse-by-default human focus render. Only consulted on the brief path.
Maximum CRAP score before the audit fails (default: 30.0, configurable via health.maxCrap). Functions meeting or exceeding this score contribute to the fail verdict alongside dead-code and complexity findings. Pair with --coverage for accurate per-function CRAP; without Istanbul data fallow estimates coverage from the module graph.
--coverage <PATH>
Path to Istanbul-format coverage data (coverage-final.json) for accurate per-function CRAP scores in the health sub-analysis. Also configurable via FALLOW_COVERAGE. Same format and semantics as fallow health --coverage; without it, fallow falls back to the static-estimate model. Relative paths resolve against --root.
--coverage-root <PATH>
Absolute prefix to strip from file paths in coverage data before prepending the project root. Also configurable via FALLOW_COVERAGE_ROOT. Use when coverage was generated under a different checkout root in CI or Docker (e.g., /home/runner/work/myapp on GitHub Actions).
Baseline file produced by fallow dead-code --save-baseline. Dead-code issues present in the baseline are excluded from the verdict.
--health-baseline <PATH>
Baseline file produced by fallow health --save-baseline. Complexity findings present in the baseline are excluded from the verdict.
--dupes-baseline <PATH>
Baseline file produced by fallow dupes --save-baseline. Clone groups present in the baseline are excluded from the verdict.
The global --baseline / --save-baseline flags are rejected on audit (exit 2) because audit runs three analyses with incompatible baseline formats. Use the per-analysis flags above, or configure defaults in .fallowrc.json:
Store committed baselines outside .fallow/, because fallow init adds that directory to .gitignore for machine-local cache.CLI flags override config. Baselines are a no-op if unset; any subset (e.g. dead-code only) is allowed.
No introduced issues in changed files (new-only) or no issues at all (all)
Ship it.
warn
0
Issues found, all warn-severity
CI passes, but consider fixing before they become errors.
fail
1
Error-severity issues found
Fix the reported issues before merging.
error
2
Runtime error (invalid ref, not a git repo, config error)
Check the error message. In JSON format, emits {"error": true, "message": "...", "exit_code": 2}.
Dead code issues follow your rules configuration severity (error/warn/off). Complexity findings above configured thresholds are always errors; thresholds are inherited from your fallow health config (defaults: cyclomatic 20, cognitive 15). Duplication is a warning unless a --threshold is configured. With the default new-only gate, inherited error-severity findings can appear in the report while the audit exits 0.
Inline suppression comments (// fallow-ignore-next-line) work in audit. Findings in changed files are suppressed the same way as in fallow dead-code.
Audit emits three counts that look interchangeable but answer different questions. CI integrations and downstream consumers must gate on the right one:
All findings in changed files (any gate), regardless of severity
No
Per-finding introduced: true | false
Whether each individual finding was introduced by the changeset
No (severity is per-rule, not per-finding)
Rule of thumb for CI gating: branch on verdict == "fail" (or check the exit code, which mirrors the verdict). Counting introduced findings re-introduces the bug command: audit was designed to fix: a project with unused-exports: warn would fail CI on every PR that introduces a warn-tier finding, even though the verdict correctly says warn (“do not fail”). The official GitHub Action and GitLab CI template already do this; third-party wrappers should mirror the same contract.Rule of thumb for AI agents: read verdict first to know whether the run passed or failed, then read attribution for new-vs-inherited counts and walk the per-category finding arrays for actionable details. Use introduced: true to filter to changes the current PR is responsible for.
Resolve base ref: uses --base if provided, then FALLOW_AUDIT_BASE, otherwise auto-detects the git merge-base (fork point) against the branch’s upstream (@{upstream}) or the remote default (origin/HEAD → origin/main → origin/master). Resolving the merge-base, rather than a bare branch name, means a feature branch compares against where it actually forked rather than a possibly-stale local main. Repositories with no origin remote fall back to the local main / master branch. Hard-errors if no base can be determined.
Find changed files: runs git diff --name-only <base>...HEAD (three-dot diff, showing changes since the merge base).
Highest cyclomatic complexity found (null if none)
summary.duplication_clone_groups
integer
Clone groups involving changed files
attribution.*_introduced
integer
Findings whose structural key did not exist at the base ref. With gate=all, these stay 0 because audit skips the base-snapshot attribution pass.
attribution.*_inherited
integer
Findings already present at the base ref. With gate=all, these stay 0 because audit skips the base-snapshot attribution pass.
dead_code.*[].introduced
boolean
Present in audit JSON sub-results when base attribution is available. true means the finding is new relative to the base ref.
The dead_code, complexity, and duplication sub-objects contain full results in the same format as fallow dead-code, fallow health, and fallow dupes respectively. These are omitted when no files changed. When --runtime-coverage is supplied, the complexity sub-object can also include the health report’s runtime_coverage and additive coverage_intelligence blocks. In audit output, individual findings and clone groups include an optional introduced boolean when the base ref comparison is available.
On exit code 2 (runtime error), JSON format emits {"error": true, "message": "...", "exit_code": 2} to stdout instead of the audit envelope.
When base is not specified, the base is the git merge-base against the branch’s upstream or the remote default (origin/main); set FALLOW_AUDIT_BASE in the server env to pin it. Set gate to all for strict mode. The response always includes _meta explanatory metadata (the MCP wrapper enables --explain by default). Returns the same JSON envelope as the CLI.audit creates a temporary git worktree to compare against the base ref. When the current checkout has node_modules, audit links it into the base worktree so tsconfig extends chains into installed packages and path aliases resolve like the working tree. The worktree is removed on normal exit. If the process is force-killed, run git worktree prune to clean up stale .git/worktrees/fallow-audit-base-* entries.See MCP integration for setup instructions.
fallow review is an alias for fallow audit --brief. It produces a deterministic, graph-derived review brief and always exits 0: the verdict is carried informationally, unlike fallow audit, which gates (exits non-zero on a fail verdict). --format is orthogonal to --brief, so the brief renders in human or json like any other audit output.The brief answers “where do I look?” rather than “will CI block this?”. It runs the same analysis as the gating audit and organizes it around four jobs:
Job
What it does
subtract
Dead code, complexity, and duplication for the changed files are reported but kept out of the judgment loop, so the reviewer is not asked to weigh in on mechanical findings.
focus
Changed-file units are ranked by a composite attention score, labeled review-here or not-prioritized, plus a full deprioritized list (re-expand the human render with --show-deprioritized).
structure
The decision surface: the few consequential structural decisions the change embeds.
The decision surface has exactly three shippable categories:
Category
Meaning
coupling-boundary
A new cross-zone dependency edge.
public-api-contract
A new exported public-API surface, or a changed contract consumed by modules outside this diff.
dependency
A new third-party dependency.
Each decision is a framed judgment question anchored to a signal_id fallow deterministically derived from the graph, ranked by consequence (blast radius x reversibility), capped to --max-decisions (default 4, clamped to a 3 to 5 band), and paired with the routed expert (who to ask). A decision may carry previous_signal_id: the signal_id it would have had before a rename of its anchor file, so a review surface can re-attach a prior reviewer comment across a git mv.Alongside the question, each decision carries a tradeoff clause (the named structural sacrifice stated as a fact, never a recommendation, e.g. “Couples app to infra; 4 in-repo modules already depend on this anchor”) and internal_consumer_count (the honest count of in-repo modules outside the diff that already depend on the anchor). The human view shows the question and the trade-off; you read reversibility from the count itself, fallow never labels the decision a one-way or two-way door or tells you which way to choose.The same decision surface is available to agents through the decision_surface MCP tool.
fallow review --format json carries decisions, focus, deltas, impact_closure, partition, and graph_facts. The deprioritized escape-hatch list inside focus is always present in JSON regardless of --show-deprioritized; that flag only re-expands the collapse-by-default human focus render.
The walkthrough guide turns the brief into a strict agent contract. The verifier is the graph, not a second model: every judgment an agent returns is post-validated against the live graph, and the agent’s free-text framing is fenced as non-deterministic and never gates.1. Fetch the guide.
fallow review --base main --walkthrough-guide --format json
Returns digest (brief + decision surface), direction, graph_snapshot_hash, agent_schema, injection_note, emitted_signal_ids, and change_anchors. The digest is built from the graph only (PR prose is never folded in, so it is injection-resistant), and the snapshot hash is pinned into the digest.change_anchors is a per-hunk anchor set: one stable, content-addressed chg: id per changed region, derived from the same diff source the run used. Its id is content-addressed (file path + normalized added text, line numbers excluded) so it survives an edit above the hunk and a whitespace-only change, and it is rename-durable via previous_change_anchor. It lets an agent anchor a judgment about a changed REGION that has no graph finding (a trade-off with no signal_id), so the broader trade-off can still be post-validated rather than hallucinated.2. The agent returns judgment JSON. Each judgment cites a signal_id fallow emitted OR a change_anchor fallow emitted; echo the hash.
{ "graph_snapshot_hash": "<echoed>", "judgments": [ { "signal_id": "<one fallow emitted>", "change_anchor": "<or one fallow emitted chg: id, for a changed region with no finding>", "framing": "<reasoning>", "concern": "<optional>" } ]}
3. Post-validate the judgment against the live graph.
fallow review --base main --walkthrough-file judgment.json --format json
Each judgment is sorted into one of three outcomes:
Outcome
Condition
Shape
accepted (signal)
The signal_id is anchored and the snapshot still matches.
Framing fenced deterministic: false; anchor_kind: "signal" (the strong anchor, a graph finding).
accepted (change)
No signal_id, but the change_anchor is one fallow emitted and the snapshot still matches.
Framing fenced deterministic: false; anchor_kind: "change" (the weaker anchor, a changed region only).
rejected (unanchored)
The signal_id is not one fallow emitted (a hallucinated id).
reason: "unanchored-signal-id".
rejected (unknown change anchor)
The change_anchor is not one fallow emitted (a hallucinated region).
reason: "unknown-change-anchor".
rejected (stale)
The echoed graph-snapshot hash no longer matches (the tree moved).
reason: "stale-snapshot", stale: true. Re-fetch the guide.
Both --walkthrough-guide and --walkthrough-file imply the brief and always exit 0.