Skip to main content

pyclaudecli

PyPI Python versions License: MIT Downloads

A Python library that wraps the Claude Code CLI (claude) so you can drive it from Python instead of shelling out by hand: one-shot prompts, JSON/streaming output, background agents, authentication, MCP servers, plugins, and the rest of the CLI's surface.

It's a thin wrapper, not a reimplementation — every call runs the real claude binary, so it always reflects whatever version, auth, and config you have installed locally.

Contents

Install

pip install pyclaudecli

Requires the claude CLI itself to be installed and on PATH (see the Claude Code docs).

Runs on Linux, macOS and Windows, Python 3.8+. On Windows the CLI is installed as claude.cmd, which CreateProcess won't find from a bare "claude" — the wrapper resolves it through PATHEXT for you, so the default ClaudeCLI() works there too.

Quickstart

from pyclaudecli import ClaudeCLI

claude = ClaudeCLI()

# One-shot prompt
print(claude.prompt("Summarize this repo's README.", model="haiku"))

Setup

from pyclaudecli import ClaudeCLI

# Defaults: binary="claude" on PATH, inherited cwd/env, no timeout
claude = ClaudeCLI()

# Pointing at a specific binary/project, with a default timeout for every call
claude = ClaudeCLI(
    "claude",
    cwd="/path/to/project",
    env={"ANTHROPIC_API_KEY": "sk-ant-..."},
    timeout=120,
)

env is merged over the current environment, so PATH, HOME and the rest still reach the CLI — passing one API key doesn't cost you the ability to find the binary or its credentials. Pass replace_env=True if you really want the child to start from a clean slate:

claude = ClaudeCLI(env={"PATH": "/usr/bin", "HOME": "/tmp/sandbox"}, replace_env=True)

Authentication

claude handles auth itself, so pyclaudecli just drives it. You have two options.

API key

Pass the key through the environment — either inherited from your shell or set per client:

from pyclaudecli import ClaudeCLI

claude = ClaudeCLI(env={"ANTHROPIC_API_KEY": "sk-ant-..."})

OAuth login (paste the code)

auth_login() runs claude auth login, prints the sign-in URL, waits for the CLI's "Paste code here" prompt, and writes your code back to it. With no arguments it reads the code from stdin, so this is the whole interactive flow:

from pyclaudecli import ClaudeCLI

claude = ClaudeCLI()

if not claude.auth_status().get("loggedIn"):
    # Prints the sign-in URL, then asks: "Paste the code from the browser here:"
    exit_code = claude.auth_login()
    print("login exit code:", exit_code)

print(claude.auth_status())   # {"loggedIn": True, "email": "you@example.com", ...}

To capture the URL yourself (open it in a browser, send it to a chat, log it) and paste the code back without stdin, use on_output plus code_provider:

import re

URL_RE = re.compile(r"https://\S+")
login_url = None

def capture(chunk: str) -> None:
    global login_url
    print(chunk, end="", flush=True)          # or log.info(chunk)
    if login_url is None:
        match = URL_RE.search(chunk)
        if match:
            login_url = match.group(0)

def supply_code(url: str) -> str:
    # `url` is the sign-in URL pyclaudecli found in the CLI's output.
    # Open it however you like, then return the code the browser shows.
    print(f"\nOpen this URL and approve the login:\n{url}\n")
    return input("Paste code here: ").strip()

claude.auth_login(
    on_output=capture,
    code_provider=supply_code,
    console=True,      # --console: force the URL/console flow instead of opening a browser
    timeout=300,       # how long to wait for the "Paste code here" prompt
)

Fully non-interactive — when the code already came from somewhere else (a queue, a browser-automation step, an operator pasting it into your own UI):

claude.auth_login(code="123456")

# Or fetch it programmatically from the printed sign-in URL
claude.auth_login(code_provider=lambda url: fetch_code_from_my_browser(url))

# Enterprise SSO, or pre-filling the account
claude.auth_login(sso=True)
claude.auth_login(email="you@example.com")

auth_login() returns the CLI's exit code (0 on success) and raises ClaudeTimeoutError if the login prompt never appears within timeout seconds.

Signing out, and long-lived tokens for CI:

claude.auth_logout()
claude.setup_token()   # interactive; sets up a long-lived auth token

Command-line usage

The package also installs a minimal pyclaudecli command (and python -m pyclaudecli) for quick one-off prompts — for anything beyond that, use ClaudeCLI directly.

pyclaudecli "What's 2+2?"                    # defaults to model "haiku"
pyclaudecli --model sonnet "Explain this diff"
pyclaudecli -m sonnet "Explain this diff"

python -m pyclaudecli "Hello, Claude!"

Errors

All CLI failures raise ClaudeCLIError (or ClaudeNotFoundError / ClaudeTimeoutError), carrying returncode, stdout, stderr, and cmd.

from pyclaudecli import ClaudeCLI, ClaudeCLIError, ClaudeNotFoundError, ClaudeTimeoutError

claude = ClaudeCLI()

try:
    claude.prompt("Do something", model="haiku", timeout=30)
except ClaudeTimeoutError as exc:
    print("timed out:", exc.cmd)
except ClaudeNotFoundError as exc:
    print("claude binary not on PATH:", exc)
except ClaudeCLIError as exc:
    print(exc.returncode, exc.stdout, exc.stderr)

build_flags

The helper ClaudeCLI uses internally to turn a {python_name: value} dict into CLI flags — handy if you're composing your own extra_flags or calling .run() directly. None/False are omitted, True becomes a bare flag, lists/tuples repeat the flag followed by each value, and anything else becomes the flag plus str(value).

from pyclaudecli import build_flags

build_flags({"model": "haiku", "verbose": True, "quiet": False, "add_dir": ["a", "b"]})
# ["--model", "haiku", "--verbose", "--add-dir", "a", "b"]

Every method, with an example

All examples assume:

from pyclaudecli import ClaudeCLI

claude = ClaudeCLI()

Raw / internal

# .run() — lowest-level call; returns a CommandResult(returncode, stdout, stderr)
result = claude.run(["--version"], check=False)
print(result.returncode, result.stdout)

Version / health

claude.version()   # "claude --version" -> "1.2.3"
claude.doctor()    # "claude doctor" -> health-check report
claude.update()    # "claude update" -> checks for and installs updates

claude.install()                       # "claude install" (latest default)
claude.install("stable")               # "claude install stable"
claude.install("1.2.3", force=True)    # "claude install 1.2.3 --force"

Prompting

# One-shot prompt -> plain text
claude.prompt("Summarize this repo's README.", model="haiku")

# Structured result (cost, session id, etc.)
result = claude.prompt_json("What's 2+2?", model="haiku")
print(result["result"], result["total_cost_usd"])

# Live streaming events
for event in claude.prompt_stream("Write a haiku about tests.", model="haiku"):
    print(event["type"])

# Full option surface
claude.prompt(
    "Refactor this function for clarity.",
    model="sonnet",
    output_format="json",
    system_prompt="You are a terse senior engineer.",
    append_system_prompt="Always answer in bullet points.",
    allowed_tools=["Read", "Edit"],
    disallowed_tools=["Bash"],
    add_dir=["../shared-lib"],
    permission_mode="acceptEdits",
    mcp_config=["./mcp.json"],
    settings="./claude-settings.json",
    resume="session-id-123",
    fork_session=True,
    effort="high",
    fallback_model="haiku",
    max_budget_usd=0.50,
    json_schema='{"type": "object"}',
    betas=["some-beta-flag"],
    no_session_persistence=True,
    dangerously_skip_permissions=False,
    restricted=True,
    input_text="piped stdin content",
    timeout=60,
    extra_flags={"worktree": True},  # anything without a named parameter
)

# continue_session=True appends --continue (keeps typing in the latest session)
claude.prompt("And now add tests for it.", continue_session=True)

Background sessions ("agents")

session_id = claude.start_background("Refactor the auth module", model="sonnet")

claude.list_agents()                          # all sessions, as a list of dicts
claude.list_agents(all=True, cwd="/repo")      # include finished ones, scoped to a project

claude.attach(session_id)                      # interactive; requires a real TTY
claude.logs(session_id)                        # raw terminal snapshot
claude.logs(session_id, strip_ansi=True)       # best-effort plain text

claude.stop(session_id)                        # stop, keep state
claude.respawn(session_id)                     # restart one stopped session
claude.respawn(all=True)                       # restart every stopped session
claude.rm(session_id)                          # delete a stopped session

Auth

claude.auth_status()                # {"loggedIn": True, "email": "...", ...}
claude.auth_status(as_json=False)   # human-readable text instead

# Interactive OAuth: prints the sign-in URL, then reads the pasted code from stdin
claude.auth_login()

# Non-interactive login: supply the code yourself
claude.auth_login(code="123456")

# Or fetch the code programmatically from the printed sign-in URL
claude.auth_login(code_provider=lambda url: fetch_code_from_my_browser(url))

# Route the printed sign-in URL/output somewhere other than stdout
claude.auth_login(on_output=lambda chunk: log.info(chunk), console=True, timeout=120)

claude.auth_login(sso=True)                      # enterprise SSO
claude.auth_login(email="you@example.com")       # pre-fill the account

claude.auth_logout()
claude.setup_token()   # interactive; sets up a long-lived auth token

MCP servers

claude.mcp_list()
claude.mcp_get("sentry")

claude.mcp_add("sentry", "https://mcp.sentry.dev/mcp", transport="http")
claude.mcp_add(
    "local-tool", "node", "server.js",
    transport="stdio", env=["API_KEY=abc"], scope="project",
)

claude.mcp_add_json("sentry", {"type": "http", "url": "https://mcp.sentry.dev/mcp"})
claude.mcp_add_from_claude_desktop(scope="user")

claude.mcp_remove("sentry", scope="project")
claude.mcp_login("sentry")             # interactive OAuth
claude.mcp_logout("sentry")
claude.mcp_reset_project_choices()

Plugins

claude.plugin_list()
claude.plugin_list(as_json=True)

claude.plugin_install("some-plugin", yes=True)
claude.plugin_install("some-plugin@my-marketplace", scope="user", as_json=True)

claude.plugin_uninstall("some-plugin", yes=True, prune=True)
claude.plugin_enable("some-plugin")
claude.plugin_disable("some-plugin")
claude.plugin_disable(all=True)        # disable every plugin

claude.plugin_update("some-plugin", yes=True)
claude.plugin_details("some-plugin")
claude.plugin_validate("./my-plugin", strict=True, as_json=True)
claude.plugin_prune(dry_run=True)

claude.plugin_marketplace_list()
claude.plugin_marketplace_add("https://github.com/org/marketplace-repo")
claude.plugin_marketplace_remove("marketplace-name")
claude.plugin_marketplace_update()             # update all
claude.plugin_marketplace_update("marketplace-name")

Project state

claude.project_purge()                 # purge state for the current project
claude.project_purge("/path/to/repo")  # or for a specific path

Auto mode

claude.auto_mode_config()      # effective classifier config, as a dict
claude.auto_mode_defaults()    # shipped default rules, as a dict
claude.auto_mode_reset()       # remove custom rules from user settings
claude.auto_mode_critique()    # AI feedback on your custom rules

Misc

claude.import_config("cursor", dry_run=True, yes=True)   # source: codex, gemini, or cursor

claude.ultrareview()                                 # review the current branch
claude.ultrareview("main")                            # review against a base branch
claude.ultrareview(482, as_json=True, post=True)       # review a PR, post results, get JSON

# Long-running server: returns a Popen handle, doesn't block
gateway = claude.start_gateway(config="./gateway.json")
...
gateway.terminate()

See ClaudeCLI's docstrings for the exact CLI flags behind each method — it covers every top-level claude command (auth, mcp, plugin, project, agents/background sessions, auto-mode, doctor, update, install, import, ultrareview, gateway) plus the main prompt flags. Anything not exposed as a named parameter can still be passed through via each method's extra_flags dict.

Security notes

A few things the wrapper does on your behalf, worth knowing if you're feeding it input from anywhere but your own code:

Prompts can't smuggle in CLI flags. prompt(), prompt_stream(), start_background() and the pyclaudecli command put -- between the options and your text, so a prompt that starts with a dash is text, not a flag. Without it, user-supplied input like --dangerously-skip-permissions or --settings /tmp/evil.json would be parsed by the CLI:

claude.prompt("--version")   # asks Claude about "--version"; does not run the flag

Flags you actually want still go through named parameters or extra_flags. Note this protection covers the prompt text — if you interpolate untrusted input into a flag value (model=, settings=, add_dir=), validate it yourself.

Errors don't spill credentials. ClaudeCLIError.args/.cmd and the exception message are redacted before they're raised, so MCP auth headers, injected --env values, tokens inside an mcp add-json payload and anything shaped like sk-ant-…, Bearer … or password=… come back as <redacted>. Flag names survive so the command is still recognisable, and long values are truncated. Tracebacks and log aggregators get the redacted form; exc.stdout/exc.stderr still hold the raw output for local debugging.

Login codes are handled as single-use secrets. auth_login() writes exactly one line to the CLI's stdin — a code containing a newline can't inject extra input — and drops the code and the captured output from memory once it's submitted. The child process and its pipes are always cleaned up, including when your code_provider raises.

Two things it deliberately does not do: it never runs a shell (every call is an argv list, so there's no shell-injection surface), and it doesn't manage credentials itself — auth lives with the claude CLI and your environment. Remember the argv of a running process is visible to other users on the same machine via ps, so prefer env vars over flags for anything sensitive.

Every call is an argv list on every platform — no shell, and no shell=True fallback on Windows, so quoting rules can't be turned into an injection.

Development

git clone https://github.com/Suriya-Ravichandran/pyclaudecli.git
cd pyclaudecli
pip install -e .
pytest          # offline; does not invoke the claude binary

License

MIT

Release files for pyclaudecli 1.0.4

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pyclaudecli 1.0.4
File Size Uploaded
pyclaudecli-1.0.4.tar.gz 23.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pyclaudecli 1.0.4
File Interpreter ABI Platform
pyclaudecli-1.0.4-py3-none-any.whl Python 3 none any Details

Total release size: 43.7 kB

Release files / pyclaudecli-1.0.4.tar.gz

Download URL pyclaudecli-1.0.4.tar.gz
Size 23.0 kB
Tags Source
SHA-256 checksum
How to use checksums
f836630bb451bb2769a7eec3ea54fffb36b1f841756f7970b24407ef48e8fbc3
BLAKE2b-256 checksum
How to use checksums
27528454455a069c47ce56d3af6338d139ed343654992be92c0001c83f21e695
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.14 {"installer":{"name":"uv","version":"0.12.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / pyclaudecli-1.0.4-py3-none-any.whl

Download URL pyclaudecli-1.0.4-py3-none-any.whl
Size 20.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
4a984e8fc73a79661fb34f1d5b72987595f4b3ac094de12ca808a1996e599f28
BLAKE2b-256 checksum
How to use checksums
734403b0156d7e3755c8e033e82cc3aefc5ba6a8d5a860caa95674eb2f8b7bd3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.12.14 {"installer":{"name":"uv","version":"0.12.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

1.0.6

2 release files

1.0.5

2 release files

This release

1.0.4 This release

2 release files

1.0.3

2 release files

1.0.2

2 release files

1.0.1

2 release files

1.0.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page