Channels (Experimental)
Experimental
The channels feature relies on a Claude Code server-side feature flag (tengu_harbor) that is not yet generally available. In -p (print/non-interactive) mode — which agentrunner uses — channels will silently fail unless this flag is enabled on your account. Channels work in interactive mode regardless of the flag. A future version will use --input-format stream-json to bypass this limitation. See anthropics/claude-code#24594.
Channels enable two-way communication with a running Claude Code session. While the agent is working, you can send it messages from external sources (CI notifications, webhook events, user input) and receive structured replies.
How It Works (Experimental)
When you enable channels, agentrunner:
- Starts an agentrunner-channel MCP server alongside the Claude CLI process
- Creates a Unix socket for your code to send messages through
- Forwards your messages to Claude as MCP channel notifications
- Claude can reply using the
replytool, which surfaces aschannel_replymessages in the stream
Your code ──send()──► Unix socket ──► MCP server ──► Claude Code
│
Your code ◄──stream── channel_reply ◄── reply tool ◄────┘Enabling Channels (Experimental)
session, err := runner.Start(ctx, "Review this PR",
claudecode.WithChannelEnabled(),
agentrunner.WithSkipPermissions(),
)const session = runner.start("Review this PR", {
channelEnabled: true,
skipPermissions: true,
});Sending Messages (Experimental)
Wait for the system init message before sending — this confirms the MCP server is connected.
import "github.com/driangle/agentrunner/go/channel"
for msg := range session.Messages {
if msg.Type == agentrunner.MessageTypeSystem {
// MCP server is ready — send a message.
err := session.Send(channel.ChannelMessage{
Content: "Build #1234 failed on main",
SourceID: "ci-build-1234",
SourceName: "GitHub Actions",
})
break
}
}for await (const msg of session.messages) {
if (msg.type === "system") {
await session.send({
content: "Build #1234 failed on main",
sourceId: "ci-build-1234",
sourceName: "GitHub Actions",
});
break;
}
}ChannelMessage Fields
| Field | Type | Description |
|---|---|---|
content | string | Message body that Claude reads |
sourceId | string | Caller-defined correlation ID |
sourceName | string | Human-readable origin (e.g. "GitHub Actions") |
replyTo | string? | Optional reference to a prior message's sourceId |
Receiving Replies (Experimental)
When Claude replies to a channel message, it emits a channel_reply message in the stream:
for msg := range session.Messages {
if msg.Type == agentrunner.MessageTypeChannelReply {
fmt.Printf("Reply to %s: %s\n",
msg.ChannelReplyDestination(),
msg.ChannelReplyContent(),
)
}
}import {
messageChannelReplyContent,
messageChannelReplyDestination,
} from "@driangle/agentrunner/claudecode";
for await (const msg of session.messages) {
if (msg.type === "channel_reply") {
const content = messageChannelReplyContent(msg);
const dest = messageChannelReplyDestination(msg);
console.log(`Reply to ${dest}: ${content}`);
}
}Channel Server Logging (Experimental)
The channel MCP server runs as a child process of Claude Code. By default it produces no log output. To enable logging for debugging, specify a log file and level:
session, err := runner.Start(ctx, "Review this PR",
claudecode.WithChannelEnabled(),
claudecode.WithChannelLogFile("/tmp/channel.log"),
claudecode.WithChannelLogLevel("debug"),
)const session = runner.start("Review this PR", {
channelEnabled: true,
channelLogFile: "/tmp/channel.log",
channelLogLevel: "debug",
});Then tail the log file to see the MCP server's activity:
tail -f /tmp/channel.logLog Levels
| Level | What it logs |
|---|---|
debug | All of the below, plus every JSON-RPC request/response and notification delivery confirmations |
info | Server start/stop, MCP handshake, channel messages forwarded, tool calls (default) |
warn | Malformed input, unknown methods/tools |
error | Failed writes, unrecoverable errors |
What the Logs Show
The channel server logs the full lifecycle of a channel session:
- Server start — socket path being listened on
- MCP handshake —
initializeandinitializedmessages from Claude Code - Channel messages — each message forwarded from your code to Claude, with source_id and content length
- Tool calls — when Claude invokes the
replytool, with the full arguments - Shutdown — clean shutdown or errors
This is useful for diagnosing issues like:
- The MCP server not starting (binary not found)
- Messages being sent but not reaching Claude (socket connected but notification not forwarded)
- Claude receiving messages but not replying (check if
replytool calls appear)
Environment Variables
When running the channel binary directly (outside of agentrunner), logging is controlled via environment variables:
| Variable | Description |
|---|---|
AGENTRUNNER_CHANNEL_LOG | File path to write logs to. No logging when unset. |
AGENTRUNNER_CHANNEL_LOG_LEVEL | Log level: debug, info (default), warn, error. |
Known Limitation: Feature Flag in -p Mode
The channels feature in Claude Code is gated behind a server-side feature flag (tengu_harbor). In interactive mode, --dangerously-load-development-channels bypasses this gate automatically. However, in -p (print/non-interactive) mode — which is what agentrunner uses — the bypass code path is skipped. This means channels currently only work in -p mode if the feature flag is enabled on your account.
If channel messages are being forwarded by the MCP server (visible in channel logs) but Claude never acts on them, this is likely the cause. You can verify by running Claude in interactive mode with the same flags — if it works interactively but not with -p, the feature flag is not enabled.
This limitation is in Claude Code itself, not in agentrunner. Check for updates to Claude Code that may remove this restriction.
Prerequisites (Experimental)
The channel feature requires the agentrunner-channel binary. It is resolved in this order:
AGENTRUNNER_CHANNEL_BINenvironment variable- Platform-specific npm optional dependency (TypeScript only)
- System
PATH - Built from source into user cache (Go only)
Full Example (Experimental)
See examples/ts/channel/main.ts for a complete working example that sends a CI notification and prints Claude's reply.