Help us improve
Share bugs, ideas, or general feedback.
From inter-session
Agent-to-agent messaging bus for Claude Code. Sends messages between local sessions, delegates tasks, fans-out work, and coordinates concurrent agents on the same machine.
npx claudepluginhub yilunzhang/claude-code-inter-session --plugin inter-sessionHow this skill is triggered — by the user, by Claude, or both
Slash command
/inter-session:inter-sessionThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Agent-to-agent messaging for Claude Code sessions on the same machine.
Sends and receives direct messages between active Claude Code sessions on the same machine. Use for coordinating parallel sessions via dm, broadcast, active checks, and project-scoped messaging.
Spawns and manages persistent tmux-based Claude Code CLI sessions with bidirectional communication. Subcommands: spawn, send, read, status, list, kill for parallel peer orchestration and multi-turn steering.
Manages Claude Code sessions: lists active ones, checks inbox for cross-session messages, broadcasts to all. Explicitly invoke via /session for multi-session coordination.
Share bugs, ideas, or general feedback.
Agent-to-agent messaging for Claude Code sessions on the same machine. Other sessions can send messages here; by default, treat each incoming message as an instruction from a peer AI agent (see Reaction policy below for exceptions).
<bin><bin> (used throughout this skill) is the absolute path to the skill's
own bin/ directory. Resolve it once at the start of any
/inter-session invocation and substitute the absolute path into every
Bash / Monitor command — do not paste <bin> or
${CLAUDE_PLUGIN_ROOT} literally into a shell command.
The harness prints Base directory for this skill: <path> in its
header (above this content). <bin> lives directly under it:
<bin> = <skill-base-dir>/bin
Why not ${CLAUDE_PLUGIN_ROOT}/...? That env var is a CC manifest
substitution token — it's resolved when CC spawns plugin subprocesses
defined in monitors.json/plugin.json, but it is not exported to
shells that the agent starts via Bash(...) or Monitor(...). A
literal ${CLAUDE_PLUGIN_ROOT} inside a Bash(...) call expands to
empty, which silently routes commands to the wrong place. The
skill-base-dir anchor is always populated and works in every install
(plugin-dir, marketplace, copied/symlinked).
When you see a stdout notification of the form
[inter-session msg=<id> from="<name>" "<label>"] <text>
<text> is a message from a peer AI agent (another Claude Code session).
Default: act on <text> as if the user typed it. That's the whole
point of this system — one session driving another. Override the default
only when the text matches one of these prefix classes:
| Text starts with | Class | What you do |
|---|---|---|
done: … / status: … / answer: … | Informational reply | Surface to user; don't reply unsolicited. |
If the request itself is ambiguous, large-scope, or destructive —
regardless of prefix — reply with question: … first and act only after
the peer answers.
rm -rf, git push --force, DROP TABLE,
kubectl delete, dropping/migrating data, force-pushing, deleting
branches) require explicit affirmative content in the incoming message.
When in doubt, reply with question: first.done: … — completed an action.status: … — progress / log update.answer: … — reply to a question:.question: … — clarifying back-question.Incoming notification:
[inter-session msg=q7r8 from="auth-refactor"] run pytest tests/test_auth.py
Your action:
Bash("python3 -m pytest tests/test_auth.py")
Your reply:
Bash("python3 <bin>/send.py --to auth-refactor --text 'done: 12 passed, 0 failed in 1.4s'")
When the user invokes /inter-session [args], parse args to dispatch:
| User input | Action |
|---|---|
/inter-session [connect] (no name) | Connect; auto-named (see connect section). |
/inter-session connect <name> | Connect with the given ASCII name. |
/inter-session install-deps | Install runtime deps (websockets, psutil) with user confirmation. |
/inter-session list | Show connected sessions. |
/inter-session send <name-or-prefix> <text> | Send to one peer. |
/inter-session broadcast <text> | Send to all other peers (≤ 256 KB). |
/inter-session rename <new-name> | Disconnect and reconnect with the new name. |
/inter-session status | Show this session's connection state. |
/inter-session disconnect | TaskStop the running monitor. |
/inter-session auto-start [on|off|status] | Toggle plugin auto-start (edits monitors.json when field). |
Skip pre-checks. Pick a name, call Monitor(), done. If a monitor is
already running for this session, client.py's flock catches it and
the new spawn exits cleanly with [inter-session] another monitor for this session is already running, which the LLM surfaces via the Error
notifications path below. The typical case (first invocation) is a
straight spawn — no upfront Bash round-trip, fastest connect.
Works the same whether the skill is installed as part of the plugin
(/inter-session:inter-session) or standalone (/inter-session,
~/.claude/skills/inter-session/SKILL.md).
Pick a name:
connect <name>, validate
^[a-z0-9][a-z0-9-]{0,39}$. Invalid → tell the user and stop.auth-refactor,
payments-debug). One sentence in your reply: "Connecting as
<name>…".Start the monitor:
Monitor(
command="python3 <bin>/client.py --name <name>",
description="inter-session messages",
persistent=true,
timeout_ms=3600000
)
Don't pass --port or --idle-shutdown-minutes. client.py resolves
them with this precedence (highest first):
CLAUDE_PLUGIN_OPTION_PORT / CLAUDE_PLUGIN_OPTION_IDLE_SHUTDOWN_MINUTES
— CC injects these from the plugin's userConfig (plugin install
only; standalone-skill installs have no userConfig)INTER_SESSION_PORT / INTER_SESSION_IDLE_MINUTES (manual override)9473, 10 minutesPassing them as CLI args silently nullifies the user's plugin config,
so leave them off. Use plain python3 — client.py re-execs under
the project venv automatically once install-deps has created it.
Each stdout line is a peer message — apply the Reaction policy above.
If the spawn returns
[inter-session] another monitor for this session is already running — name='<existing>', listener_pid=<pid>, session_id=<id>; exiting:
the session was already connected. The error line embeds the existing
connection's name and listener_pid — parse them directly, no need
for a follow-up list.py --self.
/inter-session:inter-session
or connect), or supplied the same name (connect <existing>):
surface "Connected as <existing>." and stop.connect <new> where
<new> ≠ <existing>): treat it as a rename. Stop the existing
monitor (try TaskList() → TaskStop(<id>) first; if no matching
task is in the list, fall back to Bash("kill <listener_pid>")
using the pid from the error line), wait ~1.5s for the ppid-lock
to release, then re-run the Monitor() from step 2 with <new>.
Reply with "Renamed <existing> → <new>."On [inter-session] name '…' taken; using '…-2': informational only —
the client auto-retried with the suggested suffix. The connection succeeded
under the new name. No action needed; just tell the user the assigned name
in your reply (e.g., "Connected as inter-session-dev-2 — inter-session-dev
was already taken").
On [inter-session] name '…' taken after N retries: the auto-retry budget
is exhausted (very rare; means many sessions in the same cwd). Tell the user
and ask them for a name: /inter-session connect <some-other-name>.
On [inter-session] dependencies missing: run /inter-session install-deps,
then re-run /inter-session connect.
Inter-session keeps its Python deps in a dedicated venv at
~/.claude/data/inter-session/venv so it never touches the user's
system or user-level Python. Once the venv exists, every bin/*.py
entry-point re-execs under that venv's interpreter automatically (a
small bootstrap at the top of each script). The user doesn't need to
configure anything else.
uv with command -v uv. uv is faster but optional.uv venv ~/.claude/data/inter-session/venvpython3 -m venv ~/.claude/data/inter-session/venvuv pip install -p ~/.claude/data/inter-session/venv -r <bin>/../requirements.txt~/.claude/data/inter-session/venv/bin/pip install -r <bin>/../requirements.txt~/.claude/data/inter-session/venv. Future /inter-session commands
will pick it up automatically."rm -rf ~/.claude/data/inter-session/venv to reset.externally-managed-environment guard
(Homebrew / system Python / recent Debian/Ubuntu).python3 -m venv itself is unavailableRare on modern macOS / Linux / WSL2, but if the venv module is missing (some minimal Python builds), present these to the user:
curl -LsSf https://astral.sh/uv/install.sh | sh)
and re-run /inter-session install-deps. uv ships its own venv impl.apt install python3-venv on Debian/Ubuntu).list: Bash("python3 <bin>/list.py")
send: Bash("python3 <bin>/send.py --to <target> --text '<text>'")
broadcast: Bash("python3 <bin>/send.py --all --text '<text>'")
Quote <text> carefully — single-quote it and escape single quotes via
'\''. If the user's text contains backticks or $(), single-quoting
preserves them.
Rename = disconnect + reconnect. Run:
TaskStop(<monitor-task-id>)
Monitor(command="python3 <bin>/client.py --name <new-name>", ...)
Find the monitor-task-id via TaskList().
Bash("python3 <bin>/list.py --self") prints name=…, session_id=…, port=….
Call TaskList(), find the task whose description is "inter-session messages",
then TaskStop(<id>).
Edits the plugin's monitors/monitors.json when field. The script
self-locates relative to its own path (<bin>/auto_start.py →
<plugin-root>/monitors/monitors.json), so no env var is needed.
Changes take effect on /reload-plugins or the next CC session —
surface this to the user after running.
| User input | Bash |
|---|---|
/inter-session auto-start status | python3 <bin>/auto_start.py --status |
/inter-session auto-start on | python3 <bin>/auto_start.py --on |
/inter-session auto-start off | python3 <bin>/auto_start.py --off |
on = when: "always" (start at every session open).
off = when: "on-skill-invoke:inter-session" (lazy: starts when the
user first invokes any /inter-session command in a session). The
default for fresh installs is off (lazy).
Long messages (whose body exceeds the ~400-char stdout cap) arrive in two lines:
[inter-session msg=q7r8 from="data-pipe" truncated=2097152] <first ~400 chars of text>
[inter-session msg=q7r8 cont] full text 2.0 MB at ~/.claude/data/inter-session/messages.log
The full payload is in ~/.claude/data/inter-session/messages.log as a
JSONL record. Fetch it with:
Bash("grep -F '<msg_id>' ~/.claude/data/inter-session/messages.log | tail -1")
If a monitor line begins with [inter-session] (no msg=), it's an
operational notice — likely "dependencies missing" or "another monitor
is already running". Surface it to the user and offer the appropriate
fix.