CLI Backend Protocol

Plug an external CLI (Claude Code, future: Codex, Gemini) in as a first-class Agent backend

python
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.

Diagram

CLI Backend Flow

📋 Agent Input

🧠 Resolver

🔌 Backend

💻 CLI Process

✅ Result

Quick Start

  1. Simplest: CLI flag
    bash
    praisonai "Refactor utils.py" --cli-backend claude-code
  2. 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
  3. 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
  4. Discover what's registered
    bash
    praisonai backends list
    # claude-code
    # codex-cli
    # grok-cli
    # gemini-cli

How It Works

Diagram
AgentClaudeCodeBackendresolve_cli_backend_configFramework checkYAML/Python loaderUserAgentClaudeCodeBackendresolve_cli_backend_configFramework checkYAML/Python loaderUseralt[framework is praisonai][any other framework]cli_backend = “claude-code” (or dict)framework == “praisonai”?pass-throughinstantiate (or use cached)ready to delegate turnsValueError (fast fail)
StepComponentPurpose
1User/CLISpecifies backend via flag or YAML
2ResolverFactory pattern for backend creation
3BackendProtocol implementation (e.g., ClaudeCodeBackend)
4ProcessExternal CLI subprocess execution
5ResultParsed response returned to Agent

Permission modes (Claude Code backend)

SettingDefault (PR #2122)Notes
--permission-modedefaultWas bypassPermissions
ClaudeCodeBackend(unsafe=...)FalseSet True + env var for bypass

To opt into bypass:

bash
export PRAISONAI_CLAUDE_BYPASS_PERMISSIONS=1
python
from 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

Diagram

Quickest, ad-hoc command

Versioned, per-role config

Need to tweak timeouts/args

Building a custom backend

How do I want to configure a CLI backend?

–cli-backend on the CLI

cli_backend in YAML – string form

cli_backend in YAML – dict with overrides

register_cli_backend in Python


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

bash
export PRAISONAI_CLI_BACKEND_DEBUG=1
# or the standard knob
export LOGLEVEL=DEBUG

Plugin

bash
pip install praisonai-plugins
praisonai plugins enable cli_backend_tracer

Hook (programmatic)

python
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

ShapeExampleBehavior
Omitted_(field absent)_No backend used; Agent uses normal LLM client. No warning logged.
Stringcli_backend: claude-codeResolves via resolve_cli_backend("claude-code").
Dictcli_backend: {id: claude-code, overrides: {timeout_ms: 60000}}Resolves via resolve_cli_backend("claude-code", overrides={...}).
Empty stringcli_backend: ""Raises ValueError("cli_backend string cannot be empty").
Dict missing idcli_backend: {overrides: {...}}Raises ValueError("cli_backend dict must contain an 'id' field").
overrides not a dictcli_backend: {id: claude-code, overrides: "bad"}Raises ValueError("cli_backend.overrides must be a dict").
Invalid typecli_backend: 123Raises ValueError("cli_backend must be string, dict, or instance, got: int").
Unknown idcli_backend: nopeReturns 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).

Diagram

True

False

adapter SUPPORTS_RUNTIME_FEATURES?

✅ cli_backend takes effect

❌ ValueError at config validation

framework valuecli_backend behaviour
praisonai (default)Resolved and used to delegate every agent turn.
crewai, autogen, autogen_v4, ag2Raises 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

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")
python
# 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},
    },
)
python
# 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,
)
ShapeExampleWhen to use
strcli_backend="claude-code"Quickest path. Matches YAML idiom.
dictcli_backend={"id": "claude-code", "overrides": {...}}Need timeout / args / model overrides.
instancecli_backend=resolve_cli_backend("claude-code", overrides=...)Pre-resolved once, reused across agents.
callablecli_backend=my_factoryFactory 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.

IDCLIAuth sourcePage
claude-codeclaudeClaude Pro / MaxClaude Code
codex-clicodex execChatGPT subscriptionCodex CLI
grok-cligrok -pxAI subscriptionGrok CLI
gemini-cligemini -pGoogle accountGemini CLI

Select any of them by ID — the shape is identical to claude-code:

yaml
cli_backend: codex-cli   # ChatGPT subscription
cli_backend: grok-cli    # xAI subscription
cli_backend: gemini-cli  # Google account
python
from 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:

OptionTypeDefaultDescription
commandstr"claude"CLI command (must be on PATH)
argsList[str]["-p", "--output-format", "stream-json", ..., "--permission-mode", "default"]Default permission mode is default (PR #2122; was bypassPermissions)
resume_argsList[str]["-p", "--output-format", "stream-json", "--resume", "{session_id}"]Arguments for resuming sessions
outputstr"jsonl"Output format expected from CLI
inputstr"stdin"How to pass prompts to CLI
live_sessionstr"claude-stdio"Live session mode
model_argstr"--model"CLI argument for model selection
model_aliasesDict[str, str]{"opus": "claude-opus-4-5", "sonnet": "claude-sonnet-4-5", "haiku": "claude-haiku-3-5"}Model name shortcuts
session_argstr"--session-id"CLI argument for session ID
session_modestr"always"When to use sessions
session_id_fieldsList[str]["session_id"]Fields containing session ID
system_prompt_argstr"--append-system-prompt"CLI argument for system prompts
system_prompt_whenstr"first"When to add system prompts
image_argstr"--image"CLI argument for images
clear_envList[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_mcpboolTrueEnable MCP bundling
bundle_mcp_modestr"claude-config-file"MCP bundling mode
serializeboolTrueQueue operations to avoid conflicts
timeout_msint300000Subprocess timeout (5 minutes)

The --cli-backend CLI Flag

FlagTypeBehavior
--cli-backend BACKEND_IDstringChoices 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

CommandBehavior
praisonai backends listPrints each registered backend id on its own line
praisonai backendsSame as list (list is the default)
praisonai backends bogusPrints [red]Unknown backends subcommand: bogus[/red] and the list of valid subcommands

Custom Backends (Advanced)

Register your own CLI backend for custom tools:

python
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:

Diagram
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 protocol
  • config: CliBackendConfig — Configuration object
  • async def execute(prompt, *, session=None, images=None, system_prompt=None, **kwargs) -> CliBackendResult — Single execution
  • async def stream(prompt, **kwargs) -> AsyncIterator[CliBackendDelta] — Streaming execution
  • def 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
yaml
cli_backend:
  id: claude-code
  overrides:
    timeout_ms: 60000  # 1 minute instead of 5

Rather 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.


How this differs from --external-agent


Related