Vite plugin fixing HMR when the project root lives under a hardcoded-ignored path like
.git/worktree/feature-*.
Run any of these and HMR silently breaks — save a .tsx, nothing happens, you're forced to full-reload:
- You use
git worktree add .git/worktree/feature-x ...to keep worktrees inside.git/so your filesystem stays clean. - You run a parallel AI coding agent (Claude Code, dmux,
git-worktree-manager, etc.) that places worktrees under.git/worktree/by convention. - Your source files happen to live under a path segment named
.gitfor any other reason.
Vite 6+ hardcodes **/.git/** into its chokidar ignored option (source). chokidar's ignored is OR-semantic — if any pattern matches, the file is filtered. Setting server.watch.ignored in vite.config.ts cannot remove the hardcoded default. The documented '!**/path/**' negation workaround does not produce working HMR (evidence: vitejs/vite#21045).
Vite maintainers consider the hardcoded ignore "expected behavior." Related upstream issues have been open for years (e.g., #8619 since 2022).
Runs a sidecar chokidar watcher with cwd: root, so relative ignore patterns are cwd-normalized. The sidecar sees files the built-in watcher filters out, and forwards events to server.watcher.emit(...) — feeding Vite's existing HMR pipeline without patching any Vite internals.
┌──────────── vite dev server ────────────┐
│ │
│ server.watcher ← .git/** hardcoded ignore│
│ ▲ │
│ │ emit('change' | 'add' | ...) │
│ │
│ ┌────────────────────────────────┐ │
│ │ Sidecar chokidar (cwd: root) │ │
│ │ — not fooled by .git/ prefix │ │
│ └────────────────────────────────┘ │
└──────────────────────────────────────────┘
# pnpm
pnpm add -D @lambda-script/vite-plugin-worktree-hmr
# npm
npm install --save-dev @lambda-script/vite-plugin-worktree-hmr
# yarn
yarn add -D @lambda-script/vite-plugin-worktree-hmr// vite.config.ts
import { defineConfig } from 'vite'
import worktreeHmr from '@lambda-script/vite-plugin-worktree-hmr'
export default defineConfig({
plugins: [worktreeHmr()],
})That's it. The plugin is a no-op unless your Vite root contains a .git segment.
On dev-server startup you will see one of:
[vite-plugin-worktree-hmr] enabled (root is inside .git/, using sidecar chokidar watcher)[vite-plugin-worktree-hmr] skipped (root is not inside .git/)[vite-plugin-worktree-hmr] disabled by DISABLE_WORKTREE_HMR_FIX=1
| Variable | Effect |
|---|---|
DISABLE_WORKTREE_HMR_FIX=1 |
Disable the plugin entirely (escape hatch) |
WORKTREE_HMR_POLLING=1 |
Force polling on the sidecar (for Docker / NFS / network filesystems where FSEvents or inotify are unavailable) |
The sidecar also inherits server.watch.usePolling / server.watch.interval from your Vite config if set, so most Docker users already get polling automatically.
- Vite: 5, 6, 7, 8 (declared as peer dependency)
- Node: 20+
- chokidar: 3 or 4 (auto-resolved via peer/runtime deps)
- OS: macOS, Linux. Windows/WSL should work (path check is slash-agnostic) but is not continuously verified — please file an issue if you hit problems.
- No Vite internal patching. The only integration point is
server.watcher.emit(...), which is a standard Node EventEmitter API. Works across Vite major versions. - No duplicate HMR firing. Vite's built-in watcher still runs but ignores
.git/**paths, so it emits nothing for worktree files. The sidecar is the sole source of events for these files. - Idempotent shutdown. Wired to both
httpServer 'close'andserver.closeso normal Ctrl+C andserver.restart()both clean up the sidecar. - Errors never crash the dev server. All
emitandclosecalls are wrapped in try/catch and logged vialogger.error.
MIT © LambdaScript