How Dual Claude Integration Works in claude-code-telegram: SDK Primary with CLI Fallback

The bot implements a facade pattern where ClaudeSDKManager handles requests via the official Claude Agent SDK, but automatically falls back to ClaudeProcessManager running the Claude CLI subprocess when the SDK returns JSON decode errors, task group failures, or invalid responses.

The RichardAtCT/claude-code-telegram repository implements a resilient dual Claude integration strategy that ensures continuous operation even when the primary SDK backend encounters issues. This architecture combines the feature-rich Claude Agent SDK with a reliable CLI fallback mechanism, transparently switching between them based on runtime conditions.

Architecture of the Dual Claude Integration

The integration centers on the ClaudeIntegration facade class defined in src/claude/facade.py. This facade abstracts two distinct backend implementations, allowing the rest of the application to interact with Claude through a single unified interface regardless of which underlying mechanism actually executes the request.

Component File Path Role Activation Condition
ClaudeSDKManager src/claude/sdk_integration.py Primary backend using the official claude-agent-sdk. Handles async streaming, tool calls, and extracts real session IDs from ResultMessage objects. Default for all requests when USE_SDK environment variable is not set to false.
ClaudeProcessManager src/claude/integration.py Fallback backend that spawns claude-cli as a subprocess, parses stdout/stderr, and generates temporary session IDs prefixed with temp_. Activated automatically when ClaudeSDKManager raises specific exceptions, or when USE_SDK=false.

How the Fallback Is Triggered

The dual integration employs a try-catch mechanism within the facade to detect SDK failures and seamlessly transition to the CLI backend.

SDK Error Detection

When ClaudeIntegration.run_command() is invoked, it first instantiates ClaudeSDKManager and attempts to execute the request. The SDK manager treats the following conditions as fatal errors that warrant fallback activation:

  • JSONDecodeError: Occurs when the SDK returns malformed JSON that cannot be parsed into a valid response object.
  • TaskGroupError: Indicates the SDK's internal subprocess crashed or behaved unexpectedly during execution.
  • Missing ResultMessage: When the SDK response lacks the expected ResultMessage structure or contains an unexpected HTTP status code.

Automatic Fallback Mechanism

If any of the above exceptions are caught in src/claude/facade.py, the facade immediately:

  1. Logs the SDK failure for debugging purposes.
  2. Instantiates a new ClaudeProcessManager with identical configuration parameters.
  3. Re-runs the same user request through the CLI backend.
  4. Returns the resulting ClaudeResponse object to the orchestrator in src/bot/orchestrator.py.

This fallback is transparent to the Telegram bot logic—the orchestrator receives the same response type regardless of which backend generated it.

Key Differences Between SDK and CLI Backends

While both backends implement the same interface, they differ in session management, capabilities, and configuration options.

Session Handling and Persistence

The SDK backend extracts the genuine Claude session ID directly from the ResultMessage object returned by the claude-agent-sdk. These persistent session IDs allow the bot to maintain conversation context across multiple turns.

The CLI backend generates synthetic session identifiers prefixed with temp_ (e.g., temp_abc123). These temporary IDs are never sent back to Claude's servers; they exist only to satisfy the local SQLite session store in src/db/session_store.py. This ensures that CLI-based conversations don't corrupt persistent SDK sessions.

Shared Tool Monitoring

Both backends utilize the ClaudeToolMonitor class defined in src/claude/monitor.py to validate tool calls. The monitor enforces security policies—including tool name validation, argument sanitization, and file-path boundary checks—regardless of whether the tool call originated from the SDK stream or the CLI subprocess output.

Configuration Environment Variables

Developers can control the dual integration behavior through environment variables:

  • USE_SDK: When set to true (default), enables the SDK primary path with CLI fallback. When set to false, forces the system to use ClaudeProcessManager exclusively.
  • ANTHROPIC_API_KEY: Required for SDK operation; if missing or invalid, triggers the fallback mechanism.

Implementation Examples

Standard Usage with Automatic Fallback

The following example demonstrates the normal operation path where the SDK handles the request, but the fallback remains ready:

from src.claude.facade import ClaudeIntegration
from src.config.settings import Settings

settings = Settings()  # Loads ANTHROPIC_API_KEY and USE_SDK

claude = ClaudeIntegration(settings)

response = await claude.run_command(
    user_id=12345,
    directory="/home/user/project",
    prompt="Refactor this Python class to use dataclasses."
)

print(f"Backend used: {response.backend}")
print(f"Session ID: {response.session_id}")
print(response.text)

If the SDK succeeds, response.backend equals "sdk" and response.session_id contains a persistent Claude session identifier. If the SDK fails, the same code returns response.backend == "cli" with a temp_* session ID.

Simulating SDK Failure to Observe Fallback Behavior

To verify the fallback mechanism works correctly, you can force an SDK error by invalidating the API key:

import os
from src.claude.facade import ClaudeIntegration
from src.config.settings import Settings

# Force SDK authentication failure

os.environ["ANTHROPIC_API_KEY"] = ""

claude = ClaudeIntegration(Settings())

response = await claude.run_command(
    user_id=12345,
    directory="/tmp",
    prompt="List the files in this directory."
)

assert response.backend == "cli"
print("Successfully fell back to CLI backend")

When the SDK raises an authentication error or JSONDecodeError, the facade catches the exception in src/claude/facade.py and reroutes the request to ClaudeProcessManager.

Forcing CLI-Only Mode

For debugging or environments where the SDK is incompatible, disable the primary backend entirely:

import os
from src.claude.facade import ClaudeIntegration
from src.config.settings import Settings

os.environ["USE_SDK"] = "false"

claude = ClaudeIntegration(Settings())

# All subsequent calls use ClaudeProcessManager directly

Setting USE_SDK=false bypasses the SDK attempt entirely, instantiating ClaudeProcessManager as the sole backend.

Summary

  • The dual Claude integration uses a facade pattern in src/claude/facade.py to abstract two distinct backends: the SDK-based ClaudeSDKManager and the CLI-based ClaudeProcessManager.
  • The SDK serves as the primary backend, offering streaming tool calls, persistent session IDs, and richer error diagnostics through the official claude-agent-sdk.
  • The CLI fallback triggers automatically when the SDK raises JSONDecodeError, TaskGroupError, or returns invalid/missing ResultMessage objects, ensuring the bot remains operational.
  • Session handling differs between backends: the SDK extracts real session IDs from Claude's servers, while the CLI generates temporary temp_* identifiers that exist only for local state management.
  • Developers can control the behavior via the USE_SDK environment variable to force CLI-only mode for debugging or compatibility reasons.

Frequently Asked Questions

What triggers the fallback from SDK to CLI backend?

The fallback activates when ClaudeSDKManager encounters specific fatal errors: JSONDecodeError while parsing SDK responses, TaskGroupError indicating subprocess crashes, missing ResultMessage structures, or unexpected HTTP status codes. The ClaudeIntegration facade in src/claude/facade.py catches these exceptions and immediately reroutes the request to ClaudeProcessManager.

How do session IDs differ between the SDK and CLI backends?

The SDK backend extracts genuine Claude session IDs directly from the ResultMessage objects returned by the claude-agent-sdk, enabling persistent conversation context across multiple turns. In contrast, the CLI backend generates synthetic session identifiers prefixed with temp_ (e.g., temp_abc123) that exist only for local SQLite storage in src/db/session_store.py and are never transmitted to Claude's servers.

Can I force the bot to use only the CLI backend?

Yes, set the environment variable USE_SDK to false before initializing ClaudeIntegration. This bypasses the SDK attempt entirely and forces the facade to instantiate ClaudeProcessManager as the sole backend. This configuration is useful for debugging, environments lacking SDK dependencies, or scenarios requiring guaranteed CLI behavior.

Does the CLI fallback support the same tool monitoring as the SDK?

Both backends utilize the shared ClaudeToolMonitor class defined in src/claude/monitor.py to validate tool calls. The monitor enforces identical security policies—including tool name validation, argument sanitization, and file-path boundary checks—regardless of whether the tool call originated from the SDK's async stream or the CLI's subprocess output.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →