ACP-compatible agent for Claude Code (Python version)
Project description
Claude Code ACP (Python)
Python implementation of ACP (Agent Client Protocol) for Claude Code.
This package bridges the Claude Agent SDK with the Agent Client Protocol (ACP), providing two ways to use Claude:
- ACP Server - Connect Claude to any ACP-compatible editor (Zed, Neovim, JetBrains, etc.)
- Python Client - Event-driven API for building Python applications with Claude
Features
- Uses Claude CLI subscription - No API key needed, uses your existing Claude subscription
- Full ACP protocol support - Compatible with Zed, Neovim, and other ACP clients
- Bidirectional communication - Permission requests, tool calls, streaming responses
- Event-driven Python API - Decorator-based handlers for easy integration
- Session management - Create, fork, resume, list sessions
- Multiple permission modes - default, acceptEdits, plan, bypassPermissions
Installation
pip install claude-code-acp
Or with uv:
uv tool install claude-code-acp
Requirements
- Python 3.10+
- Claude CLI installed and authenticated (
claude /login)
Components
| Class | Type | Description |
|---|---|---|
ClaudeAcpAgent |
ACP Server | For editors (Zed, Neovim) to connect |
ClaudeClient |
Python API | Event-driven wrapper (uses agent internally) |
AcpClient |
ACP Client | Connect to any ACP agent via subprocess |
Usage 1: ACP Server for Editors
Run as an ACP server to connect Claude to your editor:
claude-code-acp
Zed Editor
Add to your Zed settings.json:
{
"agent_servers": {
"Claude Code Python": {
"type": "custom",
"command": "claude-code-acp",
"args": [],
"env": {}
}
}
}
Then open the Agent Panel (Ctrl+? / Cmd+?) and select "Claude Code Python" from the + menu.
Other Editors
Any ACP-compatible client can connect by spawning claude-code-acp as a subprocess and communicating via stdio.
Usage 2: Python Event-Driven API
Use ClaudeClient for building Python applications with Claude:
import asyncio
from claude_code_acp import ClaudeClient
async def main():
client = ClaudeClient(cwd=".")
@client.on_text
async def handle_text(text: str):
"""Called for each text chunk from Claude."""
print(text, end="", flush=True)
@client.on_tool_start
async def handle_tool_start(tool_id: str, name: str, input: dict):
"""Called when Claude starts using a tool."""
print(f"\n๐ง {name}")
@client.on_tool_end
async def handle_tool_end(tool_id: str, status: str, output):
"""Called when a tool completes."""
icon = "โ
" if status == "completed" else "โ"
print(f" {icon}")
@client.on_permission
async def handle_permission(name: str, input: dict) -> bool:
"""Called when Claude needs permission. Return True to allow."""
print(f"๐ Permission requested: {name}")
return True # or prompt user
@client.on_complete
async def handle_complete():
"""Called when the query completes."""
print("\n--- Done ---")
# Send a query
response = await client.query("Create a hello.py file that prints Hello World")
print(f"\nFull response: {response}")
asyncio.run(main())
Event Handlers
| Decorator | Arguments | Description |
|---|---|---|
@client.on_text |
(text: str) |
Streaming text chunks from Claude |
@client.on_thinking |
(text: str) |
Thinking/reasoning blocks |
@client.on_tool_start |
(tool_id, name, input) |
Tool execution started |
@client.on_tool_end |
(tool_id, status, output) |
Tool execution completed |
@client.on_permission |
(name, input) -> bool |
Permission request (return True/False) |
@client.on_error |
(exception) |
Error occurred |
@client.on_complete |
() |
Query completed |
Client Methods
# Start a new session
session_id = await client.start_session()
# Send a query (returns full response text)
response = await client.query("Your prompt here")
# Set permission mode
await client.set_mode("acceptEdits") # or "default", "plan", "bypassPermissions"
Usage 3: ACP Client (Connect to Any Agent)
Use AcpClient to connect to any ACP-compatible agent:
import asyncio
from claude_code_acp import AcpClient
async def main():
# Connect to claude-code-acp (Python version)
client = AcpClient(command="claude-code-acp")
# Or connect to the TypeScript version
# client = AcpClient(command="npx", args=["@zed-industries/claude-code-acp"])
# Or any other ACP agent
# client = AcpClient(command="my-custom-agent")
@client.on_text
async def handle_text(text: str):
print(text, end="", flush=True)
@client.on_tool_start
async def handle_tool(tool_id: str, name: str, input: dict):
print(f"\n๐ง {name}")
@client.on_permission
async def handle_permission(name: str, input: dict, options: list) -> str:
"""Return option_id: 'allow', 'reject', or 'allow_always'"""
print(f"๐ Permission: {name}")
return "allow"
@client.on_complete
async def handle_complete():
print("\n--- Done ---")
async with client:
response = await client.prompt("What files are here?")
asyncio.run(main())
Connect to Different Agents
from claude_code_acp import AcpClient
# Connect to our Claude ACP server
claude = AcpClient(command="claude-code-acp")
# Connect to Gemini CLI
gemini = AcpClient(command="gemini", args=["--experimental-acp"])
# Connect to TypeScript version
ts_claude = AcpClient(command="npx", args=["@zed-industries/claude-code-acp"])
File Operation Handlers
AcpClient supports intercepting file read/write operations for security or custom handling:
@client.on_file_read
async def handle_read(path: str) -> str | None:
"""Intercept file reads. Return content to override, or None to proceed."""
print(f"๐ Reading: {path}")
return None # Proceed with normal read
@client.on_file_write
async def handle_write(path: str, content: str) -> bool:
"""Intercept file writes. Return True to allow, False to block."""
print(f"๐ Writing: {path}")
response = input("Allow write? [y/N]: ")
return response.lower() == "y"
Terminal Operation Handlers
AcpClient supports intercepting terminal/shell execution for security:
@client.on_terminal_create
async def handle_terminal(command: str, cwd: str) -> bool:
"""Intercept shell commands. Return True to allow, False to block."""
print(f"๐ฅ๏ธ Command: {command} in {cwd}")
response = input("Allow execution? [y/N]: ")
return response.lower() == "y"
@client.on_terminal_output
async def handle_output(terminal_id: str, output: str) -> None:
"""Receive terminal output in real-time."""
print(output, end="")
AcpClient vs ClaudeClient
| Feature | ClaudeClient |
AcpClient |
|---|---|---|
| Uses | Claude Agent SDK directly | Any ACP agent via subprocess |
| Connection | In-process | Subprocess + stdio |
| Agents | Claude only | Any ACP-compatible agent |
| Use case | Simple Python apps | Multi-agent, testing, flexibility |
Tested Agents
| Agent | Command | Status |
|---|---|---|
| claude-code-acp (this package) | claude-code-acp |
โ Works |
| Gemini CLI | gemini --experimental-acp |
โ Works |
| TypeScript version | npx @zed-industries/claude-code-acp |
โ Compatible |
Architecture
This package provides three ways to use Claude:
Method A: Editor via ACP (ClaudeAcpAgent)
For Zed, Neovim, and other ACP-compatible editors:
โโโโโโโโโโโโ ACP Protocol โโโโโโโโโโโโโโโโโโโ SDK โโโโโโโโโโโโโโ
โ Zed โ โโโโโโ stdio โโโโโโโโบ โ ClaudeAcpAgent โ โโโโโโโโโโโบ โ Claude CLI โ
โ Editor โ โ (ACP Server) โ โ โ
โโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโ
Method B: Python Direct (ClaudeClient)
For Python apps that want simple, direct access to Claude (no ACP protocol):
โโโโโโโโโโโโ direct call โโโโโโโโโโโโโโโโโโโ SDK โโโโโโโโโโโโโโ
โ Python โ โโโโ in-process โโโโบ โ ClaudeClient โ โโโโโโโโโโโบ โ Claude CLI โ
โ App โ โ โ โ โ
โโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโโโ
Method C: Python via ACP (AcpClient)
For Python apps that want to connect to any ACP-compatible agent:
โโโโโโโโโโโโ ACP Protocol โโโโโโโโโโโโโโโโโโโ
โ Python โ โโโโโโ stdio โโโโโโโโบ โ Any ACP Agent โ
โ App โ โ โ
โ โ โ โข claude-code โ
โ AcpClientโ โ โข gemini โ
โ โ โ โข custom agents โ
โโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโ
Summary
| Component | Uses ACP? | Purpose |
|---|---|---|
ClaudeAcpAgent |
Yes (Server) | Let editors connect to Claude |
ClaudeClient |
No | Simplest way for Python apps |
AcpClient |
Yes (Client) | Connect to any ACP agent |
What We Built
This project combines two official SDKs to create a complete Python solution:
Integrated Components
| Component | Source | Purpose |
|---|---|---|
| Agent Client Protocol SDK | Anthropic | ACP server/client protocol implementation |
| Claude Agent SDK | Anthropic | Claude CLI wrapper with streaming support |
Our Contributions
-
ClaudeAcpAgent (
agent.py)- Bridges Claude Agent SDK with ACP protocol
- Converts Claude messages to ACP session updates
- Handles bidirectional permission requests
- Session management (create, fork, resume, list)
-
ClaudeClient (
client.py)- Event-driven Python API with decorators
- Smart text deduplication for streaming
- Simple permission handling
- Clean async/await interface
-
ACP Server Entry Point
- Standalone
claude-code-acpcommand - Direct integration with Zed and other ACP clients
- No configuration needed
- Standalone
Why This Package?
| Approach | API Key | Subscription | ACP Support | Event-Driven |
|---|---|---|---|---|
| Anthropic API directly | โ Required | โ | โ | โ |
| Claude Agent SDK | โ | โ Uses CLI | โ | Partial |
| claude-code-acp | โ | โ Uses CLI | โ Full | โ Full |
Examples
Simple Chat
import asyncio
from claude_code_acp import ClaudeClient
async def main():
client = ClaudeClient()
@client.on_text
async def on_text(text):
print(text, end="")
while True:
user_input = input("\nYou: ")
if user_input.lower() == "quit":
break
await client.query(user_input)
asyncio.run(main())
File Operations with Permission Control
import asyncio
from claude_code_acp import ClaudeClient
async def main():
client = ClaudeClient(cwd="/path/to/project")
@client.on_text
async def on_text(text):
print(text, end="")
@client.on_permission
async def on_permission(name, input):
response = input(f"Allow '{name}'? [y/N]: ")
return response.lower() == "y"
await client.query("Refactor the main.py file to use async/await")
asyncio.run(main())
Auto-approve Mode
import asyncio
from claude_code_acp import ClaudeClient
async def main():
client = ClaudeClient(cwd=".")
# Bypass all permission checks
await client.set_mode("bypassPermissions")
@client.on_text
async def on_text(text):
print(text, end="")
await client.query("Create a complete Flask app with tests")
asyncio.run(main())
Development
# Clone
git clone https://github.com/yazelin/claude-code-acp-py
cd claude-code-acp-py
# Install dependencies
uv sync
# Run locally
uv run claude-code-acp
# Run tests
uv run python -c "from claude_code_acp import ClaudeClient; print('OK')"
Related Projects
- claude-code-acp - TypeScript version by Zed Industries
- agent-client-protocol - ACP specification and SDKs
- claude-agent-sdk-python - Official Claude Agent SDK
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file claude_code_acp-0.3.4.tar.gz.
File metadata
- Download URL: claude_code_acp-0.3.4.tar.gz
- Upload date:
- Size: 61.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6d37655f162d22a24c6ef410673c505de1b7bfe51ac830b2109351b1045cf7f5
|
|
| MD5 |
72ace044807d683a63df0ba552b8ced6
|
|
| BLAKE2b-256 |
e3b9b18796a366ba039b195ae3b6721549530ceba3125b6c2ba656b0f3cf1747
|
Provenance
The following attestation bundles were made for claude_code_acp-0.3.4.tar.gz:
Publisher:
publish.yml on yazelin/claude-code-acp-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
claude_code_acp-0.3.4.tar.gz -
Subject digest:
6d37655f162d22a24c6ef410673c505de1b7bfe51ac830b2109351b1045cf7f5 - Sigstore transparency entry: 909911862
- Sigstore integration time:
-
Permalink:
yazelin/claude-code-acp-py@783327381cf0c56b1870ff77ab5bd644cc6ccbb2 -
Branch / Tag:
refs/tags/v0.3.4 - Owner: https://github.com/yazelin
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@783327381cf0c56b1870ff77ab5bd644cc6ccbb2 -
Trigger Event:
release
-
Statement type:
File details
Details for the file claude_code_acp-0.3.4-py3-none-any.whl.
File metadata
- Download URL: claude_code_acp-0.3.4-py3-none-any.whl
- Upload date:
- Size: 20.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
85de6346ba9df5623d5251138467a2fd9ea5f5e3bed1d8ad8f30aebda7972139
|
|
| MD5 |
060ee3ccf186c3a0c104f679ad78dfec
|
|
| BLAKE2b-256 |
f30aee1b745d0265be3a4161b58ecd42cf2a85c09a9fd796e4d6bb03e0c12582
|
Provenance
The following attestation bundles were made for claude_code_acp-0.3.4-py3-none-any.whl:
Publisher:
publish.yml on yazelin/claude-code-acp-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
claude_code_acp-0.3.4-py3-none-any.whl -
Subject digest:
85de6346ba9df5623d5251138467a2fd9ea5f5e3bed1d8ad8f30aebda7972139 - Sigstore transparency entry: 909911866
- Sigstore integration time:
-
Permalink:
yazelin/claude-code-acp-py@783327381cf0c56b1870ff77ab5bd644cc6ccbb2 -
Branch / Tag:
refs/tags/v0.3.4 - Owner: https://github.com/yazelin
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@783327381cf0c56b1870ff77ab5bd644cc6ccbb2 -
Trigger Event:
release
-
Statement type: