Skip to content

Finndersen/claude-interactive-sdk

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

15 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

claude-interactive-sdk

A drop-in replacement for ClaudeSDKClient that runs claude Code in its interactive session mode instead of the SDK's headless stream-json mode, so that usage is billed against your Claude subscription rather than the API. Supports the full ClaudeAgentOptions API including function tools, MCP servers, system prompts, multi-turn conversations, model selection, and more.

Why

Anthropic announced that programmatic usage via the API and SDK will be billed separately from subscription usage. This SDK lets you drive Claude programmatically while keeping usage on your existing Claude subscription, by launching claude exactly the way a human would (no -p, no --input-format, no --output-format) and exposing the same Python API on top.

Install

pip install claude-interactive-sdk

Requires tmux and the claude CLI to be installed. Install tmux via your package manager:

# macOS
brew install tmux

# Ubuntu/Debian
sudo apt install tmux

Usage

from claude_interactive_sdk import ClaudeInteractiveClient
from claude_agent_sdk import ClaudeAgentOptions, tool, create_sdk_mcp_server

@tool("get_weather", "Get current weather", {"location": str})
async def get_weather(args):
    return {"content": [{"type": "text", "text": f"It's sunny in {args['location']}"}]}

mcp = create_sdk_mcp_server(name="my_tools", tools=[get_weather])

async with ClaudeInteractiveClient(
    options=ClaudeAgentOptions(
        model="claude-sonnet-4-6",
        mcp_servers={"my_tools": mcp},
    )
) as client:
    await client.query("What's the weather in Sydney?")
    async for message in client.receive_response():
        print(message)

More in examples/.

Compatibility with ClaudeAgentOptions and ClaudeSDKClient

ClaudeSDKClient methods

Method Status Notes
connect() / disconnect() / __aenter__ / __aexit__
query(prompt: str)
query(prompt: AsyncIterable[dict]) ⚠️ Each dict serialised as plain text — tool-result semantics lost
receive_messages() / receive_response()
interrupt() ⚠️ Best-effort — sends C-c to the running session
get_server_info() ⚠️ Returns a minimal stub
disconnect(detach=True) NotImplementedError
set_model, set_permission_mode, rewind_files, reconnect_mcp_server, toggle_mcp_server, stop_task, get_mcp_status, get_context_usage All require the SDK control protocol, which doesn't exist in interactive mode

ClaudeAgentOptions fields

Fully supported — translated to argv, env vars, or --settings JSON:

tools, allowed_tools, disallowed_tools, system_prompt (str or file), mcp_servers (all transport types), strict_mcp_config, continue_conversation, resume, session_id, fork_session, max_turns, max_budget_usd, model, fallback_model, betas, cwd, settings, sandbox, add_dirs, setting_sources, plugins, thinking, max_thinking_tokens, effort, output_format (json_schema), extra_args, cli_path, env, enable_file_checkpointing.

Adapted — same semantics, different mechanism:

Field Mechanism
mcp_servers={"name": {"type": "sdk", …}} Materialised into an in-process HTTP MCP server; config rewritten to {"type": "http", "url": "http://127.0.0.1:<port>/mcp/<name>/"}
can_use_tool Intercepted in the HTTP MCP server — MCP-hosted tools only
hooks (command strings) Merged into --settings JSON alongside the internal Stop hook

Restricted:

Field Restriction
permission_mode Only bypassPermissions (default) and acceptEdits are accepted. default and plan would surface interactive permission prompts the harness can't answer, so they raise at construction
can_use_tool Does not fire for built-in tools (Bash, Edit, etc.). Gate those statically via disallowed_tools (Layer 1) or settings.permissions.deny patterns (Layer 2)

Not supported — raise at construction:

Field Reason
hooks with Python callables No control-protocol channel for hook callbacks
transport This package doesn't use the SDK's Transport abstraction
debug_stderr Deprecated upstream

Limited / best-effort:

Field Limitation
include_partial_messages, include_hook_events Forwarded to the CLI, but the JSONL transcript may not emit the corresponding events
agents, skills (inline dicts) Inline definitions use the control protocol — lost. File-based agents/skills in ~/.claude/agents / ~/.claude/skills work normally
session_store Remote transcript mirroring not implemented

Full details: docs/DESIGN.md §4, §10.

Implementation overview

claude runs in interactive mode inside a detached tmux session. Prompts go in via tmux paste-buffer; output comes back via the JSONL transcript that claude writes to <CLAUDE_CONFIG_DIR>/projects/<encoded-cwd>/<session-id>.jsonl. Turn termination is signalled by a Stop hook (installed via --settings) that writes to a FIFO the harness reads. User-defined tools are hosted in an in-process HTTP MCP server bound to an ephemeral localhost port. The harness wraps the launch in a stealth chain (env -u … script -q /dev/null setsid claude …) that strips harness-identifying env vars and gives the child a fresh PTY.

Sessions launched this way produce "entrypoint":"cli" in Claude's session logs, versus "entrypoint":"sdk-cli" for sessions launched via claude -p or the official SDK.

See docs/DESIGN.md for the full design including event-flow diagrams and rationale for each decision.

Development

make test        # full test suite
make test-cov    # with coverage
make lint        # ruff
make format
make typecheck   # mypy
make run-example NAME=01_hello_world

License

MIT

About

Drop-in replacement for ClaudeSDKClient that drives Claude Code in interactive TUI mode via tmux

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors