pip install pyclacmd→ theclaudecmdcommand. Script Claude Code from the shell while keeping calls on your interactive (subscription) path, not separately-priced-p/headless usage.
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"-
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.
pip install pyclacmdThis 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
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>
| 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. |
# 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"Prints only the assistant's final reply to stdout, extracted from the rendered session. No banners, no metadata.
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 }Prints the full rendered TUI screen unchanged — useful for debugging extraction.
Prints the exact command plan (run through the secret redactor) and exits:
{ "ok": true, "dry_run": true, "command": ["claude", "say hello", "--model", "haiku"] }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.
- 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.
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.
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). |
- 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.
pip install -e ".[dev]"
pytestThe 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+.
MIT — see LICENSE.
