A thin subprocess-based Python bridge for persistent Claude Code sessions.
claude-node drives the local claude CLI as a long-lived subprocess and communicates with it over stream-json. Because it runs the installed CLI directly as its runtime, Python code gets access to Claude Code's native behavior through the CLI itself — achieving maximum compatibility with the CLI.
This is not a higher-level reimplementation or a message API wrapper. It is a direct bridge to the installed claude executable.
claude-node sits at the subprocess boundary:
Your Python code claude-node Your local claude CLI
───────────────── ──────────── ──────────────────────
ClaudeController ───► stdin/stdout ───► Claude Code runtime
◄── JSON events ◄───
- Maximum compatibility: because
claude-nodedelegates entirely to the localclaudeCLI, any capability the CLI exposes is available — skills, slash commands, tools, agent modes, and session management. - Process isolation: the Claude runtime lives in its own OS process, independent of the Python interpreter.
- Protocol transparency: raw
stream-jsonbehavior is visible and debuggable. - Easy embedding: drop it into any Python environment — workers, web backends, job runners, or supervisor processes.
claude-node gives Python direct control over the real local Claude Code runtime. It preserves native CLI capabilities with stream-json, explicit session lifecycle, and process-level supervision — built for embedding and integration, not for hiding Claude behind another framework.
What this is not
- Not another high-level agent framework
- Not a reimplementation of Claude
- Not a wrapper that hides the CLI behind a new abstraction
- Not a workflow or memory platform
What this is
- A thin Python runtime layer for controlling the real local Claude Code process
- A subprocess-first integration surface with explicit session and process control
- A practical foundation for embedding Claude Code into backends, workers, and internal tooling
This project is a pure Python package, but it has an external runtime dependency:
- The package itself is standard Python.
- At runtime, it requires a local
claudeexecutable inPATH.
In other words:
pip install claude-nodeinstalls the Python package.claude-nodeonly works ifclaude --versionworks on that machine.
This split is intentional. The project does not attempt to ship or emulate Claude Code. It controls an existing local Claude Code runtime.
pip install claude-nodeMake sure Claude Code / Claude CLI is installed and available:
claude --versionIf that command fails, claude-node cannot start a session.
- Python 3.11+
- a local
claudeexecutable inPATH - a working Claude Code login / configuration on the host machine
- macOS or Linux recommended
Windows support has not been validated in this repository.
from claude_node import ClaudeController
with ClaudeController(skip_permissions=True) as ctrl:
result = ctrl.send("List the files in the current directory")
if result:
print(result.result_text)
print("session:", ctrl.session_id)def on_message(msg):
if msg.is_assistant:
for text in msg.assistant_texts:
print("[assistant]", text)
if msg.is_tool_result:
for block in msg.tool_results:
print("[tool_result]", block.get("tool_use_id"), block.get("is_error"))
ctrl = ClaudeController(on_message=on_message, skip_permissions=True)
ctrl.start()
result = ctrl.send("Run the tests and summarize failures", timeout=120)
ctrl.stop()from claude_node import ClaudeController
with ClaudeController(skip_permissions=True) as ctrl:
result = ctrl.send("Remember that the project codename is ALPHA. Reply only OK.")
saved_session_id = ctrl.session_id
ctrl = ClaudeController(resume=saved_session_id, skip_permissions=True)
ctrl.start()
result = ctrl.send("What is the project codename?")
print(result.result_text if result else None)
ctrl.stop()from claude_node import MultiAgentRouter, AgentNode
with MultiAgentRouter() as router:
router.add(AgentNode("PM", system_prompt="You are a product manager."))
router.add(AgentNode("DEV", system_prompt="You are a backend engineer."))
router.start_all()
pm_reply = router.send("PM", "Design a JWT login feature.")
dev_reply = router.route(
pm_reply or "",
to="DEV",
wrap="PM proposal:\n{message}\n\nPlease review technical feasibility.",
)
print("PM:", pm_reply)
print("DEV:", dev_reply)The current public surface is intentionally small:
from claude_node import (
ClaudeController,
ClaudeMessage,
MultiAgentRouter,
AgentNode,
)Controls one long-lived Claude CLI subprocess.
Current responsibilities:
- start / stop one
claudeprocess, - write
usermessages to stdin, - read JSON lines from stdout / stderr,
- wait for
type=resultas the turn-completion signal, - track
session_id, - provide parsed messages via
ClaudeMessage, - expose simple callbacks through
on_message.
Represents one parsed JSON event from the CLI stream.
Useful helpers include:
is_initis_resultis_result_okis_result_erroris_api_errortruly_succeededis_assistantis_tool_resultassistant_textstool_callstool_resultsresult_textsession_idcost_usdnum_turns
A minimal multi-session routing layer.
It currently provides:
- named node registration,
- bulk start / stop,
- send to one named agent,
- message wrapping and routing,
- simple parallel fan-out,
- access to an underlying controller via
get_ctrl().
This is a lightweight primitive layer, not a full orchestration framework.
claude_node/
├── __init__.py # Public exports
├── controller.py # ClaudeController, ClaudeMessage
├── router.py # AgentNode, MultiAgentRouter
├── runtime.py # Binary discovery and version checking
└── exceptions.py # Typed exception hierarchy
Contains:
ClaudeMessageClaudeController_send_lockfor serializing concurrent send calls
Contains:
AgentNodeMultiAgentRouter
Binary discovery and version introspection:
find_claude_binary(cli_path)— resolve CLI path viashutil.whichget_claude_version(binary_path)— read version from--versioncheck_claude_available(cli_path)— raisesClaudeBinaryNotFoundif missing
Typed exception hierarchy (all inherit from ClaudeError → RuntimeError):
ClaudeBinaryNotFound— claude binary not in PATHClaudeStartupError— subprocess failed to startClaudeTimeoutError— operation exceeded timeoutClaudeSendConflictError— concurrent send to same controller
The codebase is intentionally compact. The long-term direction is to keep the library narrow and dependable, not large and feature-heavy.
claude-node communicates with Claude Code through newline-delimited JSON over stdin/stdout.
Typical launch shape:
claude --input-format stream-json --output-format stream-json --verboseTypical input shape:
{"type":"user","message":{"role":"user","content":[{"type":"text","text":"your message"}]}}Typical output flow per turn:
system/init— appears on initial startup and includes session metadataassistant— may include thinking, text, andtool_useblocksuser/tool_result— emitted by the CLI after internal tool executionresult— the turn is complete; this is the main synchronization point
The most important rule is simple:
Wait for
type=resultbefore sending the next message.
That rule is the backbone of the current implementation.
The repository currently supports:
- new sessions,
- explicit resume via
resume=<session_id>, - implicit “continue most recent” via
continue_session=True, - session forking via
controller.fork()— creates a new controller resuming the current session.
In multi-session or multi-node environments, prefer:
- explicit
resume=<session_id>
and avoid depending on:
--continue
because --continue resumes the most recent session in the working directory rather than the exact session you intend.
The README you are reading is intentionally honest about the current code:
resumeexists now,continue_sessionexists now,fork()exists now — creates a new controller resuming the current session.
This repository is functional and in alpha state.
- persistent Claude subprocess control,
- multi-turn sessions,
- result waiting,
- assistant / tool result parsing,
- basic session resume,
- session forking via
controller.fork(), - lightweight router patterns,
- controller-level send serialization (
_send_lock), - structured exception hierarchy (
ClaudeError,ClaudeBinaryNotFound, etc.), - runtime discovery (
claude_node.runtime), - transcript / JSONL export (
transcript_pathparameter).
send()timeout returnsNonerather than raisingClaudeTimeoutError(partial exception integration),- integration tests require a working local
claudebinary and are opt-in (see CONTRIBUTING.md).
This is why the project should currently be described as alpha.
For the full list of known limitations, see docs/06-roadmap-and-limitations.md.
These principles define the project’s direction.
The library controls a real Claude CLI process.
The goal is a dependable bridge, not a giant framework.
Lifecycle, resume behavior, and routing should stay visible and controllable.
The stream should remain understandable and debuggable.
Multi-session patterns are welcome; orchestration sprawl is not.
Additional docs live under docs/:
docs/00-index.md— documentation indexdocs/01-positioning.md— project identity and scopedocs/02-architecture.md— architecture and internal boundariesdocs/03-api-reference.md— API reference based on the current codedocs/04-protocol.md— stream-json protocol notesdocs/05-development.md— repository workflow and testing realitydocs/06-roadmap-and-limitations.md— current gaps and next steps
All examples are in examples/:
examples/demo_end_to_end.py— library usage reference (all core APIs)examples/demo_cli_native_features.py— CLI-native capabilities (skills, callbacks, transcript)examples/demo_protocol_trace.py— raw stream-json protocol reference
See examples/README.md for the full demo map.
See CONTRIBUTING.md for contribution guidelines, testing instructions, and scope boundaries.
Apache-2.0