@agent-ops/claude-nats-channel
v0.8.0
Published
NATS channel bridge for Claude Code — exposes a Claude Code session as a discoverable, spec-compliant Synadia Agent Protocol agent on agents.prompt.<machine>.<project>.<session>.
Readme
NATS Channel for Claude Code
Connect Claude Code to NATS messaging as a spec-compliant Synadia Agent Protocol for NATS agent.
The MCP server registers an agents micro service, exposes a
prompt endpoint at agents.prompt.<machine>.<project>.<session>, a status
endpoint at agents.status.<machine>.<project>.<session> (replies with the same
payload as a heartbeat), publishes heartbeats at
agents.hb.<machine>.<project>.<session> (the verb is the abbreviation hb),
and bridges prompt requests into the Claude Code session.
Harness identity (claude-code) lives in the service metadata (agent key),
not in the subject.
Replies stream back as typed JSON chunks
({"type":"response","data":"..."}) terminated by an empty headerless
message - the protocol's uniform end-of-stream signal.
Prerequisites
- Bun - the MCP server runs on Bun. Install with
curl -fsSL https://bun.sh/install | bash, and make surebunis on yourPATH. - NATS CLI - for managing contexts and testing.
- A NATS server to connect to (local or remote) - the plugin defaults to
demo.nats.io.
Quick Setup
1. Add the marketplace.
These are Claude Code commands - run claude to start a session first.
/plugin marketplace add synadia-ai/synadia-agents2. Install the plugin.
/plugin install nats-channel@synadia-plugins3. Launch with the channel flag.
claude --dangerously-load-development-channels plugin:nats-channel@synadia-pluginsBy default, the server connects to demo.nats.io (no credentials required)
and registers a micro service on agents.prompt.<machine>.<project>.<session>,
where <session> defaults to the working directory name.
4. (Optional) Configure the channel.
The /nats-channel:configure skill manages connection, session naming,
and permissions. All state lives in ~/.claude/channels/nats/config.json.
| Command | Description |
| --- | --- |
| /nats-channel:configure | Show current config, list available contexts, and offer to switch |
| /nats-channel:configure list | List available NATS CLI contexts |
| /nats-channel:configure <context-name> | Select a NATS CLI context to connect to |
| /nats-channel:configure session <name> | Override the session name (the <session> token in agents.prompt.<machine>.<project>.<session>) |
| /nats-channel:configure session clear | Remove session name override, revert to CWD basename |
| /nats-channel:configure permissions terminal | Prompt for permissions in the terminal (default) |
| /nats-channel:configure permissions query | Relay permission prompts as protocol query chunks |
| /nats-channel:configure permissions clear | Reset permissions to default |
| /nats-channel:configure clear | Remove all configuration |
To connect to your own NATS server, use a NATS CLI context. List your contexts
with nats context ls, then:
/nats-channel:configure <context-name>This writes the selected context to ~/.claude/channels/nats/config.json.
The server reads connection details (URL, credentials) from
~/.config/nats/context/<name>.json.
5. Send a prompt.
With the @synadia-ai/agents
TypeScript SDK:
import { connect } from "@nats-io/transport-node";
import { Agents } from "@synadia-ai/agents";
const nc = await connect({ servers: "nats://localhost:4222" });
const agents = new Agents({ nc });
const [agent] = await agents.discover();
for await (const msg of await agent!.prompt("hello Claude")) {
if (msg.type === "response") process.stdout.write(msg.text);
}
await agents.close();
await nc.close();Or directly via the NATS CLI (plain-text shorthand per spec §5.1):
nats req agents.prompt.<machine>.<project>.<session> "Hello Claude" \
--replies=0 --reply-timeout=30s --timeout=90sClaude's response streams back as typed JSON chunks on the reply subject; an empty headerless message signals completion.
Launching claude-code with the channel enabled
If you launch Claude Code via /plugin install (Quick Setup steps 1-3), the
plugin marketplace pre-registers nats-channel as an approved channel and
the inbound notification path "just works".
If you launch Claude Code directly with --mcp-config pointing at this
server's config (e.g., in containerized / CI environments), you must
also pass one of:
--channels <server-name>— production mode; requires claude.ai authentication and the channel name to be on your org'sallowedChannelPluginsallowlist (managed settings).--dangerously-load-development-channels <server-name>— local-dev mode; shows a one-time confirmation dialog at startup.
<server-name> is the key under mcpServers in your --mcp-config
file (e.g., nats for the standard installation).
Without either flag, the channel's MCP notifications/claude/channel
notifications are silently dropped by Claude Code's channel gate, and
the model never starts a turn in response to an inbound NATS prompt —
the channel still emits its §6.4 ack chunks, so callers see acks but no
response, until they time out.
This requirement is enforced inside the claude-code binary at the
tengu_mcp_channel_gate telemetry boundary; the MCP server's
experimental.claude/channel capability handshake (declared in
server.ts:586-589) is necessary but not sufficient.
v0.4 (A2A parity) advertisement
The channel advertises v0.4 capabilities via service metadata
(sesh.protocol_version: "0.4", sesh.v04_capabilities:
"messages,artifacts,cards") and registers these NATS micro endpoints, all
keyed on the clean machine-rooted scheme
(agents.<verb>.<machine>.<project>.<session>):
agents.prompt.<machine>.<project>.<session>— accepts an A2A Message envelope, drives Claude via the same MCP pipeline, and (when JetStream KV / Object Store buckets exist) durably appends Messages and publishes per-task stream events onagents.task.stream.*.agents.card.<machine>.<session>.<name>— public L3 AgentCard partial.agents.cardx.<machine>.<session>.<name>— auth-gated L3 AgentCard partial (the same payload — claude-nats-channel does not distinguish scope today; the shim re-applies access control upstream).
The clean v0.4 subject scheme is the only scheme: subjects are machine-rooted
(agents.prompt.<machine>.<project>.<session>) and harness identity
lives in service metadata (agent key), never in the subject.
KV/Object-Store wiring is best-effort: when buckets are not provisioned
(e.g. running against demo.nats.io), the v2 handler still replies on
the inbox; only the durable Message/Artifact writes are skipped, with a
single startup log line per missing surface.
Protocol compliance
This plugin implements the Synadia Agent Protocol for NATS v0.3 end-to-end:
- Registers as an
agentsNATS micro service (§3.1 - the bare subject-safe token). - Service metadata includes
agent,owner,session, andprotocol_version: "0.3"(§3.2). promptendpoint declares the server-negotiatedmax_payload(read fromnc.info.max_payloadat startup and formatted into the §2.1\d+(B|KB|MB|GB)grammar —1MBagainst a defaultnats-server, larger if the operator bumped--max_payload),attachments_ok: "true"(§2.1), and queue group"agents"(§3.3).- Accepts both plain-text shorthand and JSON envelopes with optional base64-encoded attachments (§5.1, §5.2, §5.3). Inbound attachments are staged to a per-request temp directory and exposed to Claude via file paths.
- Rejects malformed envelopes, empty payloads, oversize requests, and
invalid base64 with
Nats-Service-Error-Code: 400(§9). - Emits typed response chunks
{"type":"response","data":"..."}(§6.3) terminated by an empty headerless message (§6.5). Large responses are split into multiple UTF-8-safe chunks that each fit undermax_payload. - Publishes periodic
{"type":"status","data":"ack"}keep-alives (§6.4) every 30 s while a request is open, resetting the caller's 60-second inactivity timeout. - Publishes heartbeats at
agents.hb.<machine>.<project>.<session>(§8.1) every 5 s with the full §8.3 payload includinginstance_id(§8). - Relays Claude Code permission prompts as mid-stream
querychunks (§7) whenpermissions.mode = query.
The caller-side SDK at
client-sdk/typescript/ is the
canonical counterpart.
Session names
The micro service prompt subject is agents.prompt.<machine>.<project>.<session> (clean v0.4 scheme; identity lives in metadata, not the subject). Heartbeats go to agents.hb.<machine>.<project>.<session> and the status endpoint replies on agents.status.<machine>.<project>.<session>.
The <project> token is the 40-char lowercase-hex SESH_PROJECT_ID pin when set (the canonical, collision-resistant project key; sesh up / sesh ≥ v0.6.0 inject it). When the pin is absent — e.g. launched outside sesh up — it falls back to the sanitized SESH_PROJECT slug. A pin set to a malformed value fails loudly at startup rather than silently misdelivering. The human-readable slug is display metadata; resolve a peer's subject via nats__resolve rather than hand-building it from the slug.
The <session> token resolves as:
- Default: sanitized basename of the working directory (e.g.,
my-project) - Override: set
NATS_SESSION_NAMEenv var, or use/nats-channel:configure session <name> - Multiple sessions: if the default name is already taken by another
claude-code instance owned by the same user, the plugin auto-appends
-2,-3, etc.
Discover running sessions via the protocol's discovery subjects:
nats req '$SRV.INFO.agents' '' --replies=0 --timeout=2s
nats req '$SRV.PING.agents' '' --replies=0 --timeout=2sOr via the NATS Micro CLI:
nats micro ls
nats micro info agentsStrict mode
NATS_CHANNEL_STRICT=1 makes the channel refuse to auto-suffix on
(agent, owner, name) collision. By default (env var unset), a second
instance trying to register the same triple as a live one auto-renames to
<name>-2, <name>-3, etc. — useful for multiple parallel sessions, but
problematic when a misconfiguration accidentally spawns a duplicate (the
phantom is silent rather than loud).
Use strict mode when:
- You're running claude-nats-channel in a containerized or unattended environment where a phantom duplicate registration should be a loud failure (exit code 2).
- You've co-located claude-code with Oh My Pi, and OMP's
mcp.discoveryMode: truemay autoload claude's.mcp.json— strict mode catches the resulting double-spawn before the bus sees a phantom<name>-2instance.
To opt back out (re-enable auto-suffixing), unset the env var or set
NATS_CHANNEL_STRICT=0.
Tools exposed to the assistant
| Tool | Purpose |
| --- | --- |
| reply | Send a response over NATS. Takes request_id + text. The server wraps the text in a {"type":"response","data":...} chunk. Set done=false for intermediate replies; done=true (default) emits the empty-body terminator. |
Permissions
When Claude Code needs permission to run a tool, the plugin can either
prompt in the terminal (default) or relay the request as a protocol
query chunk on the active NATS stream. This is controlled by the
permissions config.
Terminal mode (default)
Permission prompts appear directly in the Claude Code terminal. No extra configuration needed.
Query mode
Permission requests are emitted as {"type":"query","data":{...}}
chunks on the active stream's reply subject (spec §7). The caller
replies on the query's dynamic _INBOX with yes/no, and the plugin
forwards the decision back to the harness.
/nats-channel:configure permissions queryTo switch back to terminal mode:
/nats-channel:configure permissions terminalThe legacy value "nats" is still accepted as an alias for "query" so
old configs keep working. The older permissions.subject override field
has been removed - query chunks always use a fresh NATS inbox per
request.
If Claude asks for permission while no NATS request is active (for
example from direct terminal input), the plugin denies by default in
query mode; use permissions terminal instead if you want interactive
approval in that case.
Handling permission queries with the SDK
for await (const msg of await remote.prompt("rm -rf /tmp/stale")) {
if (msg.type === "query") {
await msg.reply("yes"); // or "no"
}
if (msg.type === "response") {
process.stdout.write(msg.text);
}
}Or with the NATS CLI, by publishing to the reply_subject from the
query chunk:
nats pub _INBOX.Xj7k9Q2pA "yes"If no reply is received within 2 minutes, the permission defaults to deny.
Access control
NATS server authentication and authorization handle access control. If a
user can connect and publish to agents.prompt.<machine>.<project>.<session>, they can
interact with Claude. No additional pairing or allowlist is needed.
Anthropic auth
Set ANTHROPIC_API_KEY in your environment before launching claude.
Claude Code uses the env var in preference to any ~/.claude/
credentials, so logging out is unnecessary.
Bedrock / Vertex / Azure deployments work too — set the standard
provider env vars before launching claude and Claude Code will use
those instead.
Configuration
State lives in ~/.claude/channels/nats/:
| File | Purpose |
| --- | --- |
| config.json | Selected NATS context, session name override, and permission settings |
| attachments/<request_id>/ | Per-request staged attachments; auto-cleaned on reply completion |
NATS CLI contexts live in ~/.config/nats/context/<name>.json.
config.json
{
"context": "my-context",
"sessionName": "my-session",
"permissions": {
"mode": "query"
}
}| Field | Default | Description |
| --- | --- | --- |
| context | (none - uses demo.nats.io) | NATS CLI context name |
| sessionName | CWD basename | Override the session name |
| permissions.mode | terminal | terminal or query (nats accepted as legacy alias for query) |
Environment variables
| Variable | Overrides | Default |
| --- | --- | --- |
| NATS_SESSION_NAME | Session name (the <session> token in agents.prompt.<machine>.<project>.<session>) | sanitized basename of $CLAUDE_CWD |
| NATS_STATE_DIR | State directory location | ~/.claude/channels/nats |
| NATS_CHANNEL_STRICT | When 1, refuse to auto-suffix on (agent, owner, name) collision (exit 2). See Strict mode. | unset (auto-suffix) |
| SESH_ROLE | Free-form role token (^[a-z0-9_-]+$, 1–63 chars). Identifies the function this agent plays in the swarm — e.g. implementer, verifier, spy. Surfaced as metadata.role on the NATS Micro service and as role in sesh's session manifest. | worker |
| SESH_CLASS | One of active or observer. Coordination-subject routing keys on this: active agents subscribe to workers.*, observer agents subscribe to spies.*. | active |
