Skip to content

watch service drops events for projects under hidden-directory parents (e.g. ~/.claude/...) #798

@toddwilkens

Description

@toddwilkens

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

  1. basic-memory project add my-project ~/.claude/projects/foo/memory (or any path containing a hidden-dir parent)
  2. basic-memory mcp (or run the watch service directly)
  3. Touch a file inside the project: echo "hello $(date +%s)" >> ~/.claude/projects/foo/memory/test.md
  4. Wait — observe ~/.basic-memory/watch-status.json:
    {
      "running": true,
      "synced_files": 0,
      "recent_events": []
    }
  5. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions