fix(context): confirm before settings sync writes outside the project#484
fix(context): confirm before settings sync writes outside the project#484
Conversation
`mm context {generate,sync} --include=settings` previously wrote to
`~/.claude/settings.json` (the only registered settings target today)
silently, regardless of whether the command ran from the user's main
checkout or from a throwaway worktree. That made it easy for an
isolation harness — `setup-worktree.sh` only redirects storage and
memory dirs, not HOME — to leak hook merges into the real home
directory and forget to clean them up.
Add an opt-out confirmation gate. Before invoking
`generate_all_settings`, the CLI now collects every available
`SettingsGenerator` whose target file is not under the project root
and asks the user to acknowledge the list. `--yes` / `-y` skips the
prompt for scripted callers (e.g. `mm context sync --include=settings
--yes` in CI). Declining prints a "Skipped settings sync (declined)"
note and leaves the host file untouched; everything else
(`detect`, `diff`, project-memory generation, skills/agents/commands)
is unaffected because they do not write outside the project root.
The check uses `target.is_relative_to(root)` rather than a
`Path.home()` comparison so future project-scope settings generators
won't trigger the prompt at all. The existing `claude_settings`
generator stays user-scope; users who routinely sync from a single
checkout can pass `--yes` or stop being prompted by suppressing
generators we don't yet support filtering by name.
Tests: 7 new cases in `TestClaudeSettingsHostWritePrompt` covering
sync/generate × {decline, confirm, --yes}, plus two negatives
(missing canonical, runtime not installed) that assert no prompt
fires. Full suite: 2488 passed.
Co-Authored-By: Claude <[email protected]>
…efault Address PR #484 review feedback: * item 1 — plumb the gate to ``generate_all_settings`` so every front-end goes through it. The CLI was already gated; the MCP tools (``mem_context_sync`` / ``mem_context_generate``) and the Web route (``POST /settings-sync`` aliased as ``/context/settings/sync``) were bypassing the confirmation and silently writing ``~/.claude/settings.json``. Now the lib refuses any target whose resolved path is outside the resolved project root, marking those generators ``status="needs_confirmation"`` instead. Front-ends pass ``allow_host_writes=True`` after acknowledgement (CLI ``--yes`` / confirm, MCP tool param, Web request body). Project-scope generators are unaffected. * item 2 — generic ``--yes`` help text ("before writing settings files outside this project") so it does not rot the moment a non-Claude generator is added. * item 3 — CHANGELOG ``[Unreleased]`` ``Added`` entry covers the new prompt, the ``--yes`` flag, the lib gate, the front-end coverage, and the symlink resolution. * item 4 — ``Path.resolve(strict=False)`` on both target and project root so a symlinked ``<project>/.claude → ~/.claude`` cannot smuggle a host write past the gate. Strict=False keeps the call cheap when the target file does not exist yet. (Item 5 — preview of the diff before the prompt — left as a follow-up; out of scope here.) API change for libraries that call ``generate_all_settings`` directly: the function now takes a keyword-only ``allow_host_writes: bool = False`` parameter. The default is the safe one — old callers that imported the function without this kwarg now get a ``needs_confirmation`` status for host-scope generators instead of a silent write. Also exports the new helper ``host_write_targets(project_root)`` so a UI can list pending paths before posting back with ``allow_host_writes=True``. Tests: * Library — 9 new cases in ``TestGenerateAllSettingsHostWriteGate``: default refuses, ``allow_host_writes=True`` proceeds, ``diff_settings`` unaffected (still read-only), no-runtime + no-canonical still skip (not needs_confirmation), ``host_write_targets`` lists / empties as expected, and a symlink case that confirms ``Path.resolve()`` follows the link. * MCP — new ``test_server_tools_context_settings_gate.py`` (4 cases) pins the ``allow_host_writes`` threading through both ``mem_context_sync`` and ``mem_context_generate``. * Web — 2 new cases in ``TestSettingsSync`` covering the POST default refusal and the ``{"allow_host_writes": true}`` proceed path. Full suite: 2509 passed locally. ``ruff check`` + ``ruff format --check`` + ``mypy`` on the four touched source files all clean. Co-Authored-By: Claude <[email protected]>
76e967a to
3ed42f7
Compare
|
Thanks for the thorough review — addressed all 4 in-scope items in 3ed42f7. The big one (item 1) reshapes the API; rebased on the new main first. Item 1 — the gate now lives in
Also exported a Item 2 — Item 3 — Item 4 — (Item 5 — diff preview before the prompt — left as a follow-up; out of scope here.) Tests added (9 lib + 4 MCP + 2 Web = 15 new cases):
API note: Local: |
Summary
mm context {generate,sync} --include=settingsnow confirms beforewriting to a settings file that lives outside the project root
(today only
~/.claude/settings.json); declining leaves the hostuntouched.
--yes/-yskips the prompt for scripted callers.target.is_relative_to(root)) so futureproject-scope settings generators won't prompt at all.
Why
Running
mm context sync --include=settingsfrom a worktree silentlyedited the real
~/.claude/settings.json.setup-worktree.shredirects storage and memory dirs but not HOME, so the host
pollution was easy to miss. This is one of the three P0 items called
out in the 2026-04-26 context-gateway audit (alongside the
round-trip data loss already fixed in #482 and the codex_agents
project-scope fix in #483).
Test plan
TestClaudeSettingsHostWritePromptintest_context_settings.py(7 cases): sync/generate × {decline,confirm,
--yes} + 2 negatives (missing canonical / runtimeunavailable) that assert no prompt fires.
uv run pytest packages/memtomem/tests -m "not ollama"— 2488passed locally.
uv run ruff check+ruff format --checkclean.uv run mypy packages/memtomem/src/memtomem/cli/context_cmd.py— no issues.
🤖 Generated with Claude Code