Open-source Claude Code SDK — single-file CLIs that talk directly to the Anthropic API using your Pro/Max subscription. No binary dependency, no npm, zero overhead.
We reverse-engineered the Claude Code CLI binary (190MB Bun bundle, $20/mo subscription) and rebuilt it from scratch in 4 languages. Each SDK is a single file with zero external dependencies that:
- Authenticates via OAuth (same flow as
claude --login) - Uses your existing Pro/Max subscription (no API credits consumed)
- Implements the full agent loop: streaming, tool calling, multi-turn
- Exposes an NDJSON bridge protocol for programmatic use
- Provides an interactive REPL for human use
| Language | File | Deps | Build |
|---|---|---|---|
| Node.js | claude-native.mjs |
0 (stdlib only) | node claude-native.mjs |
| Python | claude-native.py |
0 (stdlib only) | python3 claude-native.py |
| Go | claude-native.go |
0 (stdlib only) | go run claude-native.go |
| Rust | rust-sdk/ |
serde, reqwest | cargo run --release |
# Login (opens browser, saves to macOS keychain)
node claude-native.mjs --login
# Interactive REPL (uses your Pro/Max subscription)
node claude-native.mjs
# One-shot
node claude-native.mjs -p "explain this code"
# Programmatic (NDJSON bridge)
echo '{"type":"message","content":"hello"}' | node claude-native.mjs --ndjsonThree modes, auto-detected in order:
- OAuth (Pro/Max subscription) — reads token from macOS keychain, uses
--loginto authenticate - API key —
--api-key KEYorANTHROPIC_API_KEYenv var (pay-per-token) - Auth token —
--auth-token TOKENfor direct Bearer auth
node claude-native.mjs --login # Opens browser → claude.ai OAuth → saves to keychain
node claude-native.mjs # Ready to use with your subscriptionFor programmatic use (agents, automation, CI):
→ {"type":"message","content":"search for X","tools":[...]}
← {"type":"ready","version":"1.0.0","mode":"native","session_id":"..."}
← {"type":"tool_use","id":"...","name":"search_drive","input":{"query":"X"}}
→ {"type":"tool_result","id":"...","content":"...","is_error":false}
← {"type":"response","content":"Found 3 results...","iterations":2}
Pass custom tools in the message payload. The SDK calls them via NDJSON:
{
"type": "message",
"content": "Search Google Drive for migration files",
"tools": [{
"name": "search_drive",
"description": "Search Google Drive",
"input_schema": {"type":"object","properties":{"query":{"type":"string"}},"required":["query"]}
}]
}| Tool | Description |
|---|---|
| Bash | Execute shell commands (120s timeout) |
| Read | Read files with line numbers |
| Write | Write files (creates parent dirs) |
| Glob | Find files by pattern |
| Grep | Search file contents (uses rg/grep) |
-p, --print <prompt> One-shot mode
-m, --model <name> Model: sonnet, opus, haiku (default: sonnet)
--ndjson NDJSON bridge mode
--login Login via browser OAuth
--logout Remove credentials
--oauth Force OAuth auth
--api-key <key> Use API key
--max-turns <n> Max agent turns (default: 25)
--max-tokens <n> Max output tokens (default: 16384)
--thinking <budget> Enable extended thinking
--verbose Debug logging
The key discovery: Claude Code's subscription auth requires:
- OAuth token from macOS keychain (saved by
claude --loginor our--login) - Beta headers:
anthropic-beta: claude-code-20250219,oauth-2025-04-20 - Billing header in the first system prompt block:
x-anthropic-billing-header: cc_version=2.1.81; cc_entrypoint=cli; cch=a9fc8; - Access headers:
anthropic-dangerous-direct-browser-access: true+x-app: cli
Without ALL of these, the API returns 400/401.
Copy the SDK file into your project. It's a single file, no dependencies.
# Node.js
cp claude-native.mjs ~/mon-projet/
node ~/mon-projet/claude-native.mjs -p "hello"
# Python
cp claude-native.py ~/mon-projet/
python3 ~/mon-projet/claude-native.py -p "hello"
# Go — compile en binaire statique
cp claude-native.go ~/mon-projet/
cd ~/mon-projet && go build -o claude-native claude-native.go
./claude-native -p "hello"
# Rust
cp -r rust-sdk/ ~/mon-projet/claude-sdk/
cd ~/mon-projet/claude-sdk && cargo build --release
./target/release/claude-native -p "hello"Your program spawns the SDK as a subprocess and communicates via JSON on stdin/stdout.
Python (caller):
import subprocess, json
proc = subprocess.Popen(
["node", "claude-native.mjs", "--ndjson"],
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
text=True, bufsize=1
)
# Read the "ready" message
ready = json.loads(proc.stdout.readline())
print(f"Session: {ready['session_id']}")
# Send a message with custom tools
proc.stdin.write(json.dumps({
"type": "message",
"content": "Search Google Drive for migration files",
"tools": [{
"name": "search_drive",
"description": "Search Google Drive",
"input_schema": {
"type": "object",
"properties": {"query": {"type": "string"}},
"required": ["query"]
}
}]
}) + "\n")
proc.stdin.flush()
# Loop: read events, respond to tool_use
for line in proc.stdout:
msg = json.loads(line)
if msg["type"] == "tool_use":
# Claude wants to call your tool — execute it
result = my_search_drive(msg["input"]["query"])
proc.stdin.write(json.dumps({
"type": "tool_result",
"id": msg["id"],
"content": json.dumps(result),
"is_error": False
}) + "\n")
proc.stdin.flush()
elif msg["type"] == "response":
print(f"Claude: {msg['content']}")
breakGo (caller):
cmd := exec.Command("node", "claude-native.mjs", "--ndjson")
cmd.Stdin, _ = cmd.StdinPipe()
cmd.Stdout, _ = cmd.StdoutPipe()
cmd.Start()
scanner := bufio.NewScanner(stdout)
encoder := json.NewEncoder(stdin)
// Send a message
encoder.Encode(map[string]any{
"type": "message",
"content": "List files in current directory",
})
// Read responses
for scanner.Scan() {
var msg map[string]any
json.Unmarshal(scanner.Bytes(), &msg)
switch msg["type"] {
case "tool_use":
// Respond with the result
encoder.Encode(map[string]any{
"type": "tool_result", "id": msg["id"],
"content": "file1.txt\nfile2.txt", "is_error": false,
})
case "response":
fmt.Println("Claude:", msg["content"])
return
}
}Node.js (caller):
import { spawn } from "node:child_process";
const sdk = spawn("node", ["claude-native.mjs", "--ndjson"]);
const send = (msg) => sdk.stdin.write(JSON.stringify(msg) + "\n");
let buffer = "";
sdk.stdout.on("data", (chunk) => {
buffer += chunk;
for (const line of buffer.split("\n").slice(0, -1)) {
const msg = JSON.parse(line);
if (msg.type === "ready") {
send({ type: "message", content: "What is 2+2?" });
} else if (msg.type === "response") {
console.log("Claude:", msg.content);
sdk.stdin.end();
}
}
buffer = buffer.split("\n").pop();
});#!/bin/bash
# One-shot
RESPONSE=$(node claude-native.mjs -p "Summarize this file: $(cat README.md)")
echo "$RESPONSE"
# With a specific model
node claude-native.mjs -m opus -p "Review this code" < main.py
# Pipe
cat error.log | node claude-native.mjs -p "Explain this error"# Avec un fichier de config MCP
node claude-native.mjs --mcp-config mcp-servers.json// mcp-servers.json
{
"mcpServers": {
"my-tools": {
"command": "node",
"args": ["my-mcp-server.js"]
}
}
}claude-tool-loop.js is the original bridge that wraps the claude binary via stream-json. It still works but requires the 190MB binary + subscription. The native SDKs replace it entirely.
MIT