FsHotWatch 0.8.0-alpha.13
dotnet add package FsHotWatch --version 0.8.0-alpha.13
NuGet\Install-Package FsHotWatch -Version 0.8.0-alpha.13
<PackageReference Include="FsHotWatch" Version="0.8.0-alpha.13" />
<PackageVersion Include="FsHotWatch" Version="0.8.0-alpha.13" />
<PackageReference Include="FsHotWatch" />
paket add FsHotWatch --version 0.8.0-alpha.13
#r "nuget: FsHotWatch, 0.8.0-alpha.13"
#:package FsHotWatch@0.8.0-alpha.13
#addin nuget:?package=FsHotWatch&version=0.8.0-alpha.13&prerelease
#tool nuget:?package=FsHotWatch&version=0.8.0-alpha.13&prerelease
FsHotWatch
Speed up your F# development feedback loop.
FsHotWatch is a background daemon that watches your source files and keeps the F# compiler warm. When you save a file, it instantly re-checks it and tells your tools (linters, analyzers, test runners) what changed — without restarting the compiler from scratch each time.
The problem
F# tools are slow because they each start their own compiler from zero. A 15-project solution takes ~2 minutes to analyze. Every time you save a file, your linter restarts, your analyzer restarts, your test runner restarts — all parsing and type-checking the same 500 files again.
How FsHotWatch helps
FsHotWatch runs one compiler in the background and shares it with all your tools:
- You save a file — FsHotWatch notices the change
- It re-checks just that file using the compiler that's already warm (milliseconds, not minutes)
- Plugins get the results instantly — your linter, analyzer, and test runner all see the updated check results without re-parsing anything
- You query the results —
fshw statusshows what each tool found
Changes are debounced — if you save 10 files in quick succession (like a formatter running), FsHotWatch waits for things to settle, then processes them all in one batch.
Quick start
# Install the CLI tool
dotnet tool install -g FsHotWatch.Cli
# Start the daemon in your repo (runs in foreground, Ctrl+C to stop)
fshw start
# From another terminal, check what's happening
fshw status
# Run all checks; verbose by default (per-subtask progress + last-run recap)
fshw check
# Prefer one line per plugin?
fshw check --compact # or -q
Packages
FsHotWatch is split into small packages so you only install what you need:
| Package | What it does |
|---|---|
FsHotWatch |
Core library — the daemon, file watcher, plugin system, IPC |
FsHotWatch.Cli |
CLI tool — fshw 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 |
Writing your own plugin
Plugins use a declarative framework: you define an update function that receives events and returns new state. The framework manages the agent, status tracking, and IPC command registration.
open FsHotWatch.Events
open FsHotWatch.PluginFramework
type MyState = { FilesChecked: int }
let myPlugin: PluginHandler<MyState, unit> =
{ Name = "my-plugin"
Init = { FilesChecked = 0 }
Update =
fun ctx state event ->
async {
match event with
| FileChecked result ->
// result.ParseResults and result.CheckResults come from
// the warm FSharpChecker — no re-parsing needed.
printfn "Checked: %s" result.File
ctx.ReportStatus(Completed(System.DateTime.UtcNow))
return { FilesChecked = state.FilesChecked + 1 }
| _ -> return state
}
Commands =
[ "my-status",
fun state _args ->
async { return $"checked %d{state.FilesChecked} files" } ]
Subscriptions =
{ PluginSubscriptions.none with
FileChecked = true }
CacheKey = None }
// Register with the daemon:
daemon.RegisterHandler(myPlugin)
Available events (subscribe via Subscriptions):
FileChanged— a source file was saved (you get the file paths)BuildCompleted—dotnet buildfinished (success or failure)FileChecked— a file was type-checked (you get parse + check results)TestCompleted— tests finished running (you get per-project results)
What you can do in Update via ctx:
ctx.Checker— the warm FSharpChecker (reuse it for your own analysis)ctx.RepoRoot— path to the repository rootctx.ReportStatus(status)— tell the daemon your current statusctx.ReportErrors(file, entries)— report diagnostics to the error ledgerctx.EmitBuildCompleted(result)— emit events to other pluginsctx.Post(msg)— send a custom message back to your own agentctx.StartSubtask(key, label)/ctx.EndSubtask(key)— surface named concurrent work with live per-subtask elapsed infshw checkoutputctx.Log(msg)— preferred logging path; appends to the activity tail shown under your plugin incheck, and also routes toLogging.infoctx.CompleteWithSummary(summary)— override the auto-derived summary captured in run history on the next terminal transition (e.g. "built 4 projects")
Status transitions are fully observed: when a plugin moves to Completed or
Failed, the host snapshots current subtasks + activity into a bounded run
history (per plugin), visible under the check verbose output as
started / elapsed / summary on the next run.
Configuration
Create .fshw.json in your repo root. All fields are optional — sensible defaults are used when omitted.
{
"build": {
"command": "dotnet",
"args": "build"
},
"format": true,
"lint": true,
"cache": "file",
"tests": {
"beforeRun": "dotnet build",
"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"
}
]
}
Configuration 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 accepted as 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. |
timeoutSec |
int |
— | Global default per-task timeout in seconds. Used when a plugin/project has no per-entry override. |
Per-task timeouts. Any of build[], tests.projects[], and fileCommands[] entries may set their own timeoutSec to override the global default. When a task exceeds its timeout, the daemon kills the child process tree, records the run with outcome timed out (distinct ⏱ glyph in the UI, timed-out token in agent mode), and stays running — the next change retriggers normally.
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"). |
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. |
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. |
Cache directory
FsHotWatch stores check result caches and the TestPrune database in .fshw/ at the repository root. Add this to your .gitignore:
.fshw/
The cache directory contains:
cache/— Cached FCS check results for faster cold startstest-impact.db— TestPrune dependency analysis database
| Product | Versions 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. |
-
net10.0
- FSharp.Compiler.Service (>= 43.12.203)
- FSharp.Core (>= 10.1.203)
- FSharp.Data.Adaptive (>= 1.2.26)
- Ignore (>= 0.2.1)
- Ionide.ProjInfo (>= 0.74.2)
- Ionide.ProjInfo.FCS (>= 0.74.2)
- StreamJsonRpc (>= 2.24.84)
- System.Security.Cryptography.Xml (>= 10.0.7)
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.Build
FsHotWatch plugin that runs dotnet build and emits BuildCompleted events |
|
|
FsHotWatch.Analyzers
FsHotWatch plugin for F# Analyzers SDK integration |
|
|
FsHotWatch.Fantomas
FsHotWatch plugin for Fantomas format checking |
|
|
FsHotWatch.Lint
FsHotWatch plugin for FSharpLint integration |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.8.0-alpha.13 | 69 | 5/4/2026 |
| 0.8.0-alpha.12 | 68 | 4/29/2026 |
| 0.8.0-alpha.11 | 75 | 4/26/2026 |
| 0.8.0-alpha.10 | 66 | 4/25/2026 |
| 0.8.0-alpha.9 | 62 | 4/23/2026 |
| 0.8.0-alpha.8 | 56 | 4/22/2026 |
| 0.8.0-alpha.7 | 56 | 4/20/2026 |
| 0.8.0-alpha.6 | 68 | 4/20/2026 |
| 0.8.0-alpha.3 | 67 | 4/18/2026 |
| 0.8.0-alpha.2 | 63 | 4/15/2026 |
| 0.6.0-alpha.1 | 63 | 4/12/2026 |
| 0.5.0-alpha.1 | 70 | 4/12/2026 |
| 0.3.0-alpha.1 | 63 | 4/8/2026 |
| 0.2.0-alpha.1 | 59 | 4/8/2026 |
| 0.1.0-alpha.1 | 78 | 4/3/2026 |