claude-node
March 23, 2026 · View on GitHub
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.
The CLI bridge
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.
Positioning
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
Runtime model
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.
Installation
Python package
pip install claude-node
Runtime requirement
Make sure Claude Code / Claude CLI is installed and available:
claude --version
If that command fails, claude-node cannot start a session.
Requirements
- 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.
Quick start
Single controller
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)
Real-time callback
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()
Resume a session
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()
Lightweight multi-session routing
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)
Public API
The current public surface is intentionally small:
from claude_node import (
ClaudeController,
ClaudeMessage,
MultiAgentRouter,
AgentNode,
)
ClaudeController
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.
ClaudeMessage
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
MultiAgentRouter
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.
Current architecture
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
controller.py
Contains:
ClaudeMessageClaudeController_send_lockfor serializing concurrent send calls
router.py
Contains:
AgentNodeMultiAgentRouter
runtime.py
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
exceptions.py
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.
The protocol this library is built on
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 --verbose
Typical 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.
Session model
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.
Recommended practice
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.
Important note
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.
Current status and known limitations
This repository is functional and in alpha state.
What works now
- 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).
Current limitations
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.
Design principles
These principles define the project’s direction.
1. Subprocess-first
The library controls a real Claude CLI process.
2. Thin wrapper, not platform
The goal is a dependable bridge, not a giant framework.
3. Explicit session control
Lifecycle, resume behavior, and routing should stay visible and controllable.
4. Protocol transparency
The stream should remain understandable and debuggable.
5. Lightweight routing only
Multi-session patterns are welcome; orchestration sprawl is not.
Documentation map
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
Runnable examples
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.
Contributing
See CONTRIBUTING.md for contribution guidelines, testing instructions, and scope boundaries.
License
Apache-2.0