Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -479,7 +479,7 @@ multi-word ones.

Bare `- [[Target]]` and prose `- Worth checking out [[Target]]` index as
`links_to`. Full reference in the
[docs](https://docs.basicmemory.com/getting-started/note-formatting/?utm_source=github&utm_medium=referral&utm_campaign=readme).
[docs](https://docs.basicmemory.com/concepts/knowledge-format?utm_source=github&utm_medium=referral&utm_campaign=readme).

## MCP tools

Expand Down Expand Up @@ -525,7 +525,7 @@ basic-memory import memory-json

Routing flags (`--local` / `--cloud`) force a target when you're in mixed
mode. Full CLI reference in the
[docs](https://docs.basicmemory.com/guides/cli-reference/?utm_source=github&utm_medium=referral&utm_campaign=readme).
[docs](https://docs.basicmemory.com/reference/cli-reference?utm_source=github&utm_medium=referral&utm_campaign=readme).

## Auto-updates

Expand Down
2 changes: 1 addition & 1 deletion docs/cloud-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ The transfer commands fall into two groups:
Before using Basic Memory Cloud, you need:

- **Active Subscription**: An active Basic Memory Cloud subscription is required to access cloud features
- **Subscribe**: Visit [https://basicmemory.com/subscribe](https://basicmemory.com/subscribe) to sign up
- **Subscribe**: Visit [https://basicmemory.com/pricing](https://basicmemory.com/pricing) to sign up
- **Optional**: Cloud is optional. Local-first open-source usage continues without cloud.
- **OSS Discount**: Use code `{{OSS_DISCOUNT_CODE}}` for 20% off for 3 months.

Expand Down
9 changes: 7 additions & 2 deletions src/basic_memory/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -959,8 +959,13 @@ def load_config(self) -> BasicMemoryConfig:
# Then overlay with file data for fields that aren't set via env vars
# This ensures env vars take precedence

# Get env-based config fields that are actually set
env_config = BasicMemoryConfig()
# Get env-based config fields that are actually set.
# skip_initialization_sync=True keeps this probe side-effect-free:
# model_post_init won't seed a default project, and
# ensure_project_paths_exists won't mkdir. Pydantic Settings still
# reads env vars as field defaults, so env_dict reflects env-derived
# values without writing to the filesystem. See GH#1029.
env_config = BasicMemoryConfig(skip_initialization_sync=True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve false skip-initialization env overrides

When a config file exists and the process sets BASIC_MEMORY_SKIP_INITIALIZATION_SYNC=false, this probe is built with the init kwarg forced to True. Pydantic-settings init kwargs take precedence over env sources, and the merge loop below copies env_dict["skip_initialization_sync"] whenever that env var is present, so an explicit false override is turned into true. That makes local runs report skip_local_initialization and skip project seeding/path creation plus initialize_app() database/reconcile work.

Useful? React with 👍 / 👎.

env_dict = env_config.model_dump()

# Merge: file data as base, but only use it for fields not set by env
Expand Down
52 changes: 52 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,58 @@ def test_config_file_without_default_project_key(self, config_home, monkeypatch)
loaded = config_manager.load_config()
assert loaded.default_project == "work"

def test_load_config_does_not_recreate_phantom_home_dir(
self, config_home, monkeypatch, tmp_path
):
"""Regression for GH#1029: load_config must not recreate ~/basic-memory.

When BASIC_MEMORY_HOME is unset, loading a config that already specifies
a user-chosen project path must not also seed the default ~/basic-memory
directory as a side effect of probing env vars.
"""
import json
import basic_memory.config

# HOME is set by config_home; phantom lives at $HOME/basic-memory.
monkeypatch.delenv("BASIC_MEMORY_HOME", raising=False)
monkeypatch.delenv("BASIC_MEMORY_CLOUD_MODE", raising=False)

phantom = Path(os.environ["HOME"]) / "basic-memory"
user_project = config_home / "user" / "vault"
# Force the phantom path to live inside tmp_path so the test never touches
# the real $HOME (the user may have a real ~/basic-memory vault).
# We do this by repointing Path.home() to a fresh tmp dir.
phantom_home = tmp_path / "real_home"
phantom = phantom_home / "basic-memory"
monkeypatch.setattr(Path, "home", classmethod(lambda cls: phantom_home))

config_manager = ConfigManager()
config_manager.config_dir = config_home / ".basic-memory"
config_manager.config_file = config_manager.config_dir / "config.json"
config_manager.config_dir.mkdir(parents=True, exist_ok=True)

config_data = {
"projects": {"main": {"path": str(user_project), "mode": "local"}},
"default_project": "main",
}
config_manager.config_file.write_text(json.dumps(config_data, indent=2))
basic_memory.config._CONFIG_CACHE = None
basic_memory.config._CONFIG_MTIME = None
basic_memory.config._CONFIG_SIZE = None

assert not phantom.exists()

loaded = config_manager.load_config()

# User's project is preserved — never replaced by a phantom seed.
assert Path(loaded.projects["main"].path) == user_project
assert loaded.default_project == "main"
# The phantom ~/basic-memory dir must not have been created as a side effect.
assert not phantom.exists(), (
f"load_config recreated {phantom} as a side effect of probing env vars "
f"(see GH#1029)"
)


class TestDataDirHelpers:
"""Module-level helpers that resolve the Basic Memory data directory."""
Expand Down