FsHotWatch 0.10.0-alpha.3

This is a prerelease version of FsHotWatch.
dotnet add package FsHotWatch --version 0.10.0-alpha.3
                    
NuGet\Install-Package FsHotWatch -Version 0.10.0-alpha.3
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="FsHotWatch" Version="0.10.0-alpha.3" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="FsHotWatch" Version="0.10.0-alpha.3" />
                    
Directory.Packages.props
<PackageReference Include="FsHotWatch" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add FsHotWatch --version 0.10.0-alpha.3
                    
#r "nuget: FsHotWatch, 0.10.0-alpha.3"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package FsHotWatch@0.10.0-alpha.3
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=FsHotWatch&version=0.10.0-alpha.3&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=FsHotWatch&version=0.10.0-alpha.3&prerelease
                    
Install as a Cake Tool

FsHotWatch

Trying to speed up the F# development feedback loop.

FsHotWatch is a background daemon that watches your source files and aims to keep the F# compiler warm, so saving a file re-checks just what changed and hands the results to your tools (linters, analyzers, test runners) — instead of each tool restarting the compiler from scratch every time.

Status: early alpha, and a lot of it is AI-written. It runs the author's own daily F# work, but behavior and APIs shift between versions, rough edges are expected, and your mileage may vary. The goal is a faster F# loop — it's still finding its shape, so issues and PRs are very welcome.

The problem

F# tools are slow because each one starts its own compiler from zero. A 15-project solution can take ~2 minutes to analyze. Every save restarts your linter, your analyzer, and your test runner — all re-parsing and type-checking the same hundreds of files again.

How it works

FsHotWatch runs one compiler in the background and shares it with all your tools:

  1. You save a file — FsHotWatch notices.
  2. It re-checks just that file using the already-warm compiler — ideally milliseconds rather than minutes.
  3. Plugins get the results instantly — your linter, analyzer, and test runner see the new check results without re-parsing.
  4. You query the resultsfshw check runs every plugin and reports what each one found.

Saves are debounced: if 10 files change at once (a formatter sweeping the repo, say), FsHotWatch waits for things to settle and processes them in one batch.

Quick start

# Install the CLI
dotnet tool install -g FsHotWatch.Cli

# The inner loop. Auto-starts the daemon the first time —
# no separate "start" step needed. Verbose by default.
fshw check

# Prefer one line per plugin?
fshw check --compact   # or -q

# Before you merge: run the full suite and confirm `check` told the truth.
fshw confirm

fshw init writes a starter .fshw.json; see Configuration.

check, confirm, verdict

Three verbs, and the difference between them is the difference between a fast answer and a trustworthy one.

fshw check is the inner loop, and it is not a merge decision. It runs every plugin (build, lint, analyze, test, format-check), but the tests are impact-filtered — it runs the tests a heuristic selector thinks your change can affect. That is a latency optimization, and it is the right trade for the loop you run on every save.

fshw confirm is the merge verb. It runs the same checks with the tests unfiltered, and it refuses to go green unless they actually ran that way — "nothing failed" is not a verdict if the run never produced the evidence (exit 3). Running it beside check is a comparison, and every disagreement is a bug in one of them:

What you see What it means
Failed under confirm, never selected by check The selector MISSED a test. An impact-analysis bug, not a test bug.
Passed under confirm, but check says red A stale ledger, a flake, or a test-isolation defect — a test that only passes with company. Here check is the honest one.

fshw verdict reads the answer back, from a file, without contacting the daemon — see Machine-readable state.

What these verbs do not claim

This matters more than the feature list, so it is stated up front:

  • "The full suite" means every test project your .fshw.json knows about — not every test project in your solution. A project that is in the solution but absent from .fshw.json is not run by confirm, and confirm does not claim otherwise.
  • Impact selection is still known-unsound. confirm makes the merge verdict unforgeable — a green must be backed by an unfiltered run. It does not make the selector sound: check can still miss tests it should have picked. Those are two different claims and only the first one is fixed.
  • A warm cache makes confirm exit 3, not 0. Re-run it on an unchanged tree and the test plugin replays its cached result rather than running; a replay is not evidence, so confirm refuses (exit 3) instead of greening. Clear the cache (rm -rf .fshw/cache) or change a file. It fails safe, but it is a rough edge.

Commands

Command What it does
fshw check The inner loop. Run every plugin and report findings; tests are impact-filtered. Auto-starts the daemon. Exits 0 (clean), 1 (failures), 2 (completeness unconfirmed). --run-once runs without a daemon; -q/--compact for one line per plugin.
fshw confirm The merge verb. Same checks, but the tests run unfiltered — and a green is refused unless they did. Exits 0/1/2 as check, plus 3 (unearned scope). --run-once for CI.
fshw verdict Read .fshw/verdict.json and report whether it still applies to the tree on disk. Contacts no daemon, triggers no run.
fshw status [plugin] Show the daemon's current status (optionally for one plugin). Triggers nothing.
fshw start Run the daemon in the foreground (Ctrl+C to stop). Optional — check/status start it for you.
fshw stop Stop the running daemon.
fshw format Format the code (Fantomas).
fshw test-rerun Rerun tests for an xUnit v3 --filter-class / --filter-trait slice.
fshw rerun <plugin> Force one plugin to re-run, clearing its cached state.
fshw init Generate a starter .fshw.json.
fshw config check Validate .fshw.json without starting the daemon.

Add -v for debug logging or -a for agent-friendly, parseable output. Run fshw --help for the full list, and see the CLI README for every verb and flag.

Machine-readable state (for agents and CI)

Don't parse the CLI's output. It is a progress display written for a human and it will change. Every check and confirm publishes its result as a file:

fshw verdict          # stdout: a JSON envelope; exit code: the answer
# 0 green · 1 red · 2 incomplete · 3 unearned scope · 4 STALE · 5 no verdict

.fshw/verdict.json is written atomically at the end of every run — including the ones that fail, time out, or lose the daemon mid-run, which are exactly the moments the human output is least sufficient. It is content-addressed to the tree it verified and the binary that verified it, so a green from a different tree (or from an older, buggier fshw) can never be mistaken for a current one. Reading it opens no socket and starts nothing, so asking cannot perturb the answer.

This — not the progress display — is the surface agents and CI should read. Full schema, exit codes, and the tree-hash recipe: CLI README and ADR-013.

Packages

FsHotWatch is split into small packages so you install only what you need:

Package What it does
FsHotWatch Core library — the daemon, file watcher, plugin system, IPC
FsHotWatch.Cli CLI tool — fshw check, start, stop, status, …
FsHotWatch.TestPrune Plugin: figures out which tests to run when code changes
FsHotWatch.Analyzers Plugin: runs F# analyzers (like G-Research or your own)
FsHotWatch.Lint Plugin: runs FSharpLint using the warm compiler's results
FsHotWatch.Fantomas Plugin: checks if your files are formatted with Fantomas
FsHotWatch.Build Plugin: runs dotnet build and emits BuildCompleted events
FsHotWatch.FileCommand Plugin: runs custom commands when specific files change
FsHotWatch.Coverage Plugin: checks per-file line/branch coverage thresholds after each test run

Configuration

Run fshw init to scaffold a .fshw.json in your repo root, or write one by hand. Every field is optional — sensible defaults apply when omitted.

{
  "build": {
    "command": "dotnet",
    "args": "build"
  },
  "format": true,
  "lint": true,
  "cache": "file",
  "tests": {
    "beforeRun": "dotnet build",
    "dependsOn": ["src/MyApp/Database/Migrations/**"],
    "projects": [
      {
        "project": "MyProject.Tests",
        "command": "dotnet",
        "args": "run --project tests/MyProject.Tests --no-build --",
        "filterTemplate": "--filter-class {classes}",
        "classJoin": " ",
        "group": "unit"
      }
    ]
  },
  "analyzers": {
    "paths": ["analyzers/"]
  },
  "fileCommands": [
    {
      "pattern": "*.fsx",
      "command": "dotnet",
      "args": "fsi --typecheck-only"
    }
  ],
  "coverage": {
    "configPath": "coverage-ratchet.json",
    "searchDir": "coverage"
  }
}

Reference

Field Type Default Description
build object \| bool {"command": "dotnet", "args": "build"} Build command. false disables.
format bool true Enable Fantomas format-on-save preprocessor.
lint bool true Enable FSharpLint plugin. Uses fsharplint.json if found.
cache string \| bool "file" Cache strategy: "none", "memory", or "file". ("jj" is a legacy alias for "file".)
tests object Test runner config. See below.
coverage object Coverage threshold checking.
analyzers object F# Analyzers SDK integration.
fileCommands array [] Custom commands triggered by file patterns.
exclude string[] [] Gitignore-style globs (repo-root-relative) for paths to skip entirely — watching, building, checking. (obj/ + bin/ are always skipped, independent of this.)
includeOutsideRepo bool false Report on compile items that resolve outside the repo root — e.g. NuGet-injected _content source (xunit's DefaultRunnerReporters.fs), or files above/beside the repo. Default false: the report-producing plugins (analyzers, lint) skip such third-party source — it's compiled into your project, but not yours to lint, and a latent analyzer-crash surface (AUTOMATION-49). Set true to lint them anyway.

For memory/idle-exit, FSEvents latency, and per-task timeout keys, see Memory & tuning.

build fields:

Field Type Default Description
command string "dotnet" Build command.
args string "build" Arguments to the build command.
buildTemplate string Template for incremental builds. {projects} is replaced with changed project paths.

tests fields:

Field Type Default Description
beforeRun string Command to run before each test run (e.g. "dotnet build").
dependsOn string[] [] Repo-root-relative globs (*, ?, **) naming external test inputs the symbol-diff can't see — DB migrations, generated files, schemas. Their content hash salts the test cache key, so editing a matched file forces a real test re-run even when no test source changed.
coverageDir string "coverage" Directory (repo-root-relative) for per-project Cobertura artifacts.
projects array [] List of test project configurations.

tests.projects[] fields:

Field Type Default Description
project string "unknown" Project name (used for filtering and display).
command string "dotnet" Test runner command.
args string "test --project <project>" Arguments to the test runner.
group string "default" Group name (for running subsets).
environment object {} Extra environment variables as "KEY": "VALUE" pairs.
filterTemplate string Template for class-based filtering. {classes} is replaced with affected test class names.
classJoin string " " Separator for joining class names in the filter.
reportVerificationFormat string "auto" How the pass/fail verdict's structured test report is obtained. The report (not the process exit code) decides green/red. auto injects --report-ctrf only for a runner detected as xUnit.v3 (else falls back to the dotnet heuristic); ctrf always injects it; off never injects it and the exit code stays authoritative.

analyzers fields:

Field Type Default Description
paths string[] Directories containing analyzer DLLs. Relative paths resolved from repo root.

fileCommands[] fields:

Field Type Default Description
pattern string "*.fsx" File extension pattern to match (e.g. "*.fsx", "*.sql").
command string "echo" Command to run when a matching file changes.
args string "" Arguments to the command.

coverage fields:

Field Type Default Description
configPath string "coverage-ratchet.json" Path to the coverage-ratchet thresholds file (relative to repo root or absolute).
searchDir string "." Directory tree to search for coverage.cobertura.xml files after each test run.

Cache directory

FsHotWatch keeps its check-result cache and the TestPrune database in .fshw/ at the repository root. Add it to your .gitignore:

.fshw/

What it does and doesn't cache. The on-disk cache stores FCS check results, keyed by file content — so a fresh daemon can replay unchanged files instead of re-checking them. It does not persist the compiler's in-memory warmth: FSharpChecker and its FCS caches are rebuilt from cold on every daemon start, so the first scan after a (re)start still pays that warm-up before the cached results start landing.

Writing plugins

Plugins are declarative update functions over a shared warm compiler: you define how your state reacts to events (file checked, build completed, tests finished), and the framework manages the agent, status, caching, and IPC. See Writing a plugin.

Memory & tuning

The daemon keeps the F# compiler warm, which costs memory. FsHotWatch ships with conservative defaults (aggressive GC, optional idle-exit) so this stays in check — see Memory & tuning if you want to adjust them.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (7)

Showing the top 5 NuGet packages that depend on FsHotWatch:

Package Downloads
FsHotWatch.TestPrune

FsHotWatch plugin for TestPrune test impact analysis

FsHotWatch.Analyzers

FsHotWatch plugin for F# Analyzers SDK integration

FsHotWatch.Build

FsHotWatch plugin that runs dotnet build and emits BuildCompleted events

FsHotWatch.Lint

FsHotWatch plugin for FSharpLint integration

FsHotWatch.Fantomas

FsHotWatch plugin for Fantomas format checking

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.10.0-alpha.3 45 7/15/2026
0.10.0-alpha.2 60 7/8/2026
0.10.0-alpha.1 69 7/5/2026
0.9.0-alpha.1 73 7/3/2026
0.8.0-alpha.34 61 7/2/2026
0.8.0-alpha.33 93 6/24/2026
0.8.0-alpha.32 75 6/18/2026
0.8.0-alpha.31 106 6/17/2026
0.8.0-alpha.30 131 6/16/2026
0.8.0-alpha.29 72 6/15/2026
0.8.0-alpha.28 71 6/12/2026
0.8.0-alpha.27 75 6/11/2026
0.8.0-alpha.26 67 6/10/2026
0.8.0-alpha.25 65 6/10/2026
0.8.0-alpha.24 93 6/8/2026
0.8.0-alpha.23 65 6/7/2026
0.8.0-alpha.22 67 6/7/2026
0.8.0-alpha.21 63 6/6/2026
0.8.0-alpha.20 64 6/5/2026
0.8.0-alpha.19 65 6/4/2026
Loading failed