Skip to content

fix: never run a repository's core.fsmonitor on git index reads - #409

Merged
dean0x merged 4 commits into
mainfrom
fix/fsmonitor-ls-files-guard
Sep 30, 2026
Merged

dean0x merged 4 commits into
mainfrom
fix/fsmonitor-ls-files-guard

Conversation

@dean0x

@dean0x dean0x commented Sep 30, 2026

Copy link
Copy Markdown
Owner

Summary

Git runs the command named by a repository's core.fsmonitor config on every index read — ls-files, status, diff — even for "read-only" commands. Devflow's hooks and HUD run inside arbitrary user repositories, so a crafted repo config (e.g. a repo shared as a tarball/zip including .git, or on a shared filesystem) could execute code the moment devflow reads the index. This PR guards every shipped index read with -c core.fsmonitor=false.

Changes

  • Guards seven previously-unguarded call sites with -c core.fsmonitor=false:
    • src/assets/scripts/hooks/json-helper.cjs (listGitTrackedFiles / assign-anchor collision check)
    • pre-compact-memory (status, diff)
    • background-memory-update (status, diff)
    • src/hud/git.ts (the gitExec wrapper)
  • Adds design decision tag D-NO-FSMONITOR, also retroactively applied to the two already-guarded sites (resolve-settings.cjs, verify-evidence.cjs)
  • HUD carve-out: the HUD reads git config --type=bool --get core.fsmonitor once per refresh (not an index read, never runs the hook). Only an exact true (git's built-in daemon — needed to keep status/diff fast in huge repos under the HUD's 1s timeout) skips the override on status/diff. A hook path, false, unset, or a read failure keeps the override (fail closed).
  • Fixes a pre-existing HUD bug: .trim() on git status --porcelain stripped the first line's leading space, so a lone unstaged edit to a tracked file read as staged and the tree as clean. Status now trims trailing whitespace only.
  • Adds a static guard test (tests/guards/no-fsmonitor-index-read.test.ts) that fails on any future unguarded shipped git index read.

Breaking Changes

None.

Testing

  • Behavioural fsmonitor tests (sentinel hook never runs — each proves the hook is live without the flag): tests/decisions/ledger-ops.test.ts, tests/memory-hooks-fsmonitor.test.ts (new), tests/integration/hud-git.test.ts
  • Argv-level carve-out tests: tests/hud-git-fsmonitor.test.ts (new)
  • Static guard: tests/guards/no-fsmonitor-index-read.test.ts (new)
  • Locally passing: build, tsc --noEmit, typecheck:scripts, tests/guards (272), HUD unit (156), ledger-ops + memory-hooks-fsmonitor (114), hud-git integration (30)
  • Not run locally: full npm test suite (CI covers it); Snyk (local MCP broken)
  • Not verified: a real running built-in fsmonitor daemon (argv-level only)

Related Issues

Closes #408

git runs the command a repository's config names in core.fsmonitor
whenever it reads the index - ls-files, status and diff included. The
hooks and the HUD run inside arbitrary repositories, so each unguarded
index read executed code the repository chose.

Every remaining index read now passes -c core.fsmonitor=false
(D-NO-FSMONITOR), matching resolve-settings and verify-evidence:
- json-helper listGitTrackedFiles (assign-anchor collision scan)
- pre-compact-memory git status / git diff
- background-memory-update git status / git diff
- the HUD's gitExec wrapper (status and diff on every prompt)

Regression tests arm a real repository with a recording fsmonitor hook
and assert it never runs while the reported git state stays correct,
with a known-bad unguarded probe proving the hook is live.
A static guard over src/assets/scripts and src/**: each git call spelled
with an index-reading subcommand (shell, command string, argv literal or
git-named wrapper) must carry core.fsmonitor=false before the
subcommand, or go through a wrapper that prepends it. Seeded probes pin
every spelling red and prose, guarded calls and non-index subcommands
green. Against the pre-fix tree it reports exactly the seven sites the
previous commit guarded.
The HUD passed -c core.fsmonitor=false on every call, which also turned
off git's built-in fsmonitor daemon. That daemon is git's own code, not
a command from the repository, and it is what keeps status fast in huge
repositories, where the HUD's 1s timeout would otherwise expire and draw
a clean tree.

Each refresh now reads core.fsmonitor once with
git config --type=bool --get (no index read, never cached). Only the
literal answer true lets status and diff run without the override. A
hook path (refused by --type=bool), any other value, an unset key and a
failed read keep it (fail closed). Every other HUD call keeps the
override unconditionally.

The static guard honours the conditional wrapper only in src/hud/git.ts,
only while that file defines fsmonitorOverride itself, and runs the
classifier to require it to fail closed for every answer but true.
shellExec trimmed every call's whole stdout, which also stripped the
leading blank of the first `git status --porcelain` line. An unstaged
edit prints ` M path`; trimmed, `M` moved into the index column, so a
tree whose only change was an unstaged edit to a tracked file read as
staged and clean.

shellExec now takes a trim mode: whole-output trim stays the default for
single values (refs, counts, the config answer), and the status call
trims trailing whitespace only.
@dean0x
dean0x merged commit 62c0b0c into main Sep 30, 2026
3 checks passed
@dean0x
dean0x deleted the fix/fsmonitor-ls-files-guard branch September 30, 2026 20:56
dean0x added a commit that referenced this pull request Sep 30, 2026
The fsmonitor guard and HUD status-trim fix landed in #409 without an [Unreleased] entry; add them before the 3.0.0 release.
dean0x added a commit that referenced this pull request Sep 30, 2026
Add a changelog-coverage pre-release check (#409 shipped without an
[Unreleased] entry) and replace the stale compliance-gated post-release
note: evidence extras now follow EVIDENCE_POLICY=required and are
appended to the CI-created release. Verify npm via dist-tags, which
does not lag like `npm view … version` did.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix: never run a repository's core.fsmonitor on git index reads

1 participant