Skip to main content

langchain-claude-cli

Drop-in replacement for ChatAnthropic that runs on the Claude Code CLI — use your Claude Pro/Max subscription, no API key needed.

Built on Anthropic's official claude-agent-sdk: real tool calling through an in-process MCP server, native structured output, native extended thinking, real token accounting. No prompt-injection hacks.

pip install langchain-claude-cli

Requirements

  • Claude Code CLI, installed and authenticated — npm install -g @anthropic-ai/claude-code, then run claude and log in
  • Claude Pro or Max subscription
  • Python ≥ 3.10, Node.js ≥ 18

No API key, and no configuration: the library reuses the login the CLI already has. On a server, where nobody can log in interactively, see Headless deployment.

Quick start

from langchain_claude_cli import ChatClaudeCli

llm = ChatClaudeCli(model="claude-sonnet-4-5")

response = llm.invoke("What is the capital of France?")
print(response.content)
print(response.usage_metadata)      # real token counts, cache reads included

invoke, ainvoke, stream, astream and batch all work, with genuine token-by-token streaming.

What you get

  • Real tool calling via bind_tools — the model returns tool_calls, your code executes them
  • Structured output enforced by the CLI's native JSON-schema mode, not by asking nicely
  • Extended thinking and the five effort levels, passed straight through
  • Session continuity — multi-turn conversations resume the underlying CLI session instead of resending history
  • Agentic mode (opt-in) — filesystem, shell and web tools, with budget caps and sandboxing
  • Typed errors and configurable retries, so you can build real fallback policies
  • LangGraph compatiblecreate_agent / create_react_agent work end to end

Tool calling

Tools are registered as an in-process MCP server, and a PreToolUse hook defers execution back to you. The model never runs your tools — it returns tool_calls and your code (or your graph) decides what happens:

from langchain_core.tools import tool

@tool
def get_weather(city: str) -> str:
    """Get the current weather for a city."""
    return f"25°C, sunny in {city}"

llm_with_tools = ChatClaudeCli(model="claude-sonnet-4-5").bind_tools([get_weather])

response = llm_with_tools.invoke("What's the weather in Tokyo?")
response.tool_calls
# [{'name': 'get_weather', 'args': {'city': 'Tokyo'}, 'id': 'toolu_...'}]

Parallel tool calls are supported. With LangGraph, nothing special is needed:

from langgraph.prebuilt import create_react_agent

agent = create_react_agent(model=llm, tools=[get_weather])
agent.invoke({"messages": [{"role": "user", "content": "Weather in Colombo?"}]})

Structured output

from pydantic import BaseModel

class Answer(BaseModel):
    answer: str
    confidence: float

structured = llm.with_structured_output(Answer)
structured.invoke("What is the capital of France?")
# Answer(answer='Paris', confidence=0.99)

Pydantic models (v1 and v2), plain dicts and TypedDict schemas are accepted, as is include_raw=True. Streaming works too — the parsed object arrives as the last chunk.

Conversations and sessions

BaseChatModel is stateless; the CLI is a stateful session. The bridge between them is a session prefix-cache:

  • A history that grows by appending — chatbots, agent loops, tool cycles — resumes its CLI session and sends only the new messages. Full fidelity, and the CLI's automatic prompt caching keeps input tokens cheap.
  • A history with no recognizable prefix (trimmed, reordered, hand-built) is flattened into a single role-labelled user message, preserving image and document blocks. A ClaudeCliCompatWarning tells you when that happens.
  • session_store="file" persists the cache in ~/.langchain-claude-cli/, so conversations survive process restarts. Inside a LangGraph node the thread_id is picked up automatically as a recovery path.
  • ChatClaudeCli(session_id="<uuid>") pins an explicit CLI session.

For long-lived processes, persistent=True keeps a live client per conversation — roughly twice as fast on reused turns, and it enables interrupt() and set_session_model(). The pool is per instance: every model built with persistent=True owns its own loop thread and its own set of live claude subprocesses, so a process that builds many instances should release them.

async with ChatClaudeCli(persistent=True) as llm:
    ...  # or: await llm.aclose() / llm.close()

If you never resume a conversation, persistent=True buys you nothing — leave it off.

interrupt() works in every mode, and the cancelled call raises ClaudeCliInterruptedError. Note that an interrupted turn ends its CLI session: the session is left holding an unfinished reply, and resuming it would make the model continue that reply instead of answering your next message. The following turn therefore re-sends its history into a fresh session — the conversation survives, its prompt cache does not.

History modes

history_mode decides what happens when a history cannot be resolved to a known session:

  • "auto" (default) — resume when the prefix is recognized, flatten otherwise.
  • "flatten" — always flatten, even when a resume was possible.
  • "replay" (experimental) — inject the history as separate user/assistant entries instead of flattening.

Replay looks like the obvious right answer and mostly isn't. The CLI's input is a live conversation, not a transcript upload: every historical user entry triggers a real generation, so cost grows with history length, and the fabricated assistant turn races against the model's own live reply — it may keep its own and discard yours. Fidelity is genuinely non-deterministic, which is why the invariant ships as a nightly xfail rather than a guarantee. Use it only when role fidelity matters more than cost and determinism.

Session stores

The cache maps history fingerprints to CLI session ids. It stores only SHA-256 digests and session ids — never message content — under two key shapes, plus a reserved __order__ entry that keeps the LRU (256 entries) durable:

fp:<sha256 of the history prefix>       → {"session_id": "..."}
thread:<profile digest>:<thread_id>     → {"session_id": "...", "count": N}

"memory" is per-instance and dies with the process. "file" writes ~/.langchain-claude-cli/sessions.json with POSIX locking and atomic renames; the worst case under concurrent writers is a lost entry, which degrades that conversation to flatten — never corruption. For a custom path, pass the store directly:

from langchain_claude_cli import ChatClaudeCli, FileStore

llm = ChatClaudeCli(session_store=FileStore("/var/lib/myapp/sessions.json"))

FileStore assumes a shared filesystem, so once you run more than one worker you'll want a real backend. Any object implementing SessionStoreBackend works:

import json
from langchain_claude_cli import ChatClaudeCli, SessionStoreBackend

class RedisStore(SessionStoreBackend):
    def __init__(self, client, prefix="lcc:"):
        self._r, self._p = client, prefix

    def get(self, key):
        raw = self._r.get(self._p + key)
        return json.loads(raw) if raw else None

    def set(self, key, value):
        self._r.set(self._p + key, json.dumps(value))

    def keys(self):
        return [k.decode().removeprefix(self._p) for k in self._r.scan_iter(f"{self._p}*")]

    def delete(self, key):
        self._r.delete(self._p + key)

llm = ChatClaudeCli(session_store=RedisStore(redis_client))

SessionCache serializes its own calls, so a backend only needs internal locking when the same store is shared across processes.

One caveat worth knowing: the store maps to sessions, but the transcripts live inside the CLI, which prunes inactive sessions after cleanupPeriodDays (~30 days). A persistent store therefore outlives what it points at. That case is handled — a purged session is detected on the first attempt, every mapping pointing at it is invalidated, and the turn transparently re-runs as a new session with its retry budget intact.

Agentic mode

By default the model runs with no built-in tools: pure-LLM semantics, the same risk profile as an API call. Claude Code's own capabilities are opt-in:

from langchain_claude_cli import ChatClaudeCli, READ_ONLY_TOOLS

analyst = ChatClaudeCli(
    model="claude-sonnet-4-5",
    builtin_tools=READ_ONLY_TOOLS,        # Read, Glob, Grep
    max_turns=10,
    permission_mode="bypassPermissions",
    cwd="/path/to/project",
)
analyst.invoke("Find all TODO comments and summarize them")

builtin_tools takes a list of tool names or ClaudeTool values, or the "claude_code" preset for everything. Ready-made groups: READ_ONLY_TOOLS, WRITE_TOOLS, SHELL_TOOLS, NETWORK_TOOLS, ALL_TOOLS. LangChain tools (deferred to you) and built-in tools (executed in-run) can be combined, and each built-in call is emitted as a tool_use block in the stream so you can render live activity.

You can also hand Claude Code's toolset to an agent driven by any provider:

from langchain.agents import create_agent
from langchain_claude_cli.middleware import ClaudeCodeToolsMiddleware

agent = create_agent(
    model=any_chat_model,               # ChatOpenAI, ChatAnthropic, ChatClaudeCli...
    tools=[...],
    middleware=[ClaudeCodeToolsMiddleware(cwd="/workspace", max_budget_usd=0.5)],
)

Security

With builtin_tools plus bypassPermissions, the CLI subprocess runs as your OS user: prompt injection becomes code execution, and cwd does not sandbox file access. Never enable agentic mode on untrusted input. Prefer READ_ONLY_TOOLS, disallowed_tools=["Bash"], sandbox, and containers in production. The default pure-LLM mode carries none of these risks.

Error handling

Every failure mode is a typed exception, so retry and fallback logic never has to parse error text. Everything raised out of this package is a ClaudeCliError, including errors originating in claude_agent_sdk.

API ClaudeCliAuthError · ClaudeCliRateLimitError · ClaudeCliOverloadedError
Run ClaudeCliTimeoutError · ClaudeCliBudgetExceededError · ClaudeCliInterruptedError
CLI error result ClaudeCliResultErrorClaudeCliMaxTurnsError · ClaudeCliExecutionError
Infrastructure ClaudeCliStartupErrorClaudeCliNotFoundError · ClaudeCliTransportErrorClaudeCliProcessError

The last two rows are the distinction a batch runner needs: a result error consumed budget and will fail the same way if retried, while a startup error never reached the model, so the work is safe to requeue. The SDK-derived types also inherit from the SDK class they replace, so an existing except CLIConnectionError keeps working.

An error carries the accounting of the run that failed, whenever the CLI reported one before failing — so a failed run can still be costed:

try:
    await llm.ainvoke(prompt)
except ClaudeCliError as exc:
    if exc.total_cost_usd is not None:
        ledger.add(exc.total_cost_usd, turns=exc.num_turns)
    if isinstance(exc, ClaudeCliStartupError):
        queue.requeue(job)          # never ran; not a data problem
    elif isinstance(exc, ClaudeCliMaxTurnsError):
        review.flag(job, turns=exc.num_turns)

usage, total_cost_usd, num_turns, duration_ms, session_id, subtype and the whole result_message are exposed, each None when unknown, and they survive pickling.

Retries on 429/5xx are handled for you (max_retries, default 2) — set max_retries=0 if your own fallback layer should see the raw error. response_metadata["rate_limit"] reports your subscription window as {status, type, utilization, resets_at}.

Two things worth setting in production: timeout, and inactivity_timeout (default "auto" — 120s in pure-LLM mode, off in agentic mode where slow tools are legitimate). If the CLI process dies mid-run, the SDK stream can otherwise hang open forever.

Debug logging: logging.getLogger("langchain_claude_cli") at DEBUG shows session resolution, pool activity, tool defer/delivery and retries.

Options

Model

Option Default Description
model "claude-sonnet-4-5" Model id (alias: model_name)
system_prompt None Extra system prompt, prepended to any SystemMessage
thinking None {"type": "enabled", "budget_tokens": N}, or {"type": "adaptive"}
effort None max / xhigh / high / medium / low
max_tokens None Client-side truncation (see compatibility below)
stop_sequences None Client-side, output is cut at the sequence (alias: stop)
betas None Beta feature flags passed to the CLI
mcp_servers None ChatAnthropic connector format or CLI-native stdio/SSE/HTTP

Sessions and performance

Option Default Description
session_store "memory" "memory", "file" (persistent), or a SessionStoreBackend
session_id None Resume an explicit CLI session
history_mode "auto" "auto" resume-then-flatten, "flatten" always, "replay" (experimental)
persistent False Keep a live client per conversation (release with aclose())
pool_max_clients 4 Max live clients per instance when persistent=True
pool_ttl 300.0 Seconds an idle pooled client is kept

Agentic

Option Default Description
builtin_tools None None = pure LLM; a tool list; or the "claude_code" preset
allowed_tools / disallowed_tools None Fine-grained allow/deny lists
permission_mode None default / acceptEdits / plan / bypassPermissions
max_turns None 1 in pure-LLM mode, unlimited in agentic mode
cwd / add_dirs None Working directory and extra readable roots
sandbox None CLI sandbox configuration
max_budget_usd None Hard cost cap per run

Reliability

Option Default Description
max_retries 2 Client-side retries on 429/5xx
timeout None Total run timeout, in seconds
inactivity_timeout "auto" Abort if the stream goes silent; None disables
fallback_model None Model to fall back to when the primary fails

Environment and auth

Option Default Description
auth "oauth" Neutralizes inherited ANTHROPIC_API_KEY / ANTHROPIC_AUTH_TOKEN so the CLI always uses your subscription. "inherit" opts out
env None Extra environment variables for the CLI subprocess
cli_path None Path to a specific claude binary

Headless deployment

Interactive login is not an option in a container or on CI. Mint a long-lived token on a machine where you are logged in:

claude setup-token      # prints a 1-year, inference-scoped OAuth token

From there it only has to reach the process environment — the CLI subprocess inherits it.

From a .env file

The usual choice in development. Nothing in this package reads .env, so load it yourself before the model runs:

# .env — never commit this
CLAUDE_CODE_OAUTH_TOKEN=<the token setup-token printed>
from dotenv import load_dotenv          # pip install python-dotenv
from langchain_claude_cli import ChatClaudeCli

load_dotenv()                           # must run before the first invoke
llm = ChatClaudeCli(model="claude-sonnet-4-5")

Runtimes that read env files themselves need no load_dotenv()env_file: in Docker Compose, EnvironmentFile= in a systemd unit, or your platform's secret store. Either way the variable is set for the whole process.

Per instance

When one process serves several accounts, pass the token explicitly instead. env is merged on top of the inherited environment, so each instance can carry a different one:

llm = ChatClaudeCli(
    model="claude-sonnet-4-5",
    env={"CLAUDE_CODE_OAUTH_TOKEN": token_for_this_user},
)

This also overrides any local claude login, which is what makes it work on a developer machine already authenticated as someone else.

What to watch for

  • No refresh token. These tokens last a year and cannot renew themselves. When one expires the CLI simply fails — mint a new one and restart the process.
  • Keep auth="oauth". ANTHROPIC_AUTH_TOKEN outranks CLAUDE_CODE_OAUTH_TOKEN in the CLI's auth precedence; the default guard neutralizes it so a stray value in the environment cannot silently redirect your billing.
  • Isolate tenants. Several accounts in one container should each get their own CLAUDE_CONFIG_DIR in the same env dict, or they will share session state.

ChatAnthropic compatibility

Every ChatAnthropic constructor parameter is accepted — nothing raises on migration, so it is a two-line change:

# Before
from langchain_anthropic import ChatAnthropic
llm = ChatAnthropic(model="claude-sonnet-4-5", api_key="sk-ant-...")

# After
from langchain_claude_cli import ChatClaudeCli
llm = ChatClaudeCli(model="claude-sonnet-4-5")

Support comes in three levels:

Native. Streaming, tool calling, structured output, extended thinking, effort, token usage (including cache reads and total_cost_usd), stop_reason, images, PDFs, system messages, MCP servers, web_search / web_fetch, max_retries, timeout, fallback_model, LangGraph agents.

Client-side workaround. stop_sequences (output scanned and cut client-side), max_tokens (truncation at ~4 chars/token with a synthetic stop_reason), tool_choice (system-prompt instruction, validated, one retry, then an explicit error), get_num_tokens_from_messages (heuristic — there is no count-tokens endpoint without an API key), arbitrary histories (see Conversations and sessions).

Accepted no-op, warned once: temperature, top_k, top_p, anthropic_api_url, anthropic_proxy, default_headers, inference_geo, context_management, cache_control blocks (the CLI caches automatically; you still get the token counts), citations, computer use, strict tool use.

Legal

Community project — not affiliated with or endorsed by Anthropic. You are responsible for complying with Anthropic's terms.

This package builds on the official, MIT-licensed claude-agent-sdk. No reverse engineering, no credential extraction. Your usage is governed by Anthropic's Consumer Terms (Pro/Max) or Commercial Terms (API), and the Acceptable Use Policy.

In particular: consumer subscriptions are for individual use, may not be resold or used to power a product for end users, and heavy automated usage counts against your own rate limits. For anything beyond personal or internal use, use an Anthropic API key under the Commercial Terms — at which point you probably want langchain-anthropic directly.

License

MIT © Charly López — see LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

langchain_claude_cli-1.1.0.tar.gz (96.5 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

langchain_claude_cli-1.1.0-py3-none-any.whl (55.4 kB view details)

Uploaded Python 3

File details

Details for the file langchain_claude_cli-1.1.0.tar.gz.

File metadata

  • Download URL: langchain_claude_cli-1.1.0.tar.gz
  • Upload date:
  • Size: 96.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for langchain_claude_cli-1.1.0.tar.gz
Algorithm Hash digest
SHA256 acf2c6e79984e8ae66d1f9a1c175df133e3dd5044c080643a1859356e26d94b9
MD5 bfa6149b6983569c288344e4ddf8dd12
BLAKE2b-256 fc0b5beade3e285b410748a29ac99314ca4074fbd8e2babcc659271c38bb5147

See more details on using hashes here.

Provenance

The following attestation bundles were made for langchain_claude_cli-1.1.0.tar.gz:

Publisher: release.yml on clriesco/langchain-claude-cli

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file langchain_claude_cli-1.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for langchain_claude_cli-1.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b315ea353225e3851ed86eec1d68f317923cb7c5b24cf0c41b39cbc11f09d89d
MD5 243b856177616ed51a276598f0f45a23
BLAKE2b-256 1bb19bdc26e20fa85ac084eefc85485d19cd4c775856ffdb5b5b9451e5b095ee

See more details on using hashes here.

Provenance

The following attestation bundles were made for langchain_claude_cli-1.1.0-py3-none-any.whl:

Publisher: release.yml on clriesco/langchain-claude-cli

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

1.2.1

2 files

1.2.0

2 files

1.1.1

2 files

This release

1.1.0 This release

2 files

1.0.0

2 files

0.4.3

2 files

0.4.2

2 files

0.4.1

2 files

0.4.0

2 files

0.3.1

2 files

0.3.0

2 files

0.2.1

2 files

0.2.0

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page