From 97c642af696ff87aea956744798943d437cb86e0 Mon Sep 17 00:00:00 2001 From: Mark Shust Date: Fri, 26 Jun 2026 12:40:59 -0400 Subject: [PATCH] feat: surface Claude Code multi-instance config-isolation tip on devai:install MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Running multiple concurrent Claude Code instances makes them contend on a single global ~/.claude.json, which Claude Code rewrites constantly. The contention causes Claude Code to tear down and reconnect its entire MCP fleet in lockstep — marko-mcp included — so the marko server appears to "randomly disconnect" even though nothing is wrong with it. This is a Claude Code behavior, not a Marko one, so rather than touching the user's shell, devai:install now prints a tip (only when Claude Code is among the installed agents) pointing at a new troubleshooting section documenting the per-project CLAUDE_CONFIG_DIR workaround. - InstallCommand: print the tip after the install summary when claude-code is among the selected agents - troubleshooting.md: new "Multiple Claude Code instances disconnect MCP servers" section with explanation + copy-paste shell wrapper - tests: tip shown for claude-code, hidden for other agents Co-Authored-By: Claude Opus 4.8 (1M context) --- .../devai/src/Commands/InstallCommand.php | 32 +++++++++++- .../Unit/Commands/InstallCommandTest.php | 50 +++++++++++++++++-- .../troubleshooting.md | 31 ++++++++++++ 3 files changed, 107 insertions(+), 6 deletions(-) diff --git a/packages/devai/src/Commands/InstallCommand.php b/packages/devai/src/Commands/InstallCommand.php index 5d438ea9..6f44c611 100644 --- a/packages/devai/src/Commands/InstallCommand.php +++ b/packages/devai/src/Commands/InstallCommand.php @@ -79,9 +79,39 @@ static function (string $message) use ($output): void { $output->writeLine(" - $line"); } + $this->maybePrintClaudeMultiInstanceTip($context->selectedAgents, $output); + return 0; } + /** + * Surface the Claude Code multi-instance gotcha once Claude Code is among the + * installed agents. Running several Claude Code instances concurrently makes + * them contend on a single global ~/.claude.json, which Claude Code rewrites + * constantly — the contention causes every MCP server (marko-mcp included) to + * disconnect and reconnect in lockstep. This is a Claude Code behavior, not a + * Marko one, so we only point at the documented per-project config-isolation + * workaround rather than touching the user's shell. + * + * @param list $selectedAgents + */ + private function maybePrintClaudeMultiInstanceTip( + array $selectedAgents, + Output $output, + ): void { + if (!in_array('claude-code', $selectedAgents, true)) { + return; + } + + $output->writeLine(''); + $output->writeLine('Tip: if you run multiple Claude Code instances at once, they contend on a'); + $output->writeLine('single ~/.claude.json and MCP servers (including marko-mcp) can disconnect and'); + $output->writeLine('reconnect repeatedly. To isolate Claude Code config per project, see:'); + $output->writeLine( + ' https://marko.build/docs/ai-assisted-development/troubleshooting/#multiple-claude-code-instances-disconnect-mcp-servers', + ); + } + /** * Offer to install the recommended docs search driver when none is present * and the session is interactive. Does nothing (falls through gracefully) in @@ -124,7 +154,7 @@ private function maybeInstallDocsDriver( $output->writeLine("Installing $pkg via composer (this may take a moment)…"); $result = $this->commandRunner->run( 'composer', - ['require', '--dev', '--no-interaction', '--no-progress', $pkg] + ['require', '--dev', '--no-interaction', '--no-progress', $pkg], ); if ($result['exitCode'] !== 0) { diff --git a/packages/devai/tests/Unit/Commands/InstallCommandTest.php b/packages/devai/tests/Unit/Commands/InstallCommandTest.php index c03c896b..98a82024 100644 --- a/packages/devai/tests/Unit/Commands/InstallCommandTest.php +++ b/packages/devai/tests/Unit/Commands/InstallCommandTest.php @@ -40,8 +40,7 @@ public function isInteractive(): bool public function confirm( string $question, bool $default, - ): bool - { + ): bool { return $this->answer; } }; @@ -66,8 +65,7 @@ public function __construct( public function run( string $command, array $args = [], - ): array - { + ): array { $this->calls[] = [$command, $args]; if ($command === 'composer' && ($args[0] ?? '') === 'require') { @@ -216,7 +214,7 @@ function readInstallCmdOutput(mixed $stream): string $cmd->execute(new Input(['marko', 'devai:install']), $output); expect($runner->calls)->toContain( - ['composer', ['require', '--dev', '--no-interaction', '--no-progress', 'marko/docs-fts']] + ['composer', ['require', '--dev', '--no-interaction', '--no-progress', 'marko/docs-fts']], ); }); @@ -345,6 +343,48 @@ function readInstallCmdOutput(mixed $stream): string ->and($text)->toContain('marko/docs-fts'); }); +// --------------------------------------------------------------------------- +// New tests: multi-instance config-isolation tip for Claude Code +// --------------------------------------------------------------------------- + +it('prints a multi-instance config-isolation tip when Claude Code is installed', function (): void { + chdir($this->tempRoot); + + $cmd = makeInstallCmd( + orchestrator: makeInstallCmdOrchestrator($this->tempRoot), + resolver: new DocsDriverResolver(), + prompter: makeInstallCmdFakePrompter(answer: false, interactive: false), + runner: makeInstallCmdRunner(composerOnPath: true), + ); + + ['stream' => $stream, 'output' => $output] = makeInstallCmdOutput(); + $cmd->execute( + new Input(['marko', 'devai:install', '--agents=claude-code', '--no-interaction', '--skip-lsp-deps']), + $output, + ); + + $text = readInstallCmdOutput($stream); + expect($text)->toContain('multiple Claude Code instances') + ->and($text)->toContain('marko.build/docs/ai-assisted-development/troubleshooting'); +}); + +it('does not print the Claude Code tip when only non-Claude agents are installed', function (): void { + chdir($this->tempRoot); + + $cmd = makeInstallCmd( + orchestrator: makeInstallCmdOrchestrator($this->tempRoot), + resolver: new DocsDriverResolver(), + prompter: makeInstallCmdFakePrompter(answer: false, interactive: false), + runner: makeInstallCmdRunner(composerOnPath: true), + ); + + ['stream' => $stream, 'output' => $output] = makeInstallCmdOutput(); + $cmd->execute(new Input(['marko', 'devai:install', '--agents=codex', '--no-interaction']), $output); + + $text = readInstallCmdOutput($stream); + expect($text)->not->toContain('multiple Claude Code instances'); +}); + it('keeps devai dependent on the marko/docs contract only (no driver in require)', function (): void { $composerJson = json_decode( (string) file_get_contents(dirname(__DIR__, 3) . '/composer.json'), diff --git a/packages/docs-markdown/docs/ai-assisted-development/troubleshooting.md b/packages/docs-markdown/docs/ai-assisted-development/troubleshooting.md index 89142131..1e28362d 100644 --- a/packages/docs-markdown/docs/ai-assisted-development/troubleshooting.md +++ b/packages/docs-markdown/docs/ai-assisted-development/troubleshooting.md @@ -98,6 +98,37 @@ The running MCP/LSP server re-checks staleness on every read for `app/` and `mod The `query_database` tool is only registered when `marko/database` is bound in the container. Install the database package and ensure it is configured before expecting this tool to appear. +### Multiple Claude Code instances disconnect MCP servers + +If you run several Claude Code instances at once (multiple terminals or windows) and notice MCP servers — `marko-mcp` included — repeatedly disconnecting and reconnecting, the cause is **not** Marko. Every Claude Code instance shares a single global `~/.claude.json`, and Claude Code rewrites that file constantly (history, tool-usage counters, session state). Concurrent writes to the one file make Claude Code tear down and reconnect its **entire** MCP fleet in lockstep, so all servers flap together. `marko-mcp` is often the one you notice because it boots a PHP process and is the slowest to re-handshake after each bounce. + +This is a known Claude Code issue (see [anthropics/claude-code#25768](https://github.com/anthropics/claude-code/issues/25768), [#28829](https://github.com/anthropics/claude-code/issues/28829)), independent of Marko. There is no Marko setting that fixes it — the reliable workaround is to give each project its own Claude Code config via the `CLAUDE_CONFIG_DIR` environment variable so concurrent instances stop contending on one file. + +Add this `claude` wrapper to your shell profile (`~/.zshrc` shown; adapt for bash). It gives each project its own isolated `.claude.json` under `~/.claude-profiles//` while sharing plugins, skills, commands, hooks, and settings via symlinks. Credentials live in the macOS Keychain and are shared automatically — no re-login per project. + +```bash +# Per-project Claude Code config profiles — stops concurrent instances from +# contending on a single ~/.claude.json (which causes MCP servers to flap). +claude() { + emulate -L zsh + local src="$HOME/.claude" globalcfg="$HOME/.claude.json" root profile item + root=$(git rev-parse --show-toplevel 2>/dev/null) || root="$PWD" + profile="$HOME/.claude-profiles/${root:t}" + mkdir -p "$profile" + for item in plugins skills commands hooks settings.json settings.local.json statusline-context.sh config ide; do + [[ -e "$src/$item" && ! -e "$profile/$item" ]] && ln -s "$src/$item" "$profile/$item" + done + # Seed the isolated config once from the real global ~/.claude.json so global + # MCP servers and plugin enablement carry over into the profile. + [[ ! -f "$profile/.claude.json" && -f "$globalcfg" ]] && cp "$globalcfg" "$profile/.claude.json" + CLAUDE_CONFIG_DIR="$profile" command claude "$@" +} +``` + +Open a new terminal (or `source ~/.zshrc`) and confirm isolation with `claude mcp list` from inside a project — your MCP servers should connect, and `~/.claude.json` should no longer be touched by that instance. + +> Note: `CLAUDE_CONFIG_DIR` is honored by the Claude Code CLI but ignored by the VS Code extension, which always uses `~/.claude/`. + ## LSP problems ### No completions appearing in the editor