feat(plugin): add mempalace-remote — SSH-proxied palace for remote clients#1190
feat(plugin): add mempalace-remote — SSH-proxied palace for remote clients#1190messelink wants to merge 7 commits intoMemPalace:developfrom
Conversation
…ients
Adds a sibling plugin that mirrors the mempalace plugin's MCP tools and
auto-save hooks, but proxies them to a central host over SSH instead of
running locally. Lets a fleet of client machines share one canonical
palace without each running their own mempalace install.
Plugin layout matches the existing .claude-plugin/ structure:
.claude-plugin-remote/
plugin.json
.mcp.json ssh ${HOST} mempalace-mcp
hooks/hooks.json registers Stop + PreCompact
hooks/mempal-stop-hook.sh ssh-pipe stdin to remote `mempalace hook run`
hooks/mempal-precompact-hook.sh same for precompact
The marketplace.json `plugins` array gains a second entry so the existing
`MemPalace/mempalace` marketplace serves both plugins from the same repo.
Existing users are unaffected — `claude /plugin install mempalace@mempalace`
still installs only the local-install variant.
Single required env var: MEMPALACE_REMOTE_HOST (an alias from ~/.ssh/config
or a fully qualified hostname). README documents passwordless-key prereq,
ControlMaster recommendation, and the SSH non-interactive PATH gotcha.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Lets users point at full paths on the central host without symlinking into /usr/local/bin or fixing the SSH non-interactive PATH. Both vars are optional and default to bare 'mempalace' / 'mempalace-mcp' for hosts where those are on PATH already. Documented as the recommended fix for the PATH gotcha section. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
- Replace 'raindance' (my central host alias) with the generic 'palace-host' in the env var example and ControlMaster snippet. - 'Strongly recommended: SSH ControlMaster' -> 'Optional' — sub-300ms per call without it on a real two-machine test, only worth setting up under heavy tool use. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
Hi, MEMPALACE_REMOTE_HOST is passed as the first positional argument to ssh without an option terminator, so a value starting with "-" will be parsed as ssh options (e.g., ProxyCommand), enabling unintended local command execution under the plugin process. This affects both the MCP server spawn and the Stop/PreCompact hook invocations. Severity: action required | Category: security How to fix: Terminate ssh options with -- Agent prompt to fix - you can give this to your LLM of choice:
We noticed a couple of other issues in this PR as well - happy to share if helpful. Spotted by Qodo code review - free for open-source projects. |
Per Qodo review on MemPalace#1190: passing MEMPALACE_REMOTE_HOST as the first positional argument to ssh without an option terminator means a value starting with '-' would be parsed as ssh options (e.g. -oProxyCommand=...), enabling local command execution. Adds -- before the host in both the .mcp.json args array and both hook scripts. Smoke test still returns {} cleanly. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
Thanks @qodo-ai-reviewer — good catch. Fixed in 3844bbe: added Yes please do share the other issues you spotted. |
Pre-existing concern, surfaced by the same Qodo review on MemPalace#1190 that caught the host option-parsing issue: MEMPALACE_REMOTE_BIN and MEMPALACE_REMOTE_MCP_BIN are concatenated into the SSH remote command string, which the central host's shell then parses. A value like 'mempalace; touch /tmp/X #' lands as 'sh -c "mempalace; touch /tmp/X # hook run ..."' on the remote — RCE on the central host as the SSH user. Fix: - Hook scripts validate HOST and BIN against ^[A-Za-z0-9_./@-]+$ and ^[A-Za-z0-9_./-]+$ before invoking ssh; fail loudly with a clear message if the env var contains anything outside that set. - MCP server entry switches from raw 'ssh' + JSON args to a thin wrapper bin/mempalace-mcp-ssh.sh that does the same validation. Lets us apply defense in JSON config too without trying to do shell-quoting in JSON. Both .mcp.json and plugin.json's mcpServers block now point at ${CLAUDE_PLUGIN_ROOT}/bin/mempalace-mcp-ssh.sh. The character classes cover all real paths and hostnames (incl. user@host in HOST) and reject ;, &, |, $, (, ), <, >, backtick, quote, space, and shell-glob metas. The '--' option terminator on ssh stays as belt-and- suspenders against the host-as-option case. Smoke test: legitimate values still work end-to-end (Stop hook returns {} cleanly); 'BIN=...; touch /tmp/X #' is rejected before any ssh call. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
|
Found a related second issue while looking for more — the same env-var injection vector applies to the bin path arguments:
Fixed in d6e728b:
Smoke-tested both: legitimate values still produce If you have more findings still queued, please share — happy to keep iterating. |
Bumps both the plugin manifest and the marketplace entry so installed clients pick up the security fixes (3844bbe + d6e728b) on the next '/plugin marketplace update' + '/plugin update' cycle. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The bash wrapper around the MCP server and the bash hook scripts won't run on bare Windows cmd/PowerShell. Document the requirement explicitly since this plugin introduces the bash-MCP dependency the local mempalace plugin doesn't have (it ships a pip-installed mempalace-mcp.exe directly). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Summary
Adds a sibling plugin,
mempalace-remote, that mirrors the existingmempalaceplugin's MCP tools and auto-save hooks but proxies them to a central host over SSH instead of running locally. Lets a fleet of client machines (VPS, second laptop, dev container, etc.) share one canonical palace without each running its own mempalace install.The marketplace
pluginsarray gains a second entry, so the sameMemPalace/mempalacerepo serves both plugins. Existing users are unaffected —claude /plugin install mempalace@mempalacestill installs only the local-install variant.Why
I run mempalace on a home server and use Claude Code from several other machines. The hand-rolled approach was a pair of
~/.local/bin/{mempalace,mempalace-mcp}shell wrappers thatssh ...to the central install. That works for the MCP server but skips the auto-save Stop and PreCompact hooks entirely, since the upstream plugin's hook scripts call the localmempalaceCLI directly. Packaging the wrappers + hook proxies as a real plugin makes the central-palace pattern install-and-go for anyone running the same setup.Discussed (lightly) in #1049 and adjacent threads about non-local mempalace deployments.
Layout
Mirrors the existing
.claude-plugin/structure:.claude-plugin/marketplace.jsongains a single appended entry pointing at./.claude-plugin-remote.Configuration
MEMPALACE_REMOTE_HOST~/.ssh/configor fully qualified hostnameMEMPALACE_REMOTE_BINmempalacemempalaceCLI on the remote (used by Stop / PreCompact hooks)MEMPALACE_REMOTE_MCP_BINmempalace-mcpmempalace-mcpserver on the remote (used by the MCP client)The
_BINoverrides cover the SSH non-interactive PATH gotcha where~/.local/binisn't on PATH forssh host commandshells. Bare defaults work for system-wide installs.How it works
claudespawns${CLAUDE_PLUGIN_ROOT}/bin/mempalace-mcp-ssh.sh, which validates the env vars andexecsssh -- $HOST mempalace-mcp. Stdio JSON-RPC rides the SSH channel transparently — the MCP client doesn't know it's remote.ssh -- $HOST mempalace hook run --hook {stop,precompact} --harness claude-code. The remotemempalaceCLI handles all the actual logic; the bash wrapper is intentionally thin.Coexistence
Don't enable both
mempalaceandmempalace-remoteon the same machine — both register an MCP server namedmempalaceand the second one to load shadows the first. The README calls this out.Tested
End-to-end test on a real two-machine setup (VPS client → home-server central palace):
claude /plugin marketplace add→claude /plugin install mempalace-remote@mempalace→ set env vars → restart Claude Code → workingmempalace_statusMCP call returned real wing/room counts from the central palace ✓~/.mempalace/hook_state/hook.logon the central host with the correct client session ID ✓/compact) fired and landed in the same log ✓claude plugin validatepasses on both.claude-plugin/plugin.jsonand.claude-plugin-remote/plugin.json.Security
Two issues caught in review and fixed:
MEMPALACE_REMOTE_HOSTis now passed after--so a value starting with-can't be interpreted as ssh options (e.g.-oProxyCommand=...local code execution). Caught by Qodo review._BINenv vars —MEMPALACE_REMOTE_BINandMEMPALACE_REMOTE_MCP_BINare validated against an allow-list regex (^[A-Za-z0-9_./-]+$) before being concatenated into the SSH remote command string, blocking theBIN='...; rm -rf ~ #'class. The MCP server config goes through thebin/mempalace-mcp-ssh.shwrapper to apply the same defense the JSON config can't express directly.Notes for review
MEMPALACE_REMOTE_*namespace; no clash with existingMEMPAL_*env vars used by the local plugin.messelink(mine). Happy to change to whatever attribution convention you prefer..claude-plugin/hooks/hooks.json(e.g. adds SessionStart), the remote variant should mirror the addition. Same protocol, same CLI signature. Adjacent: Consolidatehooks/mempal_*_hook.shinto thin wrappers delegating tomempalace hook run#1069's proposed consolidation of the legacyhooks/mempal_*_hook.shscripts uses the same thin-wrapper-to-mempalace hook runpattern this PR follows..codex-plugin-remote/if there's interest.