Skip to content

fix(context): confirm before settings sync writes outside the project#484

Merged
memtomem merged 2 commits intomainfrom
feat/settings-sync-host-write-prompt
Apr 26, 2026
Merged

fix(context): confirm before settings sync writes outside the project#484
memtomem merged 2 commits intomainfrom
feat/settings-sync-host-write-prompt

Conversation

@pandas-studio
Copy link
Copy Markdown
Collaborator

Summary

  • mm context {generate,sync} --include=settings now confirms before
    writing to a settings file that lives outside the project root
    (today only ~/.claude/settings.json); declining leaves the host
    untouched.
  • --yes / -y skips the prompt for scripted callers.
  • Path-based gate (target.is_relative_to(root)) so future
    project-scope settings generators won't prompt at all.

Why

Running mm context sync --include=settings from a worktree silently
edited the real ~/.claude/settings.json. setup-worktree.sh
redirects 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

  • New regression class TestClaudeSettingsHostWritePrompt in
    test_context_settings.py (7 cases): sync/generate × {decline,
    confirm, --yes} + 2 negatives (missing canonical / runtime
    unavailable) that assert no prompt fires.
  • uv run pytest packages/memtomem/tests -m "not ollama" — 2488
    passed locally.
  • uv run ruff check + ruff format --check clean.
  • uv run mypy packages/memtomem/src/memtomem/cli/context_cmd.py
    — no issues.

🤖 Generated with Claude Code

pandas-studio and others added 2 commits April 26, 2026 11:38
`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]>
@memtomem memtomem force-pushed the feat/settings-sync-host-write-prompt branch from 76e967a to 3ed42f7 Compare April 26, 2026 02:47
@memtomem
Copy link
Copy Markdown
Owner

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 generate_all_settings (the I/O boundary). New keyword-only allow_host_writes: bool = False param: targets whose resolved path is outside the resolved project root come back with status="needs_confirmation" instead of being written. Front-ends each pass True only after acknowledgement:

  • CLI — same interactive confirm / --yes / -y as before, threaded into generate_all_settings(allow_host_writes=...).
  • MCPmem_context_sync and mem_context_generate accept allow_host_writes: bool = False. Default surfaces a needs confirmation <name>: <path> line so the agent can re-call after asking the user.
  • WebPOST /settings-sync now takes a body {"allow_host_writes": false}. Default refuses; UI is expected to list targets and re-post with true.

Also exported a host_write_targets(project_root) helper so any caller (CLI prompt, MCP tool author, Web modal) gets the same list of pending host paths without re-implementing the rule.

Item 2--yes help text now generic: Skip confirmation prompts before writing settings files outside this project.

Item 3CHANGELOG.md [Unreleased] Added entry covers the prompt, --yes, the lib gate, the MCP/Web coverage, and the symlink fix.

Item 4Path.resolve(strict=False) on both target and project root, so a symlinked <project>/.claude → ~/.claude no longer smuggles a host write past the gate. There's a regression test for that exact case.

(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):

  • TestGenerateAllSettingsHostWriteGate (lib): default refuses; allow_host_writes=True proceeds; diff_settings unaffected; no-runtime / no-canonical → skipped (not needs_confirmation); host_write_targets lists/empties; symlink resolves to host write.
  • test_server_tools_context_settings_gate.py (MCP): refuse / accept × mem_context_sync and mem_context_generate.
  • TestSettingsSync (Web): POST default refusal; POST with {"allow_host_writes": true} proceeds.

API note: generate_all_settings now defaults to refusing host writes. Any library caller that imported it without the new kwarg goes from "always writes" to "needs_confirmation" — that's the desired behavior change. Captured in CHANGELOG.

Local: ruff check + ruff format --check + mypy clean on the four changed source files; 2509 passed on the full suite.

@memtomem memtomem merged commit 440936c into main Apr 26, 2026
7 checks passed
@memtomem memtomem deleted the feat/settings-sync-host-write-prompt branch April 26, 2026 02:49
@github-actions github-actions Bot locked and limited conversation to collaborators Apr 26, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants