Claude Code agents
What claude-scuba is, what it bridges, and what is in (and out of) Stage 1.
docs/agents/10-claude-code.mdxOn 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:
- Authenticates to a Bubble server as a configured
botprincipal and confirms the identity via/v1/auth/whoami. - 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 configuredbacklogpolicy (defaults:onFirstJoin: since_joined,onResume: catch_up,maxReplay: 50). Revoked grants stop the matching thread runtime. - Creates one session per active thread. Each session starts a long-lived
SDK
Querylazily on the first drainable user message, using streaming-input mode. The SDK's prompt is a push-styleAsyncIterablethat yields the nextSDKUserMessagewhenever the bot becomes idle and there is drainable input on that thread. - Translates the SDK's output back into Bubble events
(
run_started/message_part/run_finishedandtool-input-available/tool-output-availablechunks) and appends them to the thread. - Bridges Bubble events back into the SDK: new user turns become
follow-up prompts,
run_interrupt_requestedbecomesquery.interrupt(), andtool_call_approval/tool_call_resultevents resolve pendingcanUseTooldecisions. - 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:
@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 SDK | Status | Notes |
|---|---|---|
0.2.x | Unsupported | Below 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+ | Untested | Outside 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-sdkdependency (any version in range), or force one tree-wide with an npm/yarn/pnpmoverrides/resolutionsentry. - Docker: use a version-pinned image tag —
ghcr.io/octostaff/claude-scuba:<version>-sdk-<sdkversion>(see Docker). - Library embedders: pass the
queryFactoryoption toClaudeBubbleBotto supply your own SDKqueryimplementation 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.
canUseToolwired to Bubbletool_call_approval/tool_call_resultevents; the SDK's default permission mode isdefault.- 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
BubbleApiand current grants; artifact calls first negotiateartifactswith 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
- Launching a Bot — config schema, the
claude-scubaCLI, and how the dev script runs locally.