Programmatic, scriptable CLI wrapper around Claude Code
Project description
claudecmd
A small, reliable command-line facade that lets scripts, shell pipelines, and editor integrations call your local, already-installed Claude Code programmatically — with a stable output contract, session handling, timeouts, structured errors, and an optional PTY fallback.
By default it is a thin wrapper around claude -p (Claude Code's print/headless
mode). It can also drive the interactive session programmatically
(--interactive) — useful because Claude Code prices -p/headless usage
separately from interactive sessions, so --interactive keeps scripted calls on
your subscription. Either way there is no network server and no API key handling
of its own — it shells out to the claude binary you already use.
claudecmd "say hello"
echo "summarize this" | claudecmd
git diff | claudecmd "Review this diff for risky changes"
claudecmd --json "explain this repo"
claudecmd --stream "explain the current architecture"
claudecmd --session auth-refactor "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 -p "say hello" --output-format json
Install Claude Code via the Anthropic install script or Homebrew cask (npm global installs are not assumed). Authentication (
claude /loginorANTHROPIC_API_KEY) is a prerequisite —claudecmddoes not manage it.
You can point claudecmd at a specific binary with the CLAUDECMD_CLAUDE_BIN
environment variable.
Install
Option A — run from a checkout (no install)
The repository ships an executable shim; nothing to install:
git clone https://github.com/kurok/pyclacmd
cd pyclacmd
./bin/claudecmd "say hello"
Option B — install the claudecmd command
pip install .
# or, for development:
pip install -e ".[dev]"
This provides a claudecmd console script.
macOS note: the system
pipshipped with the OS Python can be old enough thatpip install -e .fails with a--user/--prefixconflict. If you hit that, use a virtualenv (recommended),pipx install ., or just use the./bin/claudecmdshim above.python3 -m venv .venv && source .venv/bin/activate pip install -e ".[dev]"
The PTY fallback needs pexpect, which is an optional extra:
pip install ".[pty]"
Interactive mode (--interactive) needs pexpect and pyte:
pip install ".[interactive]"
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). |
--stream |
Stream assistant text progressively as it is produced. |
--raw |
Print Claude Code's response exactly as returned. |
--cwd <path> |
Working directory Claude runs in. |
--session <id-or-name> |
Resume/track a session by UUID or friendly name. |
--timeout <seconds> |
Abort (and clean up) after N seconds. |
--model <model> |
Model alias (opus, sonnet) or full id. |
--max-turns <n> |
Max agent turns (forwarded to Claude Code). |
--max-budget-usd <amount> |
Spend ceiling for the run. |
--system-prompt <text> |
Replace the system prompt. |
--append-system-prompt <text> |
Append to the system prompt. |
--allowed-tools <tools> |
e.g. "Bash(git:*),Read" (sent as --allowedTools). |
--disallowed-tools <tools> |
Tools to deny (sent as --disallowedTools). |
--permission-mode <mode> |
Claude permission mode (e.g. plan, acceptEdits). |
--no-session-persistence |
Do not persist or resume the session. |
--pty |
Force execution inside a pseudo-terminal (needs pexpect). |
--interactive |
Drive the interactive session instead of -p (needs pexpect+pyte; see below). |
--tools <tools> |
Interactive only: built-in tools to allow (e.g. "Bash,Read"); "" disables all. |
--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"
# Stream output
claudecmd --stream "explain the current architecture"
# Named session (resumes automatically on the next call with the same name)
claudecmd --session auth-refactor "continue from previous context"
# Constrain tools and run in a specific directory
claudecmd --cwd ~/src/app --allowed-tools "Bash(git:*),Read" "summarize recent changes"
# See exactly what would run, without running it
claudecmd --dry-run "say hello"
Output contract
Default (human) mode
Prints only the final assistant result to stdout. No banners, no metadata.
Diagnostics go to stderr only on error, or when --debug is set.
--json mode
Always emits one JSON object.
Success:
{
"ok": true,
"result": "assistant response text",
"session_id": "84cf2949-2329-4211-9526-1d8935ee9ab9",
"duration_ms": 3845,
"cost_usd": 0.0289945,
"raw": { "...": "the full Claude Code JSON response" }
}
Failure:
{
"ok": false,
"error": "Claude command failed",
"kind": "claude_exit_nonzero",
"exit_code": 1,
"stderr": "...",
"duration_ms": 1234
}
--dry-run mode
Prints the exact command plan and exits without invoking Claude. In --json
mode it emits a dedicated envelope:
{ "ok": true, "dry_run": true, "command": ["claude", "-p", "say hello", "--output-format", "json"] }
Secrets in the plan are run through the redactor.
--raw mode
Prints Claude Code's raw response unchanged. Combine with --stream to emit raw
newline-delimited stream JSON without transformation.
--stream mode
Streams assistant text to stdout as it arrives. --raw --stream emits the raw
stream-json event lines instead.
Error kinds
Every failure carries a stable kind (in --json mode, the kind field; in
default mode, in the stderr message) 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_exit_nonzero |
(Claude's) | Claude exited non-zero; its code is preserved. |
claude_timeout |
124 | The run exceeded --timeout. |
invalid_json |
65 | Claude's output could not be parsed as JSON. |
stdin_too_large |
64 | Reserved for stdin guard failures. |
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 | PTY fallback requested but pexpect is missing. |
no_prompt |
64 | No prompt via argument or stdin. |
unknown |
1 | Anything else. |
Sessions
- A
--sessionvalue that is a UUID is resumed directly (--resume). - A friendly name is mapped to the session id Claude returns, in
~/.claudecmd/sessions.json(override the directory withCLAUDECMD_HOME). The next call with the same name resumes that conversation automatically. - The store is created with
0700/0600permissions, written atomically, and guarded with file locking to survive concurrent runs.
Example mapping:
{
"auth-refactor": {
"session_id": "00000000-0000-0000-0000-000000000000",
"created_at": "2026-06-09T12:00:00Z",
"updated_at": "2026-06-09T12:15:00Z",
"cwd": "/Users/example/src/repo"
}
}
Large input
STDIN larger than the guard (default 9 MB, just under Claude Code's ~10 MB
stdin cap) 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.
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, and
--dangerously-skip-permissionsis intentionally not exposed. Note that--permission-modeis a passthrough: a user who explicitly passes--permission-mode bypassPermissionsopts into Claude Code's auto-approve behavior. That is a deliberate, explicit choice — never a default.
Debug behavior
--debug writes redacted diagnostics to stderr (the chosen command, the
resolved session, retries). In --json mode the structured envelope still goes
to stdout; extra detail goes to stderr only with --debug. Oversize-stdin temp
files are kept (and their paths logged) when debugging.
PTY fallback
claude -p is the default and works headlessly. A PTY path exists for the rare
environment that demands a real terminal: it spawns claude under a
pseudo-terminal (via pexpect), captures output, strips ANSI / Kitty keyboard
escape sequences (a known macOS Terminal.app nuisance), and forwards terminal
resizes. It is used when you pass --pty, or automatically as a fallback if a
normal run fails with a TTY-related error. PTY output is plain text only, so
session id and cost metadata are not available on that path.
Interactive mode
claude -p (the default path) is priced separately from interactive Claude Code
usage. --interactive runs the interactive session instead, so scripted
calls count against your Claude subscription the same as hand-typed ones.
claudecmd --interactive "summarize the architecture of this repo"
claudecmd --interactive --json --model sonnet "list the open TODOs"
claudecmd --interactive --tools "" "explain this error" # no tools => no permission prompts
Under the hood it spawns the interactive TUI under a pseudo-terminal, renders it
with a real terminal emulator (pyte), auto-answers the one-time
workspace-trust dialog for the working directory, waits for the turn to settle,
and extracts the assistant's reply from the rendered screen. Requires the
interactive extra (pexpect + pyte).
Caveats — it scrapes a human-facing TUI, so it is inherently less robust
than -p:
- No
session_idorcost_usdis available (the TUI does not expose them);--jsonreports"mode": "interactive"with those fieldsnull. - 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. --stream, session-name persistence, and cost ceilings do not apply; use the default-ppath for those.- Extraction is tuned to Claude Code's current TUI (v2.1.x) and may need updating if the interface changes.
Known limitations
--max-turnsis forwarded to Claude Code as-is; some native Claude Code builds do not expose that flag and will reject it (surfaced asclaude_exit_nonzerowith Claude's own message).--jsonis ignored for envelope purposes in--streammode (streaming has its own contract; use--raw --streamfor raw event lines).- First-party HTTP/OpenAI-compatible server, daemon, and Windows/WSL support are intentionally out of scope for this version.
Development
pip install -e ".[dev]"
pytest
The test suite mocks the claude binary, so CI never requires a real Claude
login. GitHub Actions runs pytest on Python 3.12 / Ubuntu (see
.github/workflows/ci.yml); the package itself supports Python 3.8+.
Manual macOS smoke tests
claudecmd "say hello"
echo "say hello" | claudecmd
claudecmd --json "say hello"
claudecmd --stream "say hello"
claudecmd --cwd /tmp "pwd"
claudecmd --timeout 5 "say hello"
claudecmd --dry-run "say hello"
claudecmd --session test-session "remember: project is claudecmd"
claudecmd --session test-session "what is the project?"
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.1.0.tar.gz.
File metadata
- Download URL: pyclacmd-0.1.0.tar.gz
- Upload date:
- Size: 40.0 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
263974faa7701252a2885a3d8435d4d52eb4a66ab896f6b87b3c90a19900390b
|
|
| MD5 |
532d6a557f104cac050fd83f1eb359f0
|
|
| BLAKE2b-256 |
4bd3e4e4f38d57f341ce223228f24cf4623abcd711db86b015b34dd84d7ed833
|
Provenance
The following attestation bundles were made for pyclacmd-0.1.0.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.1.0.tar.gz -
Subject digest:
263974faa7701252a2885a3d8435d4d52eb4a66ab896f6b87b3c90a19900390b - Sigstore transparency entry: 1770310697
- Sigstore integration time:
-
Permalink:
kurok/pyclacmd@695f776e293318d2d3af2f84eb15907d9a5addee -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/kurok
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@695f776e293318d2d3af2f84eb15907d9a5addee -
Trigger Event:
release
-
Statement type:
File details
Details for the file pyclacmd-0.1.0-py3-none-any.whl.
File metadata
- Download URL: pyclacmd-0.1.0-py3-none-any.whl
- Upload date:
- Size: 30.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 |
6abb4ef4a9a503300fa19352aab126ee55c8b25621a2f24574d83e69e6297128
|
|
| MD5 |
202ad42781472b57a9abd8969a418128
|
|
| BLAKE2b-256 |
e8c47dc839410ee38612edb5df148c4ff61241da7f0e7b1ce7426e6f9202b20c
|
Provenance
The following attestation bundles were made for pyclacmd-0.1.0-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.1.0-py3-none-any.whl -
Subject digest:
6abb4ef4a9a503300fa19352aab126ee55c8b25621a2f24574d83e69e6297128 - Sigstore transparency entry: 1770311678
- Sigstore integration time:
-
Permalink:
kurok/pyclacmd@695f776e293318d2d3af2f84eb15907d9a5addee -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/kurok
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@695f776e293318d2d3af2f84eb15907d9a5addee -
Trigger Event:
release
-
Statement type: