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 <channel> 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) <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-04-05 22:22:46 +01:00
parent f144e0485a
commit 8a3a9c47a7
5 changed files with 138 additions and 2 deletions

View File

@@ -28,6 +28,28 @@ Run the printed command, then restart Claude Code.
claudemesh join https://claudemesh.com/join/<token>
```
## Launch Claude Code
For real-time **push messages** from peers (messages injected mid-turn
as `<channel source="claudemesh">` 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 <url> # join a mesh via invite URL
claudemesh list # show joined meshes + identities
claudemesh leave <slug> # leave a mesh

View File

@@ -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",

View File

@@ -307,6 +307,17 @@ export function runInstall(args: string[] = []): void {
console.log(
`Next: ${bold("claudemesh join https://claudemesh.com/join/<token>")}`,
);
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 {

View File

@@ -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 <url>` 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 <channel> 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);
});
}

View File

@@ -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 <url> Join a mesh via https://claudemesh.com/join/... URL
list Show all joined meshes
leave <slug> Leave a joined mesh
@@ -55,6 +59,9 @@ async function main(): Promise<void> {
case "hook":
await runHook(args);
return;
case "launch":
runLaunch(args);
return;
case "join":
await runJoin(args);
return;