@chinchillaenterprises/mcp-claude-channel
v1.1.0
Published
Lane-to-lane messaging MCP. SEND: one tool send_to_lane — append a message to a target lane's side-door inbox (chan-<target>-in.jsonl). LIVE-RECEIVE (opt-in via CHANNEL_INJECT=1): tail this lane's own inbox and emit each new line as a notifications/claude
Maintainers
Readme
@chinchillaenterprises/mcp-claude-channel
Lane-to-lane Claude Code side-door messaging. Always exposes one tool,
send_to_lane, that appends a message to a target lane's inbox file. Optionally
(opt-in) it also runs a live-receive injector that tails this lane's own inbox
and pushes each new line into the live session via the official Channels
notification (notifications/claude/channel).
The push is a doorbell, not the payload. The durable source of truth is the receiving lane's own file poll of its inbox.
What it's for
Echelon's "fused line": the PEP lanes (R&D · Arch · Dev · Deploy · Test) are Claude Code sessions in separate tabs. This server moves the baton lane-to-lane over plain append-only JSONL inbox files — no shared daemon, no HTTP, no Slack.
Two modes, one binary
| Mode | CHANNEL_INJECT | Behavior |
| --- | --- | --- |
| Send-only (default) | unset / empty / 0 | Only send_to_lane. NO claude/channel capability, NO tailer. The lean baseline — OMEGA runs it this way to relay work down without live-receiving lane chatter. |
| Live-receive | 1 | Everything above plus the injector: declares the claude/channel capability and tails this lane's inbox, emitting a notifications/claude/channel doorbell per new line. |
send_to_lane is 100% identical in both modes — turning the injector on never
changes the send path or its on-disk format.
Wire model (pure files)
- Inbox:
<LANE_INBOX_DIR>/chan-<LANE_NAME>-in.jsonl— one JSON object per line:{"content": "...", "meta": {...}}. send_to_lane(target, text, meta?)appends a line to the TARGET's inbox, stampingmeta.to_lane(the target) andmeta.from_lane(our own callsign).- With
CHANNEL_INJECT=1, the server tails its own inbox and pushes each new line as a doorbell.
The injector is a DOORBELL, not a delivery guarantee
Claude Code can silently drop a notifications/claude/channel push that arrives
mid-turn (while the session is busy). The live push is a latency optimization only —
consumers MUST also poll their own inbox file (chan-<lane>-in.jsonl) for the
durable, no-loss copy. Treat the doorbell as "check your inbox now," never as the
message itself.
Tailer cursor isolation (offset file)
The injector journals its byte offset to a distinct, lane-scoped file so it never collides with any other reader's cursor (the ghost-injection footgun that motivated stripping the old resident tailer out):
<inbox>.<LANE_NAME>.sidedoor-offset
# e.g. /tmp/chan-bravo-in.jsonl.bravo.sidedoor-offsetThis is deliberately not the plain .offset (the generic Tailer default) and
not .poll-cursor (other readers). The injector resumes from this file across
restarts (no-loss, no double-process) and does an immediate catch-up drain on boot.
It watches the inbox two ways: a 500 ms poll (the durable leg) and fs.watch
(the low-latency leg, best-effort — if watch throws, it falls back to poll-only).
Meta sanitization (critical — defends a known client bug)
Before emitting, every meta object is sanitized:
- Drop any key whose value is
nullorundefined. - Drop any key that is not strictly
[A-Za-z0-9_]+— the client silently drops hyphenated KEYS (e.g.baton-path). Hyphens in values are fine. - Stringify every remaining value — no nested objects, numbers, booleans, or
nulls ever reach the client (
round_id: 7→"7").
So an inbound line whose meta is
{from_lane:"omega", round_id:7, reply_thread_ts:null, "baton-path":"/x"} is emitted
as {from_lane:"omega", round_id:"7"} — null dropped, hyphenated key dropped, number
stringified.
Security — allowlist on the sending lane
Injection is gated by LANE_ALLOWED_SENDERS against meta.from_lane — the
sending lane callsign, checked on the raw value before sanitization. Only listed
lanes (e.g. the prev lane forward, Test/Deploy fail-back, the human) may inject into
a given lane's door. A line from a disallowed lane is dropped (logged to stderr).
An empty allowlist accepts any sender (dev only — set it in production). This is a
prompt-injection defense: provenance over content. Gating applies only in
live-receive mode; send-only never injects anything.
Claude Code also treats injected <channel> content as untrusted data, not
commands. For unattended send_to_lane, add mcp__claude-channel__send_to_lane to
the session's allowedTools.
Configuration (env-first, NO tokens)
| Env var | Default | Notes |
| --- | --- | --- |
| LANE_NAME | sidedoor | This lane's callsign. Also names its inbox file and the offset-file suffix. |
| LANE_INBOX_DIR | /tmp | Directory holding the chan-<lane>-in.jsonl inboxes. |
| CHANNEL_INJECT | (unset = send-only) | Set to 1 to enable the live-receive injector. Anything else (empty / 0) stays send-only. |
| LANE_ALLOWED_SENDERS | (empty = open) | Comma/space-separated callsigns allowed to inject (matched on meta.from_lane). Live-receive only. |
Offset file (live-receive only): <LANE_INBOX_DIR>/chan-<LANE_NAME>-in.jsonl.<LANE_NAME>.sidedoor-offset
Launch
Send-only (.mcp.json, the OMEGA-style relay):
{
"mcpServers": {
"claude-channel": {
"command": "npx",
"args": ["-y", "@chinchillaenterprises/mcp-claude-channel"],
"env": { "LANE_NAME": "omega" }
}
}
}Live-receive: custom/dev channel servers require the dangerous flag (plain
--channels is marketplace-only), with channelsEnabled: true in
~/.claude/settings.json:
claude --dangerously-load-development-channels server:claude-channel{
"mcpServers": {
"claude-channel": {
"command": "npx",
"args": ["-y", "@chinchillaenterprises/mcp-claude-channel"],
"env": {
"LANE_NAME": "bravo",
"CHANNEL_INJECT": "1",
"LANE_ALLOWED_SENDERS": "alpha,omega,human"
}
}
}
}Build
npm install
npm run build # tsc → dist/, chmod +x dist/index.jsPublished via the repo's OIDC publish.yml. Bump the version in both
package.json and src/index.ts (VERSION).
Roadmap
- v1.0: bare-bones send-only (
send_to_lane), file-inbox transport. - v1.1 (this): opt-in live-receive injector (
CHANNEL_INJECT=1) — cursor-isolated tailer, poll +fs.watch, meta sanitization, sender allowlist. Doorbell semantics; consumers still poll their own inbox.
