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

> Discover how claude-code-telegram's dual Claude integration uses SDK primary with CLI fallback. Learn how fallback is triggered automatically for seamless operation.

- Repository: [Richard A/claude-code-telegram](https://github.com/richardatct/claude-code-telegram)
- Tags: internals
- Published: 2026-02-19

---

**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`](https://github.com/RichardAtCT/claude-code-telegram/blob/main/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`](https://github.com/RichardAtCT/claude-code-telegram/blob/main/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`](https://github.com/RichardAtCT/claude-code-telegram/blob/main/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`](https://github.com/RichardAtCT/claude-code-telegram/blob/main/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`](https://github.com/RichardAtCT/claude-code-telegram/blob/main/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`](https://github.com/RichardAtCT/claude-code-telegram/blob/main/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`](https://github.com/RichardAtCT/claude-code-telegram/blob/main/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:

```python
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:

```python
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`](https://github.com/RichardAtCT/claude-code-telegram/blob/main/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:

```python
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`](https://github.com/RichardAtCT/claude-code-telegram/blob/main/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`](https://github.com/RichardAtCT/claude-code-telegram/blob/main/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`](https://github.com/RichardAtCT/claude-code-telegram/blob/main/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`](https://github.com/RichardAtCT/claude-code-telegram/blob/main/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.