New:Socket for Asana Is Now Available.Learn more
Get Started

claude-intercom

Package Overview
Dependencies
Maintainers
1
Versions
2
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

claude-intercom - npm Package Compare versions

Comparing version
2.0.0
to
2.1.0
+22
-2
bin/claude-intercom.mjs

@@ -22,2 +22,17 @@ #!/usr/bin/env node

// Claude Code signals this shim, not Bun. Unforwarded, the listener outlives
// the session: still bound to the port, still holding the secret.
const FORCE_KILL_MS = 5000
let forceKill = null
for (const signal of ['SIGTERM', 'SIGINT', 'SIGHUP']) {
process.on(signal, () => {
if (!child.killed) child.kill(signal)
if (forceKill === null) {
forceKill = setTimeout(() => child.kill('SIGKILL'), FORCE_KILL_MS)
forceKill.unref()
}
})
}
child.on('error', (err) => {

@@ -36,4 +51,9 @@ if (err.code === 'ENOENT') {

child.on('exit', (code, signal) => {
if (signal) process.kill(process.pid, signal)
else process.exit(code ?? 0)
if (forceKill !== null) clearTimeout(forceKill)
if (signal) {
process.removeAllListeners(signal)
process.kill(process.pid, signal)
} else {
process.exit(code ?? 0)
}
})
+154
-59

@@ -24,2 +24,4 @@ #!/usr/bin/env bun

*/
import { createHash, timingSafeEqual } from 'node:crypto'
import { z } from 'zod'
import { Server } from '@modelcontextprotocol/sdk/server/index.js'

@@ -35,4 +37,11 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'

/** Placeholders that ship in the docs. Never valid runtime secrets. */
const PLACEHOLDER_SECRETS = new Set([
'change-me-in-production',
'your-shared-secret',
'your-shared-secret-here',
])
/** Shared secret for authenticating messages between instances */
const SECRET = process.env.INTERCOM_SECRET || 'change-me-in-production'
const SECRET = process.env.INTERCOM_SECRET ?? ''

@@ -42,2 +51,7 @@ /** The remote machine's address (IP:port, hostname:port, or tunnel URL) */

/** A scheme in REMOTE_HOST is used as written; bare hosts get http, ngrok https */
const REMOTE_BASE = /^https?:\/\//i.test(REMOTE_HOST)
? REMOTE_HOST.replace(/\/+$/, '')
: `${REMOTE_HOST.includes('ngrok') ? 'https' : 'http'}://${REMOTE_HOST}`
/** This instance's role — appears in message tags so Claude knows who's talking */

@@ -49,5 +63,66 @@ const MY_ROLE = process.env.MY_ROLE || 'developer-a'

/** Interface to bind the listener to. Use 127.0.0.1 when a tunnel fronts it */
const HOST = process.env.INTERCOM_HOST || '0.0.0.0'
/** How long an outbound POST may hang before we give up on it */
const SEND_TIMEOUT_MS = parseInt(process.env.INTERCOM_SEND_TIMEOUT_MS || '10000', 10)
// ── Fail closed ────────────────────────────────────────────────────────
// The secret is the only thing between a stranger and a write channel into a
// live session, so it has no default. Without a real one the intercom runs
// unpaired: tools stay listed, but no port is bound and nothing can be sent.
// Claude Code passes a missing ${VAR} through as literal text rather than
// failing, which would pair both machines on the same guessable string.
const UNEXPANDED_VAR = /^\$\{[^}]*\}$/
const UNPAIRED_REASON =
SECRET === ''
? 'INTERCOM_SECRET is not set'
: UNEXPANDED_VAR.test(SECRET)
? `INTERCOM_SECRET arrived as the literal text ${SECRET}, meaning that variable was not set in the environment Claude Code was launched from`
: PLACEHOLDER_SECRETS.has(SECRET)
? 'INTERCOM_SECRET is still one of the placeholder values from the docs'
: ''
const PAIRING_ENABLED = UNPAIRED_REASON === ''
/** Reported through the tool, where whoever misconfigured it will see it. */
const UNPAIRED_MESSAGE =
`Intercom is not paired: ${UNPAIRED_REASON}. No port is being listened on and ` +
`no message can be sent. Set a strong shared secret to the same value on both ` +
`machines, e.g. INTERCOM_SECRET="$(openssl rand -base64 32)", then restart.`
// ── Authentication ─────────────────────────────────────────────────────
// !== short-circuits on the first differing byte, leaking through timing how
// much of the secret a guess got right. Hashing gives fixed-length digests,
// so timingSafeEqual needs no length check — which would leak its size.
const digest = (value: string) => createHash('sha256').update(value).digest()
const SECRET_DIGEST = digest(SECRET)
const authorized = (token: string | null): boolean =>
token !== null && timingSafeEqual(digest(token), SECRET_DIGEST)
// ── Inbound payload ────────────────────────────────────────────────────
// This ends up in a live conversation, so it is parsed rather than cast.
// `role` and `id` are rendered into the <channel ...> wrapper and must not
// carry characters that could close it; `content` needs a ceiling so one POST
// cannot flood the session. Role punctuation stays legal: `backend/api`.
// Tag punctuation, plus Unicode "Other": controls, zero-width, bidi overrides.
const SAFE_TEXT = /^[^<>"'\p{C}]+$/u
const MACHINE_ID = /^[A-Za-z0-9_-]{1,64}$/
const MAX_CONTENT_CHARS = 32_000
const MAX_BODY_BYTES = 64 * 1024
const InboundMessage = z.object({
id: z.string().regex(MACHINE_ID).optional(),
replyTo: z.string().regex(MACHINE_ID).optional(),
content: z.string().min(1).max(MAX_CONTENT_CHARS),
role: z.string().min(1).max(48).regex(SAFE_TEXT),
timestamp: z.string().max(64).regex(SAFE_TEXT).optional(),
})
// ── Delivery state ─────────────────────────────────────────────────────

@@ -125,3 +200,3 @@ // A POST returning 200 only proves the remote *process* took the message. It

const mcp = new Server(
{ name: 'intercom', version: '2.0.0' },
{ name: 'intercom', version: '2.1.0' },
{

@@ -216,2 +291,9 @@ capabilities: {

if (!PAIRING_ENABLED) {
return {
content: [{ type: 'text' as const, text: UNPAIRED_MESSAGE }],
isError: true,
}
}
const id = crypto.randomUUID().slice(0, 8)

@@ -229,7 +311,3 @@ const budgetMs = parseBudget(expectReplyWithin)

try {
// Auto-detect protocol: tunnel URLs need HTTPS, direct IPs use HTTP
const protocol =
REMOTE_HOST.includes('ngrok') || REMOTE_HOST.includes('https') ? 'https' : 'http'
const resp = await fetch(`${protocol}://${REMOTE_HOST}/message`, {
const resp = await fetch(`${REMOTE_BASE}/message`, {
method: 'POST',

@@ -336,64 +414,81 @@ headers: {

Bun.serve({
port: PORT,
hostname: '0.0.0.0',
async fetch(req) {
const url = new URL(req.url)
const handleRequest = async (req: Request): Promise<Response> => {
const url = new URL(req.url)
// Health check — useful for verifying the tunnel/connection
if (req.method === 'GET' && url.pathname === '/health') {
return new Response(
JSON.stringify({ status: 'ok', role: MY_ROLE, version: '2.0.0' }),
{ headers: { 'Content-Type': 'application/json' } },
)
// Health check — useful for verifying the tunnel/connection
if (req.method === 'GET' && url.pathname === '/health') {
return new Response(JSON.stringify({ status: 'ok', role: MY_ROLE, version: '2.1.0' }), {
headers: { 'Content-Type': 'application/json' },
})
}
// Message endpoint — receives messages from the other instance
if (req.method === 'POST' && url.pathname === '/message') {
// Authenticate: reject messages without the correct shared secret
if (!authorized(req.headers.get('X-Intercom-Secret'))) {
return new Response('Unauthorized', { status: 401 })
}
// Message endpoint — receives messages from the other instance
if (req.method === 'POST' && url.pathname === '/message') {
// Authenticate: reject messages without the correct shared secret
const token = req.headers.get('X-Intercom-Secret')
if (token !== SECRET) {
return new Response('Unauthorized', { status: 401 })
}
let body: unknown
try {
body = await req.json()
} catch {
return new Response('Bad Request: body is not valid JSON', { status: 400 })
}
const data = (await req.json()) as {
id?: string
replyTo?: string
content: string
role: string
timestamp: string
}
const parsed = InboundMessage.safeParse(body)
if (!parsed.success) {
const issue = parsed.error.issues[0]
const where = issue?.path.join('.') || 'body'
return new Response(`Bad Request: ${where} ${issue?.message ?? 'is invalid'}`, {
status: 400,
})
}
const data = parsed.data
// If this answers something we sent, close that loop out.
if (data.replyTo) {
const original = outbound.get(data.replyTo)
if (original) {
original.status = 'answered'
original.answeredAt = Date.now()
}
// If this answers something we sent, close that loop out.
if (data.replyTo) {
const original = outbound.get(data.replyTo)
if (original) {
original.status = 'answered'
original.answeredAt = Date.now()
}
}
// Push the message into Claude's conversation as a channel event.
// The id travels with it so Claude can set replyTo when it answers.
await mcp.notification({
method: 'notifications/claude/channel',
params: {
content: data.content,
meta: {
id: data.id,
replyTo: data.replyTo,
role: data.role,
timestamp: data.timestamp,
},
// Push the message into Claude's conversation as a channel event.
// The id travels with it so Claude can set replyTo when it answers.
await mcp.notification({
method: 'notifications/claude/channel',
params: {
content: data.content,
meta: {
id: data.id,
replyTo: data.replyTo,
role: data.role,
timestamp: data.timestamp,
},
})
},
})
return new Response('ok')
}
return new Response('ok')
}
return new Response('Not Found', { status: 404 })
},
})
return new Response('Not Found', { status: 404 })
}
console.error(`[intercom] ${MY_ROLE} listening on port ${PORT}`)
console.error(`[intercom] Remote: ${REMOTE_HOST}`)
// Unpaired binds nothing; the tools stay listed and report why they are inert.
if (PAIRING_ENABLED) {
// A chunked request carries no length for the handler to check, so the
// ceiling is enforced here, as the body arrives.
Bun.serve({
port: PORT,
hostname: HOST,
maxRequestBodySize: MAX_BODY_BYTES,
fetch: handleRequest,
})
console.error(`[intercom] ${MY_ROLE} listening on ${HOST}:${PORT}`)
console.error(`[intercom] Remote: ${REMOTE_BASE}`)
} else {
console.error(`[intercom] ${UNPAIRED_MESSAGE}`)
console.error('[intercom] Running MCP-only: no port bound, tools inert.')
}
{
"name": "claude-intercom",
"version": "2.0.0",
"version": "2.1.0",
"mcpName": "io.github.MuhammadTalhaMT/intercom",

@@ -5,0 +5,0 @@ "description": "Two-way communication bridge between Claude Code sessions using the Channels API",

+112
-53

@@ -10,42 +10,15 @@ # Claude Intercom

## Read this first: Claude Code has native cross-session messaging now
## What Intercom does that native can't
On 7 August 2026, Claude Code v2.1.224 shipped [cross-session messaging](https://code.claude.com/docs/en/cross-session-messaging) — `ListAgents` and `SendMessage`, built in, nothing to install.
Claude Code has [native cross-session messaging](https://code.claude.com/docs/en/cross-session-messaging) now. Here's where Intercom is still the answer:
**If you are on macOS or Linux and you want your own sessions to talk to each other, use the native feature.** It is better than this project in every way that matters: no setup, no shared secret, no open port, permission-aware delivery, and same-machine messages never leave your machine. This README is not going to pretend otherwise.
- **Two different people.** This is the big one. Native connects *your* sessions — the inbox is tied to your own OS user and cross-machine delivery rides your own Remote Control. A backend dev on their laptop and a frontend dev on theirs are two accounts, and native can't join them. That two-person hotline is exactly what Intercom was built for.
- **Native Windows.** Native messaging is macOS and Linux only. Intercom is Bun over HTTP and doesn't care what OS you run.
- **Your network, not Anthropic's.** Native cross-machine messages relay through Anthropic servers. Intercom POSTs straight over your own network, VPN or tunnel.
- **Any provider.** Works on Bedrock, Vertex and Foundry, and in telemetry-disabled environments, where native messaging is switched off.
- **Container to host.** Native discovery works through files on disk, so a session in a container and one on the host can't see each other. Intercom just needs a reachable port.
- **Honest delivery.** `send_message` reports `sent`, `delivered-to-process` and `answered` as three separate states, plus a `check_message` tool. You find out when the other side never picked it up, instead of planning around an answer that was never coming.
Intercom was built in March 2026, five months before that landed. What follows is an honest account of what each one covers.
If you're on macOS or Linux and you just want your own sessions talking to each other, native is simpler — use that. Everything else above is what this is for.
### What native messaging covers
| | |
|---|---|
| Your own sessions, same machine | Yes — over a per-session socket, never through Anthropic servers |
| Your own sessions, your other machines | Yes — requires [Remote Control](https://code.claude.com/docs/en/remote-control) on both ends; messages travel through Anthropic servers. Starting a conversation needs v2.1.225+ |
| Your Claude Code on the web sessions | Yes — through Anthropic servers |
| Subagents and agent teams | Yes — same `SendMessage` tool |
### What native messaging does not cover
These are the cases where Intercom is still the answer:
1. **Native Windows.** Cross-session messaging is macOS and Linux only, including Linux inside WSL 2. Anthropic states plainly that it is not offered on native Windows. Intercom is Bun over HTTP and does not care what OS you run.
2. **Two different people.** This is the big one, and it is architectural rather than a gap waiting to be filled. Native messaging connects *your* sessions — the inbox socket is restricted to your own OS user, and cross-machine delivery rides *your* Remote Control connection. A backend dev on their laptop and a frontend dev on theirs are two accounts, and native messaging does not join them. That two-person hotline is the use case Intercom was written for.
3. **Cross-machine without an Anthropic round trip.** Native cross-machine messages relay through Anthropic servers. Intercom POSTs directly over your own network, VPN, or tunnel.
4. **Non-Anthropic providers.** Native messaging is unavailable on Amazon Bedrock, Claude Platform on AWS, Google Cloud's Agent Platform, and Microsoft Foundry.
5. **Telemetry-disabled environments.** The feature depends on feature-flag evaluation, so `CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC`, `DISABLE_TELEMETRY`, `DO_NOT_TRACK`, or `DISABLE_GROWTHBOOK` turn it off.
6. **Container to host.** Same-machine discovery works through files on disk, so a session inside a container and one on the host cannot see each other. Intercom just needs a reachable port.
### Which should you use
```
Your own sessions, macOS/Linux -> native cross-session messaging
Two different developers -> Intercom
Native Windows -> Intercom
Bedrock / Vertex / Foundry -> Intercom
Must not transit Anthropic servers -> Intercom
```
One more difference worth knowing: native messaging is plain text between Claudes and applies the receiving session's own permission rules and inbound controls. Intercom is a raw pipe with a shared secret — anyone holding your secret and address can push text straight into your session. Read [Security](#security) before you expose a port.
## Why?

@@ -79,2 +52,57 @@

## Set it up with Claude
Paste this into a Claude Code session on each machine and it will walk you
through the whole thing. Run it on machine A first, keep the secret it gives
you, then run it on machine B.
````text
Set up claude-intercom on this machine so this Claude Code session can message
a Claude Code session on my other machine.
Work through this in order, and stop and ask me whenever you need something
only I can tell you.
1. Check prerequisites. Confirm `bun --version` works and that `claude --version`
is 2.1.80 or newer. If Bun is missing, tell me how to install it for my OS
and stop there.
2. Work out how the two machines will reach each other. Run `tailscale ip -4`.
If that returns an address we'll use Tailscale. If Tailscale isn't installed,
tell me it's the recommended option and ask whether I want to install it or
use something else.
3. Ask me these, one at a time:
- a short role name for THIS machine, e.g. "frontend", "vps", "laptop"
- the address of the OTHER machine (hostname or IP)
- whether this is the first machine I'm setting up or the second
4. Handle the shared secret.
- First machine: generate a strong random secret, show it to me once, and
tell me I'll need it when I run this on the other machine.
- Second machine: ask me to paste the secret from the first one.
Both machines must end up with exactly the same secret.
5. Write `.mcp.json` in this project with an "intercom" server: command `npx`,
args `["-y", "claude-intercom"]`, and env `MY_ROLE`, `REMOTE_HOST` (the other
machine plus `:8788`), `INTERCOM_SECRET`, `INTERCOM_PORT` set to 8788.
If `.mcp.json` already exists, merge the intercom entry into it instead of
overwriting the file. Make sure `.mcp.json` is gitignored, it holds the secret.
6. Tell me to restart Claude Code on this machine with:
`claude --dangerously-load-development-channels server:intercom`
and explain that the flag is needed because the channels capability is still
experimental, and that messages only arrive while both sides are running.
7. Once both machines are up, tell me to test it by asking one session to send
a message to the other. Remind me that `send_message` reports sent /
delivered-to-process / answered separately, so "delivered" means the remote
process accepted it, not that the other Claude has read it.
Don't invent configuration keys. The only environment variables are MY_ROLE,
REMOTE_HOST, INTERCOM_SECRET, INTERCOM_PORT and INTERCOM_SEND_TIMEOUT_MS.
````
Prefer to do it by hand? The manual steps are below.
## Quick Start

@@ -84,4 +112,9 @@

On **both machines**:
Nothing to install. `npx` fetches it on first run, on both machines.
You do need [Bun](https://bun.sh) on your PATH — the server uses `Bun.serve` for its HTTP listener, and the `npx` entry point hands off to it.
<details>
<summary>Prefer to run from source?</summary>
```bash

@@ -93,6 +126,24 @@ git clone https://github.com/MuhammadTalhaMT/claude-intercom.git

Then use `"command": "bun"` with `"args": ["/path/to/claude-intercom/intercom.ts"]` in the config below instead of the `npx` form.
</details>
### 2. Configure
Copy the example config into your project's `.mcp.json`:
Copy the example config into your project's `.mcp.json`.
First generate a secret, and use the **same value on both machines**:
```bash
openssl rand -base64 32
```
Then export it in the shell you launch Claude Code from, rather than typing it into the config:
```bash
export INTERCOM_SECRET='the-value-you-just-generated'
```
`.mcp.json` is a project file, and project files get committed. [Claude Code expands `${VAR}` in `.mcp.json`](https://code.claude.com/docs/en/mcp#environment-variable-expansion-in-mcp-json) — in `command`, `args`, `env`, `url` and `headers` — so the config below can be checked in and shared with your teammate while the secret itself never leaves your environment.
**Machine A** (e.g. backend — static IP or VPS):

@@ -104,8 +155,8 @@

"intercom": {
"command": "bun",
"args": ["/path/to/claude-intercom/intercom.ts"],
"command": "npx",
"args": ["-y", "claude-intercom"],
"env": {
"MY_ROLE": "backend",
"REMOTE_HOST": "MACHINE_B_IP:8788",
"INTERCOM_SECRET": "your-shared-secret",
"INTERCOM_SECRET": "${INTERCOM_SECRET}",
"INTERCOM_PORT": "8788"

@@ -124,8 +175,8 @@ }

"intercom": {
"command": "bun",
"args": ["/path/to/claude-intercom/intercom.ts"],
"command": "npx",
"args": ["-y", "claude-intercom"],
"env": {
"MY_ROLE": "frontend",
"REMOTE_HOST": "MACHINE_A_IP:8788",
"INTERCOM_SECRET": "your-shared-secret",
"INTERCOM_SECRET": "${INTERCOM_SECRET}",
"INTERCOM_PORT": "8788"

@@ -162,3 +213,3 @@ }

This is strictly better than exposing a port to the internet: no public listener, no port forwarding, the address doesn't change when your ISP reassigns your IP, and device identity is enforced by Tailscale rather than resting entirely on a shared string. Set `hostname` to `127.0.0.1` in `Bun.serve` if you want to be certain nothing outside the tailnet can reach it at all.
This is strictly better than exposing a port to the internet: no public listener, no port forwarding, the address doesn't change when your ISP reassigns your IP, and device identity is enforced by Tailscale rather than resting entirely on a shared string. Set `INTERCOM_HOST` to your tailnet address — or to `127.0.0.1` if you are also fronting it with a tunnel — to be certain nothing outside can reach it at all.

@@ -179,4 +230,6 @@ <details>

The intercom auto-detects ngrok URLs and switches to HTTPS. Note this does put a publicly reachable endpoint in front of your Claude session, gated only by the shared secret — pick a strong one.
ngrok hostnames are detected and switched to HTTPS automatically. For any other tunnel — Cloudflare, Caddy, a reverse proxy of your own — write the scheme into `REMOTE_HOST` explicitly (`https://your-host`), or the secret goes out over cleartext HTTP.
Note this does put a publicly reachable endpoint in front of your Claude session, gated only by the shared secret — pick a strong one, and consider `INTERCOM_HOST=127.0.0.1` so only the tunnel can reach the listener.
</details>

@@ -217,5 +270,6 @@

| `MY_ROLE` | Yes | `developer-a` | Label for this instance (appears in message tags) |
| `REMOTE_HOST` | Yes | `localhost:8789` | Address of the other machine (`host:port` or tunnel URL) |
| `INTERCOM_SECRET` | Yes | `change-me-in-production` | Shared secret — must match on both sides |
| `REMOTE_HOST` | Yes | `localhost:8789` | Address of the other machine (`host:port` or tunnel URL). Include `https://` for any TLS tunnel that isn't ngrok |
| `INTERCOM_SECRET` | Yes | *none* | Shared secret — must match on both sides. There is no default: unset, left as a docs placeholder, or an unexpanded `${VAR}`, and the intercom refuses to pair |
| `INTERCOM_PORT` | No | `8788` | Port to listen on for incoming messages |
| `INTERCOM_HOST` | No | `0.0.0.0` | Interface to bind the listener to. Use `127.0.0.1` when a tunnel fronts it |
| `INTERCOM_SEND_TIMEOUT_MS` | No | `10000` | How long an outbound POST may hang before giving up |

@@ -237,6 +291,9 @@

- **Shared secret authentication**: Every message requires an `X-Intercom-Secret` header matching the configured secret. Requests without it get a `401 Unauthorized`.
- **Shared secret authentication**: `POST /message` requires an `X-Intercom-Secret` header matching the configured secret, compared in constant time. Anything else gets a `401 Unauthorized`. `GET /health` is deliberately *not* authenticated, so you can verify a tunnel end to end — it reports this instance's role and version to anyone who asks, so treat a reachable intercom as discoverable.
- **The secret is only as private as the transport**: it is sent as a plaintext header on every message. Over Tailscale (WireGuard) or an HTTPS tunnel that is fine. Over plain HTTP on a shared network, anyone on the path can read it and then use it.
- **No replay protection**: messages carry an `id` and a `timestamp`, but neither is checked for freshness or reuse. Someone who captures a single authenticated request on a cleartext link can resend it verbatim, as often as they like.
- **No default secret**: `INTERCOM_SECRET` has no fallback value. Leave it unset, leave a docs placeholder in place, or reference a `${VAR}` you never exported, and the intercom starts *unpaired* — it binds no port and `send_message` refuses, explaining why. The unexpanded-`${VAR}` case matters because Claude Code passes a missing variable through as literal text, which would otherwise give both machines the same guessable secret.
- **Inbound is treated as data, not instructions**: the server tells the receiving Claude that a channel message comes from another person's session — it can't approve anything, can't change configuration, a slash command in the text is inert, and requests for credentials or env files should be refused and surfaced to you.
- **No data persistence**: Messages are forwarded in real-time and not stored.
- **Localhost binding optional**: By default listens on `0.0.0.0` for cross-machine access. Set to `127.0.0.1` if using a tunnel.
- **Configurable bind address**: By default listens on `0.0.0.0` for cross-machine access. Set `INTERCOM_HOST=127.0.0.1` when a tunnel is doing the reaching, so only the tunnel can connect.

@@ -266,2 +323,4 @@ > **Warning**: Those instructions are a default, not a boundary. You cannot fix prompt injection with prompt instructions — anyone holding your secret and address can put text into your Claude session, and the only real limits are that session's own permission prompts. Native cross-session messaging enforces this properly with hold/accept/refuse inbound controls; this does not. Use a strong secret, keep it off the public internet, and don't pair with a peer you wouldn't hand a terminal to.

The body is schema-validated before anything reaches your session. `content` is required and capped at 32,000 characters; `id` and `replyTo` must look like machine ids (`[A-Za-z0-9_-]`, ≤64); `role` and `timestamp` may not contain `< > " '` or any Unicode control, zero-width, or bidi-override character, because those are what a sender would use to forge the `<channel …>` wrapper the message is rendered inside. Anything else gets a `400`, and a body over 64 KB gets a `413`.
## Use Cases

@@ -278,5 +337,5 @@

<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/image?repos=MuhammadTalhaMT/claude-intercom&type=date&theme=dark&legend=top-left" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/image?repos=MuhammadTalhaMT/claude-intercom&type=date&legend=top-left" />
<img alt="Star History Chart" src="https://api.star-history.com/image?repos=MuhammadTalhaMT/claude-intercom&type=date&legend=top-left" />
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=MuhammadTalhaMT/claude-intercom&type=date&theme=dark&legend=top-left&sealed_token=Ud1kanPppbKNYxVWeWhXNaje8aO3qowksrsjC_x6Sn4DVP0vraT_UzrNbPv42LNKmb0eXgr3Pfr0dGcHQ_5lOaTHorFij5eh6OOHQgzUqipwB820zlkP9OinKDs7wQ8cq3XHmSn-UHENiXFeBK_PG5wA88RHmopqKlySA4Bd4BPhD4_daeYqAcKXvh-I" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=MuhammadTalhaMT/claude-intercom&type=date&legend=top-left&sealed_token=Ud1kanPppbKNYxVWeWhXNaje8aO3qowksrsjC_x6Sn4DVP0vraT_UzrNbPv42LNKmb0eXgr3Pfr0dGcHQ_5lOaTHorFij5eh6OOHQgzUqipwB820zlkP9OinKDs7wQ8cq3XHmSn-UHENiXFeBK_PG5wA88RHmopqKlySA4Bd4BPhD4_daeYqAcKXvh-I" />
<img alt="Star History Chart" src="https://api.star-history.com/chart?repos=MuhammadTalhaMT/claude-intercom&type=date&legend=top-left&sealed_token=Ud1kanPppbKNYxVWeWhXNaje8aO3qowksrsjC_x6Sn4DVP0vraT_UzrNbPv42LNKmb0eXgr3Pfr0dGcHQ_5lOaTHorFij5eh6OOHQgzUqipwB820zlkP9OinKDs7wQ8cq3XHmSn-UHENiXFeBK_PG5wA88RHmopqKlySA4Bd4BPhD4_daeYqAcKXvh-I" />
</picture>

@@ -283,0 +342,0 @@ </a>

@@ -9,3 +9,3 @@ {

},
"version": "2.0.0",
"version": "2.1.0",
"packages": [

@@ -15,3 +15,3 @@ {

"identifier": "claude-intercom",
"version": "2.0.0",
"version": "2.1.0",
"transport": {

@@ -37,6 +37,7 @@ "type": "stdio"

"name": "INTERCOM_SECRET",
"description": "Shared secret authenticating messages between the two instances. Must match on both sides.",
"description": "Shared secret authenticating messages between the two instances. Must match on both sides. There is no default: without it the intercom starts unpaired, binds no port, and refuses to send.",
"isRequired": true,
"isSecret": true,
"format": "string"
"format": "string",
"placeholder": "output of: openssl rand -base64 32"
},

@@ -51,2 +52,9 @@ {

{
"name": "INTERCOM_HOST",
"description": "Interface to bind the listener to. Defaults to 0.0.0.0. Set 127.0.0.1 when a tunnel fronts the intercom, so only the tunnel can connect.",
"isRequired": false,
"isSecret": false,
"format": "string"
},
{
"name": "INTERCOM_SEND_TIMEOUT_MS",

@@ -53,0 +61,0 @@ "description": "How long an outbound POST may hang before giving up. Defaults to 10000.",