Bug Description
.bmignore is documented as using "standard gitignore-style syntax" — both in create_default_bmignore() and in the header of the file it generates — but the loader is a naive line filter, so no pattern beginning with # can ever take effect.
load_bmignore_patterns() in basic_memory/ignore_utils.py:
for line in f:
line = line.strip()
# Skip empty lines and comments
if line and not line.startswith("#"):
patterns.add(line)
There is no backslash unescaping anywhere, so:
#*# is silently dropped as a comment.
\#*# is loaded literally, then matched with fnmatch, which has no escape syntax — so it matches nothing.
Real gitignore covers exactly this case: "Put a backslash (\) in front of the first hash for patterns that begin with a hash."
Why it matters
This makes it impossible to ignore Emacs autosave files (#file#) — which is precisely what we needed, after an autosave corrupted a note's index row (#1537).
The failure mode is silent and actively misleading: the pattern looks correct sitting in .bmignore, and simply never loads. The shipped defaults already cover *~, *.swp and *.swo, so editor scratch files are clearly meant to be in scope — #file# is the gap, and it is the one that cannot be expressed.
Steps To Reproduce
Verified against the shipped functions, not from reading the code:
>>> from basic_memory.ignore_utils import load_bmignore_patterns, should_ignore_path
>>> # .bmignore containing: '# comment', '.*', '*~', '#*#', '*#'
>>> sorted(load_bmignore_patterns())
['*#', '*~', '.*'] # '#*#' silently dropped
>>> base = Path('/path/to/project')
>>> should_ignore_path(base/'notes/#note.md#', base, {'#*#'})
True
>>> should_ignore_path(base/'notes/#note.md#', base, {'\\#*#'})
False
>>> should_ignore_path(base/'notes/#note.md#', base, {'*#'})
True
Expected Behavior
Either honor the gitignore escape (\#), or stop describing the format as gitignore-style and document which subset is actually supported.
Actual Behavior
#*# never loads; \#*# loads but never matches. Note that should_ignore_path() itself handles #*# correctly — it fnmatches each path part individually, so the matcher is fine and only the loader is broken. That asymmetry is what makes this hard to diagnose from the outside.
Possible Solution
- In the loader, strip a single leading backslash after the comment check, so
\#*# yields the pattern #*#. The matcher already handles it.
- Add an Emacs autosave pattern to
DEFAULT_IGNORE_PATTERNS, alongside *~ and *.swp.
- Or adopt
pathspec (or similar) for genuine gitignore semantics, and keep the documented claim accurate.
Workaround for anyone hitting this: use *#, which survives the loader and matches per-path-part. It has no false positives in a Markdown project, since no legitimate note name ends in #. Confirmed live — a probe #probe.md# produced no index row and was not even logged, and the pattern took effect in already-running servers with no restart.
Environment
- OS: macOS 26.6.2 (Darwin 25.6.0), arm64
- Python: 3.14.7
- basic-memory: 0.23.2
- Install method: pipenv virtualenv; MCP servers launched by Claude Desktop and Claude Code
- Config: local SQLite,
semantic_search_enabled: true, index_changes: true, kebab_filenames: false
Bug Description
.bmignoreis documented as using "standard gitignore-style syntax" — both increate_default_bmignore()and in the header of the file it generates — but the loader is a naive line filter, so no pattern beginning with#can ever take effect.load_bmignore_patterns()inbasic_memory/ignore_utils.py:There is no backslash unescaping anywhere, so:
#*#is silently dropped as a comment.\#*#is loaded literally, then matched withfnmatch, which has no escape syntax — so it matches nothing.Real gitignore covers exactly this case: "Put a backslash (
\) in front of the first hash for patterns that begin with a hash."Why it matters
This makes it impossible to ignore Emacs autosave files (
#file#) — which is precisely what we needed, after an autosave corrupted a note's index row (#1537).The failure mode is silent and actively misleading: the pattern looks correct sitting in
.bmignore, and simply never loads. The shipped defaults already cover*~,*.swpand*.swo, so editor scratch files are clearly meant to be in scope —#file#is the gap, and it is the one that cannot be expressed.Steps To Reproduce
Verified against the shipped functions, not from reading the code:
Expected Behavior
Either honor the gitignore escape (
\#), or stop describing the format as gitignore-style and document which subset is actually supported.Actual Behavior
#*#never loads;\#*#loads but never matches. Note thatshould_ignore_path()itself handles#*#correctly — itfnmatches each path part individually, so the matcher is fine and only the loader is broken. That asymmetry is what makes this hard to diagnose from the outside.Possible Solution
\#*#yields the pattern#*#. The matcher already handles it.DEFAULT_IGNORE_PATTERNS, alongside*~and*.swp.pathspec(or similar) for genuine gitignore semantics, and keep the documented claim accurate.Workaround for anyone hitting this: use
*#, which survives the loader and matches per-path-part. It has no false positives in a Markdown project, since no legitimate note name ends in#. Confirmed live — a probe#probe.md#produced no index row and was not even logged, and the pattern took effect in already-running servers with no restart.Environment
semantic_search_enabled: true,index_changes: true,kebab_filenames: false