Programmatic, scriptable CLI wrapper around Claude Code
Project description
claudecmd
A small, reliable command that drives your local, already-installed Claude Code interactive session programmatically — from scripts, shell pipelines, and editor integrations — and returns the assistant's reply as plain text or a stable JSON envelope.
It drives the interactive session (not claude -p) on purpose: Claude Code
prices -p/headless usage separately from interactive sessions, so claudecmd
keeps automated calls on your interactive (subscription) path. There is no
network server and no API key handling of its own — it shells out to the
claude binary you already use, under a pseudo-terminal.
The PyPI package is
pyclacmd; the installed command isclaudecmd.
claudecmd "say hello"
echo "summarize this" | claudecmd
git diff | claudecmd "Review this diff for risky changes"
claudecmd --json "explain this repo"
claudecmd --session 8f3c… "continue from previous context"
Demo
Requirements
-
macOS (primary target; also works on Linux).
-
Python 3.8+.
-
Claude Code installed and authenticated.
claudecmdinvokes theclaudebinary found on yourPATH. Validate your setup:command -v claude claude --version claude "say hello" # interactive session works for your login
Authentication (
claude /loginor a subscription token) is a prerequisite —claudecmddoes not manage it. Pointclaudecmdat a specific binary with theCLAUDECMD_CLAUDE_BINenvironment variable.
Install
pip install pyclacmd
This provides the claudecmd console script. pexpect and pyte (used to
drive and render the interactive TUI) are installed automatically.
macOS note: if the system
pipis too old, use a virtualenv:python3 -m venv .venv && source .venv/bin/activate pip install pyclacmd
Usage
claudecmd [options] [prompt]
The prompt comes from (in priority order): the positional argument, then piped STDIN. If both are present they are combined:
<positional prompt>
--- STDIN ---
<stdin content>
Flags
| Flag | Description |
|---|---|
--json |
Emit a stable JSON envelope (see below). |
--raw |
Print the full rendered TUI screen (debugging aid). |
--cwd <path> |
Working directory Claude runs in. |
--session <id-or-name> |
Resume a session by UUID or local name. |
--timeout <seconds> |
Abort (and clean up) after N seconds. |
--model <model> |
Model alias (opus, sonnet, haiku) or full id. |
--tools <tools> |
Built-in tools to allow (e.g. "Bash,Read"); "" disables all. |
--permission-mode <mode> |
Claude permission mode (e.g. plan, acceptEdits). |
--system-prompt <text> |
Replace the system prompt. |
--append-system-prompt <text> |
Append to the system prompt. |
--allowed-tools <patterns> |
Permission allow patterns, e.g. "Bash(git:*),Read". |
--disallowed-tools <patterns> |
Permission deny patterns. |
--add-dir <path> |
Extra allowed directory (repeatable). |
--debug |
Emit redacted diagnostics to stderr; keep oversize-stdin temp files. |
--dry-run |
Print the command plan and exit without calling Claude. |
--version / --help |
Standard. |
Examples
# Basic
claudecmd "explain this repo"
# Pipe input as JSON
cat task.md | claudecmd --json
# Review a git diff
git diff | claudecmd "Review for risky changes and return concise findings"
# Unattended: disable tools so no permission prompt can stall the run
claudecmd --tools "" "summarize the open questions in this file"
# See exactly what would run, without running it
claudecmd --dry-run "say hello"
Output contract
Default (human) mode
Prints only the assistant's final reply to stdout, extracted from the rendered session. No banners, no metadata.
--json mode
Emits one JSON object:
{
"ok": true,
"result": "assistant response text",
"session_id": null,
"duration_ms": 6824,
"cost_usd": null,
"raw": null,
"mode": "interactive"
}
session_id, cost_usd, and raw are null — the interactive TUI does not
expose them. On failure:
{ "ok": false, "error": "…", "kind": "claude_timeout", "exit_code": 124, "duration_ms": 1234 }
--raw mode
Prints the full rendered TUI screen unchanged — useful for debugging extraction.
--dry-run mode
Prints the exact command plan (run through the secret redactor) and exits:
{ "ok": true, "dry_run": true, "command": ["claude", "say hello", "--model", "haiku"] }
How it works
claudecmd spawns claude "<prompt>" (interactive, no -p) under a
pseudo-terminal, renders the TUI with a real terminal emulator (pyte) so
layout and whitespace survive, auto-answers the one-time workspace-trust dialog
for --cwd, waits for the turn to settle, and extracts the assistant's reply
from the rendered screen.
Caveats — it scrapes a human-facing TUI, so it is inherently less robust than a headless API:
- No
session_idorcost_usdis available (the TUI does not expose them). - Completion is detected heuristically (the reply settles and the input box
returns). Give long replies a larger
--timeout. - A tool-permission prompt will stall an unattended run — pass
--tools ""to disable tools, or an appropriate--permission-mode. - Extraction is tuned to Claude Code's current TUI (v2.1.x) and may need updating if the interface changes.
Sessions
- A
--sessionvalue that is a UUID is resumed directly (--resume). - A friendly name is looked up in
~/.claudecmd/sessions.json(override the directory withCLAUDECMD_HOME) and resumed if present. Note: because the interactive TUI does not surface the session id,claudecmdcannot record a new name→id mapping on this path — pre-seed names or resume by UUID.
Large input
STDIN larger than the guard (default 9 MB) is not silently truncated. It
is written to a restrictive (0600) temp file under the OS temp directory, and
the prompt references that file path so Claude can read it. The temp file is
deleted after the run unless --debug is set.
Error kinds
Every failure carries a stable kind (the kind field in --json mode; in the
stderr message otherwise) and a non-zero exit code:
| Kind | Exit | Meaning |
|---|---|---|
claude_not_found |
127 | The claude binary was not found / not executable. |
claude_auth_required |
2 | Claude reported an authentication problem. |
claude_timeout |
124 | The run exceeded --timeout. |
cwd_not_found |
66 | --cwd is not an existing directory. |
session_store_error |
74 | The session mapping file could not be read/written. |
pty_unavailable |
69 | pexpect/pyte could not be imported. |
no_prompt |
64 | No prompt via argument or stdin. |
unknown |
1 | Anything else (including no extractable reply). |
Security notes
- Prompts and flags are passed as a subprocess argument array — never interpolated into a shell string, so there is no shell-injection surface.
- Prompts are not written to disk except for the oversize-stdin case above.
- Temp files and the session store use restrictive permissions.
--debugoutput and the--dry-runcommand plan are run through a best-effort secret redactor (API keys, bearer tokens,Authorization:headers,.env-style assignments).- No permission-bypass flags are enabled by default.
--permission-modeis a passthrough: explicitly passingbypassPermissionsopts into Claude Code's auto-approve behavior — a deliberate choice, never a default.
Development
pip install -e ".[dev]"
pytest
The test suite mocks the claude binary / interactive runner, so CI never
requires a real Claude login. GitHub Actions runs pytest on Python 3.12 /
Ubuntu; the package itself supports Python 3.8+.
License
MIT — see LICENSE.
Project details
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 pyclacmd-0.2.1.tar.gz.
File metadata
- Download URL: pyclacmd-0.2.1.tar.gz
- Upload date:
- Size: 26.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
36cfec72dfab79775f56d6a46c337a8369298ac5dc276cc7149f16f104e514fb
|
|
| MD5 |
12163f533bd760f2f8e37c9dbd3ef761
|
|
| BLAKE2b-256 |
96ed2f69b32e216dab5cf4d22989fc28abc9c80ccca0edf6c74e5b933b1ac18a
|
Provenance
The following attestation bundles were made for pyclacmd-0.2.1.tar.gz:
Publisher:
publish.yml on kurok/pyclacmd
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyclacmd-0.2.1.tar.gz -
Subject digest:
36cfec72dfab79775f56d6a46c337a8369298ac5dc276cc7149f16f104e514fb - Sigstore transparency entry: 1770785022
- Sigstore integration time:
-
Permalink:
kurok/pyclacmd@752ecc44a217439040a569e13180841773ccb2c9 -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/kurok
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@752ecc44a217439040a569e13180841773ccb2c9 -
Trigger Event:
release
-
Statement type:
File details
Details for the file pyclacmd-0.2.1-py3-none-any.whl.
File metadata
- Download URL: pyclacmd-0.2.1-py3-none-any.whl
- Upload date:
- Size: 20.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2f964db533d64fdaa8a04fa28d3c0b5bb481fefdb41a47b2ef55c1f1bf072d7a
|
|
| MD5 |
cad15b9edb7e72eed1ad0fa0fe6930d5
|
|
| BLAKE2b-256 |
a538ff1e5bdf6ef596a6f50c4bd8169edbb2388310930c1b6d8971885cd73e40
|
Provenance
The following attestation bundles were made for pyclacmd-0.2.1-py3-none-any.whl:
Publisher:
publish.yml on kurok/pyclacmd
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pyclacmd-0.2.1-py3-none-any.whl -
Subject digest:
2f964db533d64fdaa8a04fa28d3c0b5bb481fefdb41a47b2ef55c1f1bf072d7a - Sigstore transparency entry: 1770785119
- Sigstore integration time:
-
Permalink:
kurok/pyclacmd@752ecc44a217439040a569e13180841773ccb2c9 -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/kurok
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@752ecc44a217439040a569e13180841773ccb2c9 -
Trigger Event:
release
-
Statement type: