- Startup passed on ANY scan error: WalkDir stopped at the first
unreadable dir and Scan discarded what it had found, while search
kept walking and served a planted symlink. Scan now records
unreadable dirs and keeps going; startup refuses on symlinks or
unreadable dirs and only lets a missing brain dir pass.
- Watch went silent while a scan error persisted, and marked a set as
reported before Notify, so an ntfy outage lost the alert. It now
alerts on unreadable dirs and scan errors too, and records a set as
reported only after Notify succeeds (retried each tick).
- Only the root .git is skipped; a nested .git is scanned.
- A non-positive BRAIN_SYMLINK_SCAN_INTERVAL fell into time.NewTicker
and crashlooped the server; it now falls back to the default.
- Findings are quoted in alerts, so a newline in a name cannot merge
two sets; alertingWarnings names the tripwire when ntfy is unset.
- Test names match what they assert (two-set dedupe, symlinked entries
inside a symlinked root, nested .git).
Flipped: startup ignoring unreadable dirs, .git skipped at any depth,
and no retry each turn a test red. Watch tests stable over -count=10
and under -race.
Refs #157
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The brain dir is a Syncthing sendreceive folder and Syncthing syncs
symlinks; search, vectorstore, context and graph readers follow one out
of the brain and would index or return what it points to (reproduced
with search.Query). Decision (#157): a tripwire now, brain-wide os.Root
readers next.
- symlinkguard.Scan lists every symlink inside the brain dir (skipping
.git; the brain dir itself may be a mount symlink).
- At startup the server refuses to start while any exists, naming them.
- symlinkguard.Watch rescans every BRAIN_SYMLINK_SCAN_INTERVAL seconds
(default 300) and sends one ntfy alert per new set of symlinks.
The live brain dir had 0 symlinks on 2026-10-03, checked before this
change, so deploying it does not trip.
Refs #157
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>