vite-plugin-agent-quiet
v1.0.0
Published
Pause Vite's HMR during AI-agent edit bursts. Coalesce many edits into one reload.
Maintainers
Readme
vite-plugin-agent-quiet
Pause Vite's HMR during AI-agent edit bursts. Coalesce many edits into one reload.
Status: v1. Plugin is small (~110 LOC core) and stable. Claude Code hooks are tested; Cursor and Aider configs are community-contributed placeholders.
The problem
AI coding agents (Claude Code, Cursor, Aider, Codex, Continue) edit codebases in bursts — often 5 to 20 writes within a few seconds. Vite's HMR was designed for a human cadence: one save, an incremental update, a stable intermediate state. When edits arrive faster than HMR can settle, the dev server runs overlapping cycles against a temporarily inconsistent dependency graph.
Concretely, here's a 4-file 2-second agent burst on a vanilla Vite setup:
t=0.0s Edit Button.tsx → HMR cycle 1
t=0.4s Edit Modal.tsx → HMR cycle 2
t=0.9s Edit store.ts → HMR cycle 3 (full reload — store edit)
t=1.3s Edit hooks.ts → HMR cycle 4
↳ flashing errors, lost component state, wasted rebuild workFour reload cycles for one logical change. The middle cycles fire while the graph is half-rewritten — the kind of state a human editor never produces, so HMR has no special handling for it.
The solution
A filesystem sentinel acts as a mutex between the agent and Vite. The agent creates .agent-editing before its first write and removes it after its last. The plugin watches that file and pauses HMR while it exists.
The same burst with the plugin installed:
t=0.0s sentinel up, edits 1–4 swallowed and queued
t=2.0s sentinel down → 1 full-reload, queue drainedOne reload, applied to a stable post-burst snapshot. No flashing, no half-state.
The mechanism is small on purpose. The plugin watches for one file, polls every 250ms, and emits exactly one full-reload when the file disappears. Stale sentinels are auto-recovered after a configurable timeout.
Install
yarn add -D vite-plugin-agent-quiet// vite.config.ts
import { defineConfig } from 'vite';
import agentQuiet from 'vite-plugin-agent-quiet';
export default defineConfig({
plugins: [agentQuiet()],
});That's the whole plugin install. The agent side needs hooks — see the next section.
Minimal setup
Plugin only
The snippet above. The plugin is a no-op in build mode (apply: 'serve'), so it ships zero overhead to production.
With Claude Code
Drop the examples/claude-code/.claude/settings.json into your project root and add .agent-editing to .gitignore. The example's README walks through verification.
Agent hook examples
| Agent | Example | Status |
|---|---|---|
| Claude Code | examples/claude-code/ | Tested. Three-hook config covers PreToolUse, Stop, SubagentStop. |
| Cursor | examples/cursor/ | Placeholder. PRs welcome. |
| Aider | examples/aider/ | Placeholder. PRs welcome. |
| Custom / shell wrapper | examples/manual/ | Tested pattern with trap ... EXIT for signal-safe cleanup. |
The plugin itself is agent-agnostic. As long as something creates the sentinel before the burst and removes it after, the plugin works.
How it works
The plugin maintains a small state machine around one filesystem primitive: the sentinel file.
- Agent hooks create
.agent-editingbefore the burst (touch .agent-editing) and remove it after (rm -f .agent-editing). handleHotUpdatesees the sentinel and returns[], which tells Vite to skip HMR for that file. The changed path is added to an in-memorySetso we don't double-count repeated edits to the same file.- A 250ms poller watches for the sentinel's removal. When it disappears, the plugin emits exactly one
{ type: 'full-reload' }over the dev server's WebSocket and clears the queue. - Staleness is tracked via
mtime. If the sentinel sits longer thanstaleMs(default 60s) without being touched, it's treated as abandoned: the plugin unlinks it and flushes whatever was queued. This is the recovery path for crashed or interrupted agents.
The whole thing is about 110 lines. See src/index.ts if you want the full picture.
Options
| Option | Default | Description |
|---|---|---|
| sentinel | .agent-editing | Sentinel filename, resolved relative to Vite root. |
| staleMs | 60_000 | Treat the sentinel as dead after this many ms without modification. |
| log | true | Print one line per resume: [agent-quiet] coalesced N edits → 1 reload. |
agentQuiet({
sentinel: '.my-agent.lock',
staleMs: 30_000,
log: false,
});FAQ
Why a file instead of an IPC socket or in-process API?
A sentinel file is agent-agnostic and has zero coupling. Any agent or shell script that can run touch and rm works — no plugin-specific protocol, no version-matching, no language bindings. Sockets and named pipes would lock the plugin to one ecosystem.
Why not just use server.watch.ignored?
That ignores files permanently. We need dynamic pause/resume — HMR works normally while you edit by hand, and only pauses during agent bursts.
What if my agent crashes mid-edit?
The staleMs timeout (default 60s) takes over: the next poll tick unlinks the abandoned sentinel and flushes any queued edits with one full-reload.
Does this affect production builds?
No. The plugin is apply: 'serve', so it isn't even loaded during vite build.
Does it work with Next.js / Remix / SvelteKit?
Only with Vite-based setups currently. Next.js uses Turbopack/Webpack; Remix has both Vite and non-Vite variants. The plugin hooks into Vite's handleHotUpdate, so anything not built on Vite is out of scope.
Will this slow down normal HMR when no agent is editing?
The only added work is one statSync call per HMR event. Sub-millisecond. The poller is a 250ms setInterval that does one statSync per tick.
What about my framework's HMR (Vue / Svelte / React Fast Refresh)?
Works fine. The plugin intercepts at Vite's level, before framework-specific HMR runs. Framework HMR happens downstream of handleHotUpdate, so when the plugin passes through, the framework's machinery is untouched.
What about monorepos?
The sentinel is resolved relative to each Vite root, so if you run multiple Vite servers in one repo each gets its own sentinel — but the agent hooks touch only one filename. Either set sentinel per-package (e.g. .agent-editing-web, .agent-editing-admin) and use multiple hook entries, or accept that one touch pauses all of them simultaneously. The second option is usually fine and is what we run.
Benchmarks
Workload: 8 file edits over 1.1 seconds (mimicking an agent burst), then a 2-second settle window. Cycles are counted by spying on server.ws.send for update and full-reload payloads.
| | Without plugin | With plugin | |---|---|---| | HMR cycles | 8 | 1 |
One reload instead of eight, for the same set of edits. Run it yourself with yarn benchmark — the script is at benchmark/hmr-cycles.ts. Measured on Node 23, macOS, Vite 6.4. Numbers will vary by host.
Comparison
| Approach | Coalesces edits | Auto-recovers | Works during agent turn | Setup |
|---|---|---|---|---|
| vite-plugin-agent-quiet | ✅ | ✅ | ✅ | One plugin + agent hooks |
| server.watch.ignored | ❌ permanently | n/a | ❌ | Vite config edit |
| server.hmr: false | ❌ entirely | n/a | ❌ | Vite config edit |
| Do nothing | ❌ | n/a | ❌ | n/a |
Contributing
Bug reports and PRs are welcome. The Cursor and Aider example folders are placeholders — if you use those tools and get a config working, please open a PR.
- Commits follow Conventional Commits (
feat:,fix:,docs:, etc.). - Run
yarn testbefore pushing. Unit + integration tests should be green. - For non-trivial changes, open an issue first to align on scope before writing code.
