Skip to content

Optional channel transport: push app events directly into a Claude Code session #11

Description

@V3RON

Split out of #6, which now covers only the dependency-free core (per-session ring buffer, events.since RPC, cordierite_events / cordierite_wait_for_event, cordierite events --since).

Blocked on #6, and gated on a research-preview feature — see "Why this can't be the primary path" below. Filed so the design isn't lost, not as near-term work.

Idea

MCP has no generic server→client event channel, so #6's agent surface is pull-based: the agent drains the buffer or blocks on a wait tool. Claude Code has a documented mechanism that genuinely pushes into the model's context, and Cordierite is a natural fit for it — the app already emits events; there is simply nowhere for them to go unprompted.

A server declares the capability and emits a notification:

const mcp = new Server(
  { name: 'cordierite', version },
  {
    capabilities: { experimental: { 'claude/channel': {} }, tools: {} },
    instructions:
      'App events arrive as <channel source="cordierite" alias="..." event="...">. ' +
      'They are one-way telemetry from a connected device; read them and act.',
  },
)

await mcp.notification({
  method: 'notifications/claude/channel',
  params: {
    content: 'checkout_completed',
    meta: { alias: 'pixel_8', event: 'checkout_completed', screen: 'Checkout' },
  },
})

The event lands in the model's context, not merely in the UI:

<channel source="cordierite" alias="pixel_8" event="checkout_completed" screen="Checkout">
checkout_completed
</channel>

The terminal renders a one-line summary alongside it. The instructions string goes into the system prompt, so the event shape can be explained once without spending a tool description on it.

Reference: Channels and Channels reference.

Why this can't be the primary path

  1. Research preview. --channels isn't listed in claude --help, and the docs say the flag syntax and protocol contract may change on feedback.
  2. Allowlist. During the preview only Anthropic-curated plugins register as channels. A custom server needs claude --dangerously-load-development-channels server:cordierite, an org admin's allowedChannelPlugins, or a coordinated official-marketplace listing. It can't be the default behavior of cordierite mcp.
  3. Anthropic auth only — unavailable on Amazon Bedrock, Google Cloud's Agent Platform, and Microsoft Foundry. Team and Enterprise organizations must set channelsEnabled before any channel delivers.
  4. Silent drops. Per the docs, if the session hasn't loaded the server as a channel or org policy blocks it, "Claude Code drops the events silently and returns no error to your server." The daemon cannot detect that push isn't working, so it can never treat delivery as confirmed.
  5. Claude Code only. Codex, Cursor and the rest have no equivalent, so App events never reach an agent: add daemon-side retention and a pull surface for app_event #6's pull surface remains the thing that always works.

Point 4 is the structural one: this transport is strictly additive telemetry. Nothing in Cordierite may depend on an event having been delivered this way.

Prompt injection — the part that needs real design

The channels documentation is blunt that an ungated channel is a prompt-injection vector: anything emitted lands as text in front of the model. Here the "sender" is the app, and postEvent(name, payload) payloads can carry user-generated content — an in-app chat message, a push-notification body, a deep link, a server-controlled feature-flag string.

So this must not mirror every app_event by default. Options, roughly in order of preference:

  • Explicit per-event opt-in. Only events whose registration marks them as channel-safe are mirrored — e.g. postEvent(name, payload, { channel: true }), or an allowlist of event names in config.json. Default off.
  • Name-only mirroring. Push the event name and a small set of validated meta keys; never the free-form payload. Cheap and safe, and enough for "something happened, go look" — the agent can then call cordierite_events for the detail.
  • Sanitization. Weaker, and the docs' own sanitization notes for permission relay show how much work doing this properly is (direction-override characters, invisible characters, quote/bracket lookalikes, length elision).

meta keys also have a hard constraint worth encoding: letters, digits and underscores only — keys with hyphens or other characters are silently dropped. Session aliases are slugified with - today (docs/ARCHITECTURE.md §5), so alias values are fine but any key derived from an alias would need converting.

Scope

  • Declare capabilities.experimental['claude/channel'] in packages/cordierite/src/mcp/server.ts, behind an explicit opt-in (config or CLI flag) so it is never on by default.
  • Subscribe to app_event on the existing daemon event stream and mirror qualifying events as notifications/claude/channel.
  • Write the instructions string.
  • Whatever gating model is chosen above, plus documentation for app authors on what must never be put in a mirrored payload.
  • Note in the docs that this is preview-gated and how to run it (--dangerously-load-development-channels server:cordierite today).

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions