CLI Backend Protocol
Plug an external CLI (Claude Code, future: Codex, Gemini) in as a first-class Agent backend
from praisonaiagents import Agent
agent = Agent(name="cli-worker", instructions="Run this turn via an external CLI backend.", runtime="claude-code")
agent.start("Implement the login form.")The user runs an agent turn; the CLI backend resolver spawns the external CLI and returns the result through the same Agent API.
CLI Backends let you run an agent’s turn through an external CLI tool (like Claude Code) instead of a Python LLM client, while keeping the same Agent/Task API.
Quick Start
- Simplest: CLI flag
bash praisonai "Refactor utils.py" --cli-backend claude-code - Declarative: YAML
yaml framework: praisonai topic: coding roles: coder: role: Code refactorer goal: Refactor Python modules backstory: Senior engineer cli_backend: claude-code # string form tasks: refactor: description: Refactor utils.py expected_output: Refactored code - YAML with overrides
yaml roles: coder: role: Code refactorer goal: Refactor Python modules backstory: Senior engineer cli_backend: id: claude-code overrides: timeout_ms: 60000 tasks: refactor: description: Refactor utils.py expected_output: Refactored code - Discover what's registered
bash praisonai backends list # claude-code # codex-cli # grok-cli # gemini-cli
How It Works
| Step | Component | Purpose |
|---|---|---|
| 1 | User/CLI | Specifies backend via flag or YAML |
| 2 | Resolver | Factory pattern for backend creation |
| 3 | Backend | Protocol implementation (e.g., ClaudeCodeBackend) |
| 4 | Process | External CLI subprocess execution |
| 5 | Result | Parsed response returned to Agent |
Permission modes (Claude Code backend)
| Setting | Default (PR #2122) | Notes |
|---|---|---|
--permission-mode | default | Was bypassPermissions |
ClaudeCodeBackend(unsafe=...) | False | Set True + env var for bypass |
To opt into bypass:
export PRAISONAI_CLAUDE_BYPASS_PERMISSIONS=1from praisonai.cli_backends.claude import ClaudeCodeBackend
backend = ClaudeCodeBackend(config={...}, unsafe=True)With only unsafe=True or only the env var set, the backend overrides to default mode and logs a warning.
Configuration Surfaces
Observability
Confirm that PraisonAI is skipping the LiteLLM HTTP call and delegating to a CLI subprocess.
A user adopts cli_backend="codex-cli" and wants proof delegation is actually happening. They set PRAISONAI_CLI_BACKEND_DEBUG=1, enable cli_backend_tracer, and see one log line per turn — with the prompt already redacted so the log stream is safe to ship to an aggregator. If it silently isn’t delegating, no log line appears — and they know something is wrong at config time.
Env var
export PRAISONAI_CLI_BACKEND_DEBUG=1
# or the standard knob
export LOGLEVEL=DEBUGPlugin
pip install praisonai-plugins
praisonai plugins enable cli_backend_tracerHook (programmatic)
from praisonaiagents import Agent
from praisonaiagents.hooks import HookRegistry, HookEvent, HookResult
registry = HookRegistry()
@registry.on(HookEvent.CLI_BACKEND_EXECUTE)
def trace(event_data):
print(f"{event_data.backend} → {event_data.command}")
return HookResult.allow()
agent = Agent(name="assistant", cli_backend="codex-cli", hooks=registry)
agent.start("Refactor utils.py")See Hook Events → CLI Backend Events for the full payload reference. The command field is redacted at the serialisation boundary — user prompts and system instructions never leak into log sinks.
The cli_backend YAML Field
| Shape | Example | Behavior |
|---|---|---|
| Omitted | _(field absent)_ | No backend used; Agent uses normal LLM client. No warning logged. |
| String | cli_backend: claude-code | Resolves via resolve_cli_backend("claude-code"). |
| Dict | cli_backend: {id: claude-code, overrides: {timeout_ms: 60000}} | Resolves via resolve_cli_backend("claude-code", overrides={...}). |
| Empty string | cli_backend: "" | Raises ValueError("cli_backend string cannot be empty"). |
Dict missing id | cli_backend: {overrides: {...}} | Raises ValueError("cli_backend dict must contain an 'id' field"). |
overrides not a dict | cli_backend: {id: claude-code, overrides: "bad"} | Raises ValueError("cli_backend.overrides must be a dict"). |
| Invalid type | cli_backend: 123 | Raises ValueError("cli_backend must be string, dict, or instance, got: int"). |
| Unknown id | cli_backend: nope | Returns None + logged warning. (Registry lookup, unchanged.) |
Framework Compatibility
cli_backend is a runtime feature — it works only with an adapter whose SUPPORTS_RUNTIME_FEATURES = True (the built-in praisonai adapter has it).
framework value | cli_backend behaviour |
|---|---|
praisonai (default) | Resolved and used to delegate every agent turn. |
crewai, autogen, autogen_v4, ag2 | Raises ValueError: Runtime features (...) are not supported for framework='<x>'. Use a framework whose adapter sets SUPPORTS_RUNTIME_FEATURES = True (e.g. framework='praisonai'). Validation runs before any agent starts, so no partial run is performed. |
Third-party adapters opt in by setting SUPPORTS_RUNTIME_FEATURES = True on their subclass — see Capability Flags.
Using cli_backend in Python
from praisonaiagents import Agent
# 1. String form — same as YAML
agent = Agent(
name="Refactorer",
instructions="Refactor Python files cleanly",
cli_backend="claude-code",
)
agent.start("Refactor utils.py")# 2. Dict form with overrides — now works in Python too (PR #1797)
agent = Agent(
name="Refactorer",
instructions="Refactor Python files cleanly",
cli_backend={
"id": "claude-code",
"overrides": {"timeout_ms": 60000},
},
)# 3. Pre-resolved instance — full control
from praisonai.cli_backends import resolve_cli_backend
backend = resolve_cli_backend("claude-code", overrides={"timeout_ms": 60000})
agent = Agent(
name="Refactorer",
cli_backend=backend,
)| Shape | Example | When to use |
|---|---|---|
str | cli_backend="claude-code" | Quickest path. Matches YAML idiom. |
dict | cli_backend={"id": "claude-code", "overrides": {...}} | Need timeout / args / model overrides. |
| instance | cli_backend=resolve_cli_backend("claude-code", overrides=...) | Pre-resolved once, reused across agents. |
callable | cli_backend=my_factory | Factory function returning a backend. |
Built-in backends
Four backends ship registered out of the box. Each routes through a CLI that owns its own subscription/OAuth session, so no raw API key is needed.
| ID | CLI | Auth source | Page |
|---|---|---|---|
claude-code | claude | Claude Pro / Max | Claude Code |
codex-cli | codex exec | ChatGPT subscription | Codex CLI |
grok-cli | grok -p | xAI subscription | Grok CLI |
gemini-cli | gemini -p | Google account | Gemini CLI |
Select any of them by ID — the shape is identical to claude-code:
cli_backend: codex-cli # ChatGPT subscription
cli_backend: grok-cli # xAI subscription
cli_backend: gemini-cli # Google accountfrom praisonaiagents import Agent
agent = Agent(name="assistant", cli_backend="codex-cli")
agent.start("Hello")Built-in Backend: claude-code
The claude-code backend executes commands via the Claude Code CLI with these default settings:
| Option | Type | Default | Description |
|---|---|---|---|
command | str | "claude" | CLI command (must be on PATH) |
args | List[str] | ["-p", "--output-format", "stream-json", ..., "--permission-mode", "default"] | Default permission mode is default (PR #2122; was bypassPermissions) |
resume_args | List[str] | ["-p", "--output-format", "stream-json", "--resume", "{session_id}"] | Arguments for resuming sessions |
output | str | "jsonl" | Output format expected from CLI |
input | str | "stdin" | How to pass prompts to CLI |
live_session | str | "claude-stdio" | Live session mode |
model_arg | str | "--model" | CLI argument for model selection |
model_aliases | Dict[str, str] | {"opus": "claude-opus-4-5", "sonnet": "claude-sonnet-4-5", "haiku": "claude-haiku-3-5"} | Model name shortcuts |
session_arg | str | "--session-id" | CLI argument for session ID |
session_mode | str | "always" | When to use sessions |
session_id_fields | List[str] | ["session_id"] | Fields containing session ID |
system_prompt_arg | str | "--append-system-prompt" | CLI argument for system prompts |
system_prompt_when | str | "first" | When to add system prompts |
image_arg | str | "--image" | CLI argument for images |
clear_env | List[str] | ["ANTHROPIC_API_KEY", "ANTHROPIC_BASE_URL", "ANTHROPIC_OAUTH_TOKEN", "CLAUDE_CODE_USE_BEDROCK", "CLAUDE_CODE_USE_VERTEX", "CLAUDE_CONFIG_DIR", "CLAUDE_CODE_OAUTH_TOKEN", "OTEL_EXPORTER_OTLP_ENDPOINT", "OTEL_EXPORTER_OTLP_HEADERS", "OTEL_RESOURCE_ATTRIBUTES", "GOOGLE_APPLICATION_CREDENTIALS", "AWS_PROFILE", "AWS_REGION", "AWS_ACCESS_KEY_ID", "AWS_SECRET_ACCESS_KEY", "AWS_SESSION_TOKEN"] | Environment variables sanitized before subprocess |
bundle_mcp | bool | True | Enable MCP bundling |
bundle_mcp_mode | str | "claude-config-file" | MCP bundling mode |
serialize | bool | True | Queue operations to avoid conflicts |
timeout_ms | int | 300000 | Subprocess timeout (5 minutes) |
The --cli-backend CLI Flag
| Flag | Type | Behavior |
|---|---|---|
--cli-backend BACKEND_ID | string | Choices populated dynamically from list_cli_backends(). Currently: claude-code, codex-cli, grok-cli, gemini-cli. |
--cli-backend X --external-agent Y | — | Mutually exclusive — argparse rejects with “not allowed with argument” |
Unknown id (e.g. --cli-backend bogus) | — | Rejected by argparse with “invalid choice” |
The backends Subcommand
| Command | Behavior |
|---|---|
praisonai backends list | Prints each registered backend id on its own line |
praisonai backends | Same as list (list is the default) |
praisonai backends bogus | Prints [red]Unknown backends subcommand: bogus[/red] and the list of valid subcommands |
Custom Backends (Advanced)
Register your own CLI backend for custom tools:
from praisonai.cli_backends import register_cli_backend
from praisonaiagents import CliBackendConfig
def my_backend_factory():
from my_pkg import MyBackend
return MyBackend(config=CliBackendConfig(command="my-cli"))
register_cli_backend("my-backend", my_backend_factory)After registering, praisonai backends list shows it, --cli-backend my-backend accepts it, and cli_backend: my-backend works in YAML.
CliBackendProtocol Reference
For backend authors implementing the protocol:
graph LR
subgraph "CliBackendProtocol"
A[config: CliBackendConfig]
B[async execute(...)]
C[async stream(...)]
D[capabilities()]
end
classDef protocol fill:#8B0000,stroke:#7C90A0,color:#fff
class A,B,C,D protocolconfig: CliBackendConfig— Configuration objectasync def execute(prompt, *, session=None, images=None, system_prompt=None, **kwargs) -> CliBackendResult— Single executionasync def stream(prompt, **kwargs) -> AsyncIterator[CliBackendDelta]— Streaming executiondef capabilities() -> RuntimeCapabilityMatrix— Required (new). Returns the capability matrix for this backend.
Best Practices
Prefer YAML for production
Use the YAML cli_backend: field for versioned, declarative configuration. Use --cli-backend flag for quick one-off commands and testing.
Set timeout overrides for slow CLIs
cli_backend:
id: claude-code
overrides:
timeout_ms: 60000 # 1 minute instead of 5Rather than monkey-patching, use the overrides system for custom timeouts.
Use a framework whose adapter sets SUPPORTS_RUNTIME_FEATURES
The cli_backend field requires an adapter whose SUPPORTS_RUNTIME_FEATURES = True — the built-in framework: praisonai (the default) qualifies. PraisonAI validates this up front — if you try it with crewai, autogen, autogen_v4, or ag2, you’ll get a ValueError immediately, before any agent runs.
Don't combine with –external-agent
The --cli-backend flag and --external-agent flag are mutually exclusive. Pick one approach:
- CLI Backends (new): Pluggable, configurable, YAML-supported
- External Agent (legacy): Class-based, limited configuration
Ensure claude CLI is on PATH
The claude-code backend requires the claude CLI to be installed and accessible. Install via the Claude Code SDK or ensure it’s in your system PATH.