Claude Code agents

What claude-scuba is, what it bridges, and what is in (and out of) Stage 1.

Connecting agentsdocs/agents/10-claude-code.mdx
On this page

@octostaff/claude-scuba lets a Claude Code agent join Bubble threads as a first-class bot principal and participate in the conversation alongside users and other bots. It is a thin adapter on top of the Claude Agent SDK — we never reimplement Claude Code's tool execution, sub-agent orchestration, or session lifecycle.

When to Use It

Use @octostaff/claude-scuba when Claude Code should participate in Bubble threads from a local machine, developer workstation, or long-running daemon that dials into Bubble.

Use @octostaff/reef instead when Bubble needs to call out to a hosted A2A agent over HTTP.

What the package does

claude-scuba runs one bot per process. On startup it:

  1. Authenticates to a Bubble server as a configured bot principal and confirms the identity via /v1/auth/whoami.
  2. Subscribes to the principal's grants via subscribeGrantsAsIterable, then for every newly granted thread opens a WebSocket subscription after replaying the bounded history window selected by the configured backlog policy (defaults: onFirstJoin: since_joined, onResume: catch_up, maxReplay: 50). Revoked grants stop the matching thread runtime.
  3. Creates one session per active thread. Each session starts a long-lived SDK Query lazily on the first drainable user message, using streaming-input mode. The SDK's prompt is a push-style AsyncIterable that yields the next SDKUserMessage whenever the bot becomes idle and there is drainable input on that thread.
  4. Translates the SDK's output back into Bubble events (run_started / message_part / run_finished and tool-input-available / tool-output-available chunks) and appends them to the thread.
  5. Bridges Bubble events back into the SDK: new user turns become follow-up prompts, run_interrupt_requested becomes query.interrupt(), and tool_call_approval / tool_call_result events resolve pending canUseTool decisions.
  6. Routes Claude's sub-agent invocations onto Bubble subthreads so the parent thread reads as a clean conversation while each sub-agent gets its own transcript.

Supported Claude Agent SDK versions

claude-scuba bridges the inner @anthropic-ai/claude-agent-sdk. It declares a range rather than an exact pin:

text
@anthropic-ai/claude-agent-sdk: >=0.3.201 <0.4.0

Every change runs the full claude-scuba integration suite (live + fake SDK) at both ends of that range on CI — the 0.3.201 floor and the latest 0.3.x — so the whole range is continuously supported, not tested at a single point in time.

Inner SDKStatusNotes
0.2.xUnsupportedBelow the declared floor. Its batch TodoWrite plan tool is still normalized by the bridge, but the combination is no longer exercised.
0.3.x (≥ 0.3.201)Supported (default)Granular TaskCreate/TaskUpdate/TaskList plan tools + system:thinking_tokens telemetry — the bridge normalizes both tool shapes to the same neutral plan and drops the telemetry noise.
0.4.x+UntestedOutside the declared range; may work but is not exercised.

Both plan-tool shapes render identically to Bubble clients (a neutral todo_list part), so the bridge stays agnostic about which one an inner SDK emits.

From 0.3.268, Claude Code offers these plan tools only on Claude 3.x, Opus 4.0–4.7, Sonnet 4.0–4.6, and Haiku 4.5. With a newer model the agent has no plan tool, so no plan appears in the thread. To keep plans, set CLAUDE_CODE_ENABLE_TODO_TOOLS=1 in the environment claude-scuba runs in.

Pinning a custom inner SDK version

The SDK is resolved by normal Node module resolution at the install location and loaded lazily at runtime, so you can override it from the outside — no fork needed:

  • Consumer app: declare your own exact @anthropic-ai/claude-agent-sdk dependency (any version in range), or force one tree-wide with an npm/yarn/pnpm overrides / resolutions entry.
  • Docker: use a version-pinned image tag — ghcr.io/octostaff/claude-scuba:<version>-sdk-<sdkversion> (see Docker).
  • Library embedders: pass the queryFactory option to ClaudeBubbleBot to supply your own SDK query implementation entirely.

Stage 1 scope (M1.0–M1.5)

In:

  • One bot principal that listens to every joined thread in the process.
  • Streaming input mode with query.interrupt() for cooperative stop.
  • SDK message translation: text, thinking, tool_use, tool_result, result.
  • Wait-for-idle drain rule for the user-prompt direction.
  • canUseTool wired to Bubble tool_call_approval / tool_call_result events; the SDK's default permission mode is default.
  • The core local Bubble MCP catalog for thread browsing, principal discovery, subthread creation, and member invitations, plus the explicitly loaded artifact extension tools. Calls use the bot's authenticated BubbleApi and current grants; artifact calls first negotiate artifacts with the server.
  • Sub-agent → subthread routing keyed by parent_tool_use_id.
  • WebSocket reconnect with exponential backoff and periodic joined-thread discovery.
  • Pino-based structured logging with optional pretty-print.

Out (deferred to later stages):

  • Multi-bot per process.
  • File checkpointing, user-configured custom MCP servers, plugins, slash commands.
  • Pushing user input back into a running sub-agent (the SDK does not expose a channel for it). A subthread is a Bubble thread with its own subscription, but messages there do not feed the already-running Claude sub-agent invocation.
  • Cost / usage telemetry surfaced into Bubble.

Where to go next

  1. Launching a Bot — config schema, the claude-scuba CLI, and how the dev script runs locally.