Summary
WatchService.filter_changes in basic_memory/sync/watch_service.py silently drops every filesystem event for any project located under a directory whose name starts with . (e.g. ~/.claude/projects/..., ~/.config/...). The daemon runs, watch-status.json reports running: true, but synced_files: 0 and recent_events: [] indefinitely.
Reproduction
basic-memory project add my-project ~/.claude/projects/foo/memory (or any path containing a hidden-dir parent)
basic-memory mcp (or run the watch service directly)
- Touch a file inside the project:
echo "hello $(date +%s)" >> ~/.claude/projects/foo/memory/test.md
- Wait — observe
~/.basic-memory/watch-status.json:
{
"running": true,
"synced_files": 0,
"recent_events": []
}
- The file is never indexed; FTS / semantic search never finds it.
Root cause
filter_changes evaluates Path(path).parts — that is, every component of the absolute path, including parents above the watched root. Any component starting with . returns False. For projects under ~/.claude/..., the .claude component matches and the event is dropped before the sync handler ever sees it.
The intent of the filter is presumably "skip dotfiles within the project tree" (e.g. .git/, editor swap files). But evaluating absolute path components instead of components relative to the project root makes any project under any hidden parent unwatchable.
Suggested fix
Evaluate path parts relative to the project root, not absolute parts:
def filter_changes(self, change: Change, path: str) -> bool:
abs_path = Path(path)
for project in self._projects_cache: # however project list is accessed
try:
rel = abs_path.relative_to(Path(project.path))
except ValueError:
continue
for part in rel.parts:
if part.startswith("."):
return False
return not path.endswith(".tmp")
return False
This preserves the dotfile-skip behavior within projects (.git/, .DS_Store, etc.) while letting projects live under hidden parent dirs.
Why this matters
~/.claude/projects/<slug>/memory/ is the canonical location for Claude Code's per-project markdown auto-memory, and basic-memory is a natural FTS/semantic search backend over that tree. Currently the watcher cannot serve that use case at all — users have to fall back to a basic-memory reindex --search cron/timer to keep the index fresh.
Workaround
A 2-minute systemd-user reindex timer keeps FTS current without the watcher. End-to-end works, but it's wasteful (full reindex every 2 min vs. event-driven) and adds operational surface (two timers, status to monitor).
Environment
basic-memory 0.20.3 (installed via pip install --user)
- Linux (systemd-user), Python 3.13
- Several projects under
~/.claude/projects/<slug>/memory/
Tracking on our side: https://github.com/toddwilkens/isidore-infra/issues/81
Summary
WatchService.filter_changesinbasic_memory/sync/watch_service.pysilently drops every filesystem event for any project located under a directory whose name starts with.(e.g.~/.claude/projects/...,~/.config/...). The daemon runs,watch-status.jsonreportsrunning: true, butsynced_files: 0andrecent_events: []indefinitely.Reproduction
basic-memory project add my-project ~/.claude/projects/foo/memory(or any path containing a hidden-dir parent)basic-memory mcp(or run the watch service directly)echo "hello $(date +%s)" >> ~/.claude/projects/foo/memory/test.md~/.basic-memory/watch-status.json:{ "running": true, "synced_files": 0, "recent_events": [] }Root cause
filter_changesevaluatesPath(path).parts— that is, every component of the absolute path, including parents above the watched root. Any component starting with.returnsFalse. For projects under~/.claude/..., the.claudecomponent matches and the event is dropped before the sync handler ever sees it.The intent of the filter is presumably "skip dotfiles within the project tree" (e.g.
.git/, editor swap files). But evaluating absolute path components instead of components relative to the project root makes any project under any hidden parent unwatchable.Suggested fix
Evaluate path parts relative to the project root, not absolute parts:
This preserves the dotfile-skip behavior within projects (
.git/,.DS_Store, etc.) while letting projects live under hidden parent dirs.Why this matters
~/.claude/projects/<slug>/memory/is the canonical location for Claude Code's per-project markdown auto-memory, andbasic-memoryis a natural FTS/semantic search backend over that tree. Currently the watcher cannot serve that use case at all — users have to fall back to abasic-memory reindex --searchcron/timer to keep the index fresh.Workaround
A 2-minute systemd-user reindex timer keeps FTS current without the watcher. End-to-end works, but it's wasteful (full reindex every 2 min vs. event-driven) and adds operational surface (two timers, status to monitor).
Environment
basic-memory0.20.3 (installed viapip install --user)~/.claude/projects/<slug>/memory/Tracking on our side: https://github.com/toddwilkens/isidore-infra/issues/81