Claude Code Engine¶
ClaudeCodeEngine is a standard LazyBridge Engine
that runs the model/tool loop through the locally installed Claude Code
runtime (via the Claude Agent SDK) instead of a raw provider API call. The
application remains a normal lazybridge.Agent — Claude Code is only the
engine that drives it.
Scope and design¶
The integration is designed to use a Claude.ai account with a Claude Code compatible plan (for example, Pro or Max), without putting an API key in the project. It does not create a persistent service, open ports, or store Claude credentials or separate Claude conversations.
LazyBridge owns conversation memory. LazyBridge owns tools. For one agent run, the engine creates a temporary in-process MCP adapter: it only lets Claude invoke tools that LazyBridge has already normalised.
1. First-time Claude Code setup and sign-in¶
Prerequisites¶
- A Claude.ai account with a plan that includes Claude Code.
- Node.js and npm (Anthropic's npm installation documentation requires Node.js 18 or later).
- Python 3.11 or later.
- An Internet connection for Claude Code sign-in and model processing.
On Windows, Claude Code is supported in WSL or Git Bash. The Python process
using ClaudeCodeEngine may run in the normal project environment as long as
it can find the claude command.
Install Claude Code¶
In a shell supported by Claude Code:
If a global npm installation does not suit the environment, use one of the
alternative methods in Anthropic's documentation. Do not use sudo with the
global npm installation.
Sign in once¶
- Change into any working directory.
- Start
claude. - At the sign-in screen, choose the Claude App / Claude.ai subscription option.
- Complete browser sign-in with the account that owns the Claude plan.
- Return to the terminal. The interactive session can then be closed if it is not otherwise needed.
Claude Code stores its own credentials in local secure storage.
ClaudeCodeEngine reuses that login through the Agent SDK: it does not read,
copy, or write tokens into the repository, project environment variables, or
LazyBridge memory.
Quick verification:
If the first command prints the requested response, sign-in is ready. Do not
set ANTHROPIC_API_KEY when the explicit goal is to use Claude.ai sign-in; an
API key uses a separate billing path.
Official Anthropic references: Claude Code setup and CLI reference.
2. Install the extra¶
ClaudeCodeEngine is always importable from lazybridge; actually
constructing/running one needs the optional dependencies:
This pulls claude-agent-sdk and mcp. Without the extra installed,
constructing a ClaudeCodeEngine still works, but the first call that
touches the SDK raises a clear ImportError pointing back to this install
command — see sdk_client.AgentSdkClient in
lazybridge.engines.claude_code.
3. Basic usage: a standard LazyBridge engine¶
from lazybridge import Agent, ClaudeCodeEngine
def get_quote(symbol: str) -> dict[str, str]:
"""Return a deterministic quote for the requested symbol."""
return {"symbol": symbol, "price": "123.45"}
agent = Agent(
name="research",
engine=ClaudeCodeEngine(model="sonnet", system="Be concise."),
tools=[get_quote],
)
result = agent("Find AMZN's quote using the available tool.")
print(result.text())
Nothing changes for a caller compared with a standard LazyBridge engine:
Agent,Memory,Session, andstream()remain LazyBridge concepts.resultis a normal LazyBridgeEnvelopewithprovider="claude-code".- Functions,
Toolobjects,ToolProviderinstances, and otherAgentinstances are passed throughtools=[...]. - There is no application MCP registration to perform manually.
4. Tool lifecycle¶
tools=[function | ToolProvider | Agent]
|
v
LazyBridge normalises/expands them into Tool objects
|
v
ClaudeCodeEngine creates an in-process MCP adapter for this run only
|
v
Claude Code selects a tool and sends validated arguments
|
v
LazyBridge Tool.run(**args) runs the function or child agent
|
v
Result or error returns to Claude, then to the LazyBridge Envelope
JSON schemas come from LazyBridge Tool.definition(). Argument validation
therefore stays in normal Tool.run() execution. A tool failure is returned
to Claude as a tool error, allowing the model to correct its arguments or
choose another strategy rather than terminating the entire run immediately.
Tool providers and child agents¶
agent = Agent(
name="coordinator",
engine=ClaudeCodeEngine(),
tools=[my_provider, specialist_agent],
)
LazyBridge expands my_provider.as_tools() before engine execution.
specialist_agent becomes a normal tool with a task argument; when Claude
calls it, LazyBridge runs that child agent with its usual configuration.
Tool names must be unique — the same Agent/build_tool_map rule LazyBridge
uses everywhere else.
5. Memory, sessions, and streaming¶
Memory¶
LazyBridge Memory is the sole conversation memory. Before a run, the engine
builds its prompt from messages already in that memory; after a successful
run, it adds the task and response. It does not use SDK resume, persistent
Claude sessions, or a parallel Claude database (unless session_mode="runtime"
— see below).
To keep continuity, pass the same Memory to the agent. To isolate a run,
use a new Memory or no memory.
Session¶
If the agent has a Session, the engine emits normal LazyBridge events:
agent start/finish, tool call, tool result, and tool error, plus a
MODEL_RESPONSE event shaped like LLMEngine's (provider="claude-code",
token/cost usage) so Session.usage_summary() and any cost-report tooling
that reads event_type="model_response" sees this engine's usage too.
Existing observability consumers do not need Claude Code-specific logic.
Streaming¶
agent.stream(...) forwards Agent SDK text chunks as a LazyBridge stream. At
the end, the complete text is added to the same Memory, just as it is for
agent(...).
6. Web and filesystem: default profile¶
The engine's default profile is intentionally useful but read-only:
| Capability | Default | Enablement | Boundary |
|---|---|---|---|
Web search (WebSearch) |
yes | web=True (default) |
no implicit application tools |
Web fetch (WebFetch) |
yes | web=True (default) |
same as above |
File read (Read) |
only with a root | cwd=... or file_roots=[...] |
declared roots only |
File discovery (Glob) |
only with a root | same as above | declared roots only |
Text search (Grep) |
only with a root | same as above | declared roots only |
Shell (Bash) |
no | not exposed | — |
File modifications (Write, Edit) |
no | not exposed | — |
| Native Claude Code subagents | no | not exposed | — |
Example with a restricted filesystem:
engine = ClaudeCodeEngine(
model="sonnet",
cwd="C:/work/project",
# Or, without cwd:
# file_roots=["C:/work/project", "C:/work/reference-docs"],
)
When cwd is set and file_roots is omitted, cwd automatically becomes the
only readable root. A path outside the declared roots is denied by the
engine's permission callback. To disable web tools:
7. Engine configuration¶
ClaudeCodeEngine(
model="sonnet", # supported Claude model alias/name
cwd="C:/work/project", # optional working directory
system="...", # optional system instructions
max_turns=20, # Claude agentic-turn limit
file_roots=["C:/work/project"], # optional read boundaries
web=True, # WebSearch/WebFetch
reasoning_effort=None, # "low"/"medium"/"high"/"xhigh"/"max"
thinking=None, # "adaptive" / "disabled" / token budget int
fallback_model=None,
session_mode="memory", # "memory" (default) or "runtime"
session_name=None,
request_timeout=120.0, # per-run deadline; None disables
stream_idle_timeout=90.0, # idle gap before TimeoutError in stream()
max_retries=3, # transient-failure retries, exp. backoff + jitter
retry_delay=1.0,
tool_timeout=None, # per-tool asyncio.wait_for deadline
)
Retries follow the same "429/5xx/network/timeout" policy LLMEngine uses,
at the coarser granularity available to an SDK/CLI-backed engine (pass/fail
on the whole call, not individual HTTP round-trips). A missing/unreadable
claude executable (FileNotFoundError/PermissionError) is treated as a
permanent configuration problem and is not retried.
Claude Code model identifiers¶
The stable model values accepted by the Claude Code CLI interface are:
| Value | Meaning | Recommendation |
|---|---|---|
"sonnet" |
The latest Sonnet model available to the authenticated Claude Code account. | Default. |
"opus" |
The latest Opus model available to the authenticated Claude Code account. | Use for harder tasks when the plan permits it. |
"haiku" |
The latest Haiku model, where it is enabled by the Claude Code runtime and account. | Fast/low-cost choice; validate on the target account. |
"inherit" |
Inherit the model from a parent Claude Code agent definition. | Not useful as this engine's top-level default. |
| A full Claude model ID | A specifically pinned model version accepted by the installed Claude Code runtime. | Use only after verifying it locally. |
The installed Agent SDK explicitly identifies sonnet, opus, haiku, and
inherit as model aliases for agent definitions. Anthropic's public CLI
reference currently documents only sonnet and opus, so haiku must be
validated with the target CLI version and subscription before it becomes a
production default. Do not hard-code informal names such as "sonnet-5" or
"sonnet-4.6" — they are not documented Claude Code aliases.
Runtime session mode¶
The default session_mode="memory" sends LazyBridge Memory as prompt
context on every call. Set session_mode="runtime" to make the Claude Code
session itself the conversation source instead:
engine = ClaudeCodeEngine(
model="sonnet",
session_mode="runtime",
session_name="research", # optional fixed channel inside one LazyBridge Session
reasoning_effort="high",
thinking="adaptive",
)
The first call in a LazyBridge Session starts a clean Claude session and
stores its ID on that Session. Later calls with the same engine/channel
resume it. A new LazyBridge Session has no stored ID and starts cleanly
again. Memory is still updated after each answer so it can be explicitly
supplied to other agents through from_memory(...), but it is not reinjected
into the parent runtime conversation. Envelope.context, Agent.sources,
from_agent(...), and from_memory(...) continue to be passed on every turn.
An explicit file_roots list overrides the root inferred from cwd. Do not
grant broad directories for convenience — declare only directories the agent
needs to inspect.
Distinguishing LazyBridge sessions on disk¶
Unlike CodexEngine's threadSource (sent at thread creation, on the wire),
the Agent SDK has no creation-time metadata field. It does have a public,
post-hoc tagging API — claude_agent_sdk.tag_session(session_id, tag,
directory=...), appending a {"type": "tag", ...} JSONL line that
list_sessions() reads back as .tag — and ClaudeCodeEngine uses it
automatically:
engine = ClaudeCodeEngine(persist_session=True) # tag="lazybridge" (default)
engine = ClaudeCodeEngine(persist_session=True, tag="my-app") # a caller-specific label
engine = ClaudeCodeEngine(persist_session=True, tag=None) # skip tagging
Tagging fires once, on the run that creates a durable session — not on
every resume, since the SDK's "last tag wins" semantics make repeated calls
redundant I/O, not idempotent no-ops. It requires persist_session=True
(or an explicit session_id); an ephemeral session is never tagged, since
there is nothing durable to tag. A tagging failure raises a UserWarning
rather than failing the run — it is identification metadata, not something
correctness should depend on.
Finding and cleaning up tagged sessions later (list_sessions() has no
server-side tag filter — filter the returned list):
from claude_agent_sdk import delete_session, list_sessions
mine = [s for s in list_sessions() if s.tag == "lazybridge"]
for s in mine:
delete_session(s.session_id)
8. Verification and troubleshooting¶
pytest tests/unit/engines/claude_code/ -q
python examples/claude_code/live_mcp_smoke.py
python examples/claude_code/live_engine_smoke.py
Unit tests do not use Claude credentials. The two example scripts make real
requests using the local Claude Code login: live_mcp_smoke.py verifies the
in-process MCP bridge directly; live_engine_smoke.py runs through a
complete lazybridge.Agent.
Common issues:
claudecannot be found: install Claude Code and ensure it is on thePATHof the process running Python.- Sign-in is requested: start
claudeinteractively and complete Claude.ai authentication, then repeat the quick verification command. - File access is denied: add the correct directory to
file_rootsor setcwdto the desired root. Do not broaden permissions with Bash or bypass modes. - A tool is not visible: pass it through
Agent(tools=[...]), confirm a provider exposesas_tools(), and check for duplicate names. - A tool fails: the error is available to Claude and in
Sessionevents; check the tool signature, schema, and implementation.
9. What this integration intentionally does not do¶
- It does not replace LazyBridge as the orchestrator.
- It does not create separate persistent Claude memory.
- It does not run an external or resident MCP server.
- It does not grant filesystem write access or shell execution by default.
- It does not expose tools that were not explicitly passed to the agent.
These limits are intentional: they keep the engine replaceable, configuration predictable, and the security boundary small.
Structured output¶
Agent(output=<type>) is enforced by the model, not by prompt discipline. The
engine derives the JSON schema and passes it as the Agent SDK's
output_format ({"type": "json_schema", "schema": ...}, the CLI's
--json-schema); the CLI returns the validated object on
ResultMessage.structured_output, so Envelope.payload is the parsed model.
class Quote(BaseModel):
symbol: str
price: float
agent = Agent(name="quoter", engine=ClaudeCodeEngine(model="sonnet"), output=Quote)
This is the same server-side guarantee LLMEngine gets from
StructuredOutputConfig. Verified live (claude_agent_sdk 0.2.128) with a
plain Pydantic schema including an optional field and a nested model
($defs) — no strict-mode rewrite needed, unlike Codex's turn/start
outputSchema. output=str (the default) sets no output_format; if the
schema cannot be derived, the run falls back to LazyBridge's post-hoc JSON
parse and retry.
Reading the account's usage budget¶
snapshot = await engine.usage()
print(snapshot.session.used_percent, "% of the session")
for label, window in snapshot.weekly.items():
print(label, window.used_percent, "%, resets", window.resets_at)
There is no typed field for this — verified live by scanning every message
type of a normal run: the Agent SDK's RateLimitEvent arrives free on every
run, but on a current account its utilization is None, and nothing else
in the stream carries a percentage. The weekly figures exist in exactly one
place: the prose Claude Code's own /usage slash command prints. So
engine.usage() spends one small turn (~13s including process startup, no
completion of its own since /usage is answered by the CLI directly) sending
that command and parses the reply into a ClaudeUsageSnapshot
(lazybridge.engines.claude_code.usage):
session— the current 5-hour-scale window.weekly— one entry per label the CLI reports, keyed by that label verbatim:"all models"plus, when the account has one, a per-model breakdown (observed live:"Fable"). The key set is not fixed by this library — a new label is a new dict key, not a version bump.most_used()— the weekly window closest to its limit.
Screen-scraping a CLI's human-facing text means the wording can drift, so
every field this cannot extract is None rather than guessed, and
snapshot.parsed says whether anything was recognised at all — raw_text
always carries the untouched report underneath, for a caller that wants to
fall back rather than trust an empty result. A reset date carries no year in
the CLI's own text; one already in the past by more than a day is read as
next year's, so a reading taken near 31 December does not report a window
that already closed. A reset clause with no recognised timezone is left
unparsed rather than assumed to be UTC or the machine's own zone.
This is a different failure mode from the SDK ending the turn in error
(authentication, no active session): that raises RuntimeError from
engine.usage() directly, distinguishable from a successful call whose
report just did not parse.
The free function fetch_claude_usage() underneath takes an explicit
model=/cwd=/client= for use outside an Engine — the standalone
parse_usage_report(text) for a report captured elsewhere works with no SDK
call at all.
Multimodal¶
images= is forwarded as Anthropic image content blocks. Because a
plain-string prompt has nowhere to carry an attachment, a run with images
switches to the SDK's async user-message stream and sends content as
[{"type": "text", ...}, {"type": "image", ...}].
Inline bytes only: the CLI accepts a base64 source but rejects a url one,
so URL-only images are dropped with a UserWarning naming the URL rather than
being fetched behind the caller's back — pass a path or bytes and LazyBridge
inlines them. audio= is never forwarded, since Claude accepts no audio
input; it is dropped with a warning too.
See also: Codex Engine¶
CodexEngine is the same Engine contract backed by the
Codex App Server (codex app-server over JSON-RPC, not codex exec).
Both engines compose identically with LazyBridge — the differences are
documented in that guide: Codex reports no dollar cost, primes structured
output in the prompt rather than enforcing it, and has no persistent-thread
equivalent of session_mode="runtime".