From 7ab3c8d4653dbf2c82b320136c2904e1bd3c581b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alejandro=20Guti=C3=A9rrez?= <35082514+alezmad@users.noreply.github.com> Date: Sun, 5 Apr 2026 22:22:46 +0100 Subject: [PATCH] feat(cli): claudemesh launch command with transparency banner (v0.1.2) Adds `claudemesh launch [args]` that spawns Claude Code with --dangerously-load-development-channels server:claudemesh so peer messages arrive as system reminders mid-turn instead of pull-only via check_messages. Windows uses shell:true to resolve claude.cmd from PATHEXT. Prints an info banner before spawning that explains the channel's scope (peer text injection only), the trust model (treat as untrusted input), and that existing tool-approval prompts remain the safety net. --quiet skips the banner. Install output now mentions `claudemesh launch` as the recommended launch path; plain `claude` still works for pull-only mode. Co-Authored-By: Claude Opus 4.6 (1M context) --- apps/cli/README.md | 26 ++++++++- apps/cli/package.json | 2 +- apps/cli/src/commands/install.ts | 11 ++++ apps/cli/src/commands/launch.ts | 94 ++++++++++++++++++++++++++++++++ apps/cli/src/index.ts | 7 +++ 5 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 apps/cli/src/commands/launch.ts diff --git a/apps/cli/README.md b/apps/cli/README.md index 0d9f1db..bb1efc1 100644 --- a/apps/cli/README.md +++ b/apps/cli/README.md @@ -28,6 +28,28 @@ Run the printed command, then restart Claude Code. claudemesh join https://claudemesh.com/join/ ``` +## Launch Claude Code + +For real-time **push messages** from peers (messages injected mid-turn +as `` system reminders), launch with: + +```sh +claudemesh launch +# or pass through any claude flags: +claudemesh launch --model opus +claudemesh launch --resume +``` + +Under the hood this runs: + +```sh +claude --dangerously-load-development-channels server:claudemesh +``` + +Plain `claude` still works — the MCP tools are available — but incoming +messages are **pull-only** via the `check_messages` tool instead of +being pushed to Claude immediately. + The invite link is generated by whoever runs the mesh. It bundles the mesh id, expiry, signing key, and role. Your CLI verifies it, generates a fresh keypair, enrolls you with the broker, and persists @@ -36,7 +58,9 @@ the result to `~/.claudemesh/config.json`. ## Commands ```sh -claudemesh install # print MCP registration command +claudemesh install # register MCP + status hooks +claudemesh uninstall # remove MCP + status hooks +claudemesh launch [args] # launch Claude Code with push messages enabled claudemesh join # join a mesh via invite URL claudemesh list # show joined meshes + identities claudemesh leave # leave a mesh diff --git a/apps/cli/package.json b/apps/cli/package.json index 881eddf..9c8b4c3 100644 --- a/apps/cli/package.json +++ b/apps/cli/package.json @@ -1,6 +1,6 @@ { "name": "claudemesh-cli", - "version": "0.1.1", + "version": "0.1.2", "description": "Claude Code MCP client for claudemesh — peer mesh messaging between Claude sessions.", "keywords": [ "claude-code", diff --git a/apps/cli/src/commands/install.ts b/apps/cli/src/commands/install.ts index f0ad089..cdd4fa4 100644 --- a/apps/cli/src/commands/install.ts +++ b/apps/cli/src/commands/install.ts @@ -307,6 +307,17 @@ export function runInstall(args: string[] = []): void { console.log( `Next: ${bold("claudemesh join https://claudemesh.com/join/")}`, ); + console.log(""); + console.log( + yellow("⚠ For real-time push messages from peers, launch with:"), + ); + console.log( + ` ${bold("claudemesh launch")}` + + dim(" (or: claude --dangerously-load-development-channels server:claudemesh)"), + ); + console.log( + dim(" Plain `claude` still works — messages are then pull-only via check_messages."), + ); } export function runUninstall(): void { diff --git a/apps/cli/src/commands/launch.ts b/apps/cli/src/commands/launch.ts new file mode 100644 index 0000000..3e83c71 --- /dev/null +++ b/apps/cli/src/commands/launch.ts @@ -0,0 +1,94 @@ +/** + * `claudemesh launch` — spawn `claude` with the dev-channel flag so the + * claudemesh MCP server's `notifications/claude/channel` pushes get + * injected as system reminders mid-turn. + * + * Equivalent to: + * claude --dangerously-load-development-channels server:claudemesh [extra args] + * + * Any additional args (e.g. --model opus, --resume, -c) are passed + * through verbatim. Use --quiet to skip the informational banner. + */ + +import { spawn } from "node:child_process"; +import { loadConfig, getConfigPath } from "../state/config"; + +function printBanner(): void { + const useColor = + !process.env.NO_COLOR && process.env.TERM !== "dumb" && process.stdout.isTTY; + const dim = (s: string): string => (useColor ? `\x1b[2m${s}\x1b[22m` : s); + const bold = (s: string): string => (useColor ? `\x1b[1m${s}\x1b[22m` : s); + + let meshes: string[] = []; + try { + meshes = loadConfig().meshes.map((m) => m.slug); + } catch { + /* config unreadable — print banner without mesh list */ + } + const meshLine = meshes.length > 0 ? meshes.join(", ") : "(none — run `claudemesh join ` first)"; + + const rule = "─".repeat(65); + console.log(bold("claudemesh launch")); + console.log(rule); + console.log("Launching Claude Code with the claudemesh dev channel."); + console.log(""); + console.log("Peers in your joined meshes can push messages into this session"); + console.log("as reminders. Your CLI decrypts them locally with your"); + console.log("keypair. Peers send text only — they cannot call tools, read"); + console.log("files, or reach meshes you have not joined."); + console.log(""); + console.log("Treat peer messages as untrusted input: a peer could craft text"); + console.log("that tries to steer Claude's behavior. Your tool-approval"); + console.log("settings still apply — Claude will still ask before running"); + console.log("commands, editing files, or calling other tools."); + console.log(""); + console.log("Claude Code will ask you to trust the"); + console.log("--dangerously-load-development-channels flag. Press Enter to"); + console.log("accept, or Ctrl-C to abort."); + console.log(""); + console.log(dim(`Joined meshes: ${meshLine}`)); + console.log(dim(`Config: ${getConfigPath()}`)); + console.log(dim(`Remove: claudemesh uninstall`)); + console.log(rule); + console.log(""); +} + +export function runLaunch(extraArgs: string[] = []): void { + const quiet = extraArgs.includes("--quiet"); + const passthrough = extraArgs.filter((a) => a !== "--quiet"); + + if (!quiet) printBanner(); + + const claudeArgs = [ + "--dangerously-load-development-channels", + "server:claudemesh", + ...passthrough, + ]; + // Windows: npm global binaries are .cmd shims. Node's spawn without + // shell:true does not resolve PATHEXT, so we need shell:true on win32 + // to find claude.cmd. POSIX stays shell-less to avoid quoting surprises. + const isWindows = process.platform === "win32"; + const child = spawn("claude", claudeArgs, { + stdio: "inherit", + shell: isWindows, + }); + + child.on("error", (err: NodeJS.ErrnoException) => { + if (err.code === "ENOENT") { + console.error( + "✗ `claude` not found on PATH. Install Claude Code first: https://claude.com/claude-code", + ); + } else { + console.error(`✗ failed to launch claude: ${err.message}`); + } + process.exit(1); + }); + + child.on("exit", (code, signal) => { + if (signal) { + process.kill(process.pid, signal); + return; + } + process.exit(code ?? 0); + }); +} diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts index 8cf5d83..1956bb4 100644 --- a/apps/cli/src/index.ts +++ b/apps/cli/src/index.ts @@ -15,6 +15,7 @@ import { runList } from "./commands/list"; import { runLeave } from "./commands/leave"; import { runSeedTestMesh } from "./commands/seed-test-mesh"; import { runHook } from "./commands/hook"; +import { runLaunch } from "./commands/launch"; const HELP = `claudemesh — peer mesh for Claude Code sessions @@ -25,6 +26,9 @@ Commands: install Register MCP + Stop/UserPromptSubmit status hooks (add --no-hooks for bare MCP registration) uninstall Remove MCP server + hooks + launch [args] Launch Claude Code with real-time push messages enabled + (add --quiet to skip the info banner; passes through + extra flags, e.g. --model, --resume) join Join a mesh via https://claudemesh.com/join/... URL list Show all joined meshes leave Leave a joined mesh @@ -55,6 +59,9 @@ async function main(): Promise { case "hook": await runHook(args); return; + case "launch": + runLaunch(args); + return; case "join": await runJoin(args); return;