pty-claude is a CLI compatibility wrapper for common claude -p
subprocess usage.
It accepts -p / --print at your call site, but it does not forward print
mode to Claude Code. Instead it starts the normal interactive Claude Code CLI
inside a pseudo-terminal, sends one prompt, reads Claude's local transcript
JSONL, and emits claude -p-shaped output.
This is useful when existing automation expects a claude -p-style command,
but you want the request to go through interactive Claude Code behavior.
pty-claude is not official Anthropic software and is not an Agent SDK.
- Local
claudeCLI installed and authenticated. - Go 1.26+ to build from source.
- A Unix-like PTY environment. Linux and macOS are the intended targets.
- Claude Code must be able to write its normal transcript files under
~/.claude/projects.
Windows is not verified.
go install github.com/cunninghamcard-bit/pty-claude/cmd/pty-claude@latestOr build from a checkout:
git clone https://github.com/cunninghamcard-bit/pty-claude.git
cd pty-claude
go build -o ./bin/pty-claude ./cmd/pty-claudeIf your real Claude binary is not named claude, set it explicitly:
PTY_CLAUDE_REAL_BIN=/usr/local/bin/claude pty-claude -p "hello"You can also pass it per invocation:
pty-claude --claude-bin /usr/local/bin/claude -p "hello"Text:
pty-claude -p "only output OK" --output-format textJSON:
pty-claude -p "only output OK" --output-format jsonStream JSON:
pty-claude -p "only output OK" --output-format stream-jsonStdin:
printf "only output OK\n" | pty-claude -p --output-format jsonCommon Claude Code flags are forwarded to the real interactive CLI:
pty-claude -p "fix this bug" \
--cwd /path/to/repo \
--model sonnet \
--system-prompt "Be concise" \
--permission-mode acceptEdits \
--output-format json \
--timeout 120000--input-format stream-json is accepted for simple single-shot callers. User
messages are extracted from stdin and sent as one interactive prompt:
printf '%s\n' \
'{"type":"user","message":{"role":"user","content":"only output OK"}}' \
| pty-claude -p --input-format stream-json --output-format stream-jsonHelp:
pty-claude --help
pty-claude --versionNode:
import { spawnSync } from "node:child_process";
const child = spawnSync(
"pty-claude",
["-p", "only output OK", "--output-format", "json"],
{ encoding: "utf8" },
);
if (child.status !== 0) {
throw new Error(child.stderr || child.stdout);
}
const result = JSON.parse(child.stdout);
console.log(result.result);Python:
import json
import subprocess
completed = subprocess.run(
["pty-claude", "-p", "only output OK", "--output-format", "json"],
text=True,
capture_output=True,
check=True,
)
result = json.loads(completed.stdout)
print(result["result"])Text output prints only the assistant text.
JSON output is shaped like the common claude -p --output-format json result:
{
"type": "result",
"subtype": "success",
"is_error": false,
"api_error_status": null,
"duration_ms": 1234,
"duration_api_ms": null,
"num_turns": 1,
"result": "OK",
"stop_reason": "end_turn",
"session_id": "...",
"total_cost_usd": null,
"usage": {},
"modelUsage": null,
"permission_denials": null,
"terminal_reason": "completed",
"fast_mode_state": null,
"uuid": "..."
}stream-json prints assistant transcript events as JSONL, followed by the final
result event. Field names are normalized toward print-mode style, for example
session_id, parent_tool_use_id, and final result.
pty-claude does not invent values just to make the JSON look complete.
Fields that are available from the interactive transcript are copied or derived
from that transcript. Fields that are native to print mode but unavailable from
interactive transcript data are emitted as null.
Common null fields include:
duration_api_mstotal_cost_usdmodelUsagepermission_denialsfast_mode_stateuuid, when the transcript event does not contain one
By default, pty-claude also does not pass --session-id to Claude. Claude
generates the session id, and pty-claude discovers the matching transcript by
looking for the user prompt in newly written transcript files.
If you explicitly pass --session-id, it is forwarded to Claude:
pty-claude -p "hello" --session-id 11111111-2222-4333-8444-555555555555caller
-> pty-claude -p "prompt"
-> real claude interactive CLI in a PTY
-> prompt is written as keyboard input
-> ~/.claude/projects/<cwd-encoded>/*.jsonl is discovered and tailed
-> text/json/stream-json output is emitted
For a working directory /tmp/example, Claude usually writes transcripts under:
~/.claude/projects/-tmp-example/*.jsonl
The -p / --print flag is consumed only for compatibility. It is never sent
to the real Claude binary.
Print-only flags such as --max-budget-usd, --json-schema,
--no-session-persistence, and --include-partial-messages are also consumed
instead of being forwarded.
The target is common subprocess usage:
claude -p "prompt" --output-format json
printf "prompt" | claude -p --output-format json
printf '{"type":"user","message":{"role":"user","content":"prompt"}}\n' \
| claude -p --input-format stream-json --output-format stream-jsonThe replacement is explicit:
pty-claude -p "prompt" --output-format jsonThis project aims to replace the most common claude -p process boundary, not
to clone every Agent SDK protocol detail.
- Transcript JSONL is a local Claude Code implementation detail.
stream-jsonis transcript-based, not the official print-mode stream.- Event timing, token accounting, and some print-only fields can differ.
- Tool permission prompts can still block unless your Claude Code flags allow the operation.
- Claude UI changes can affect prompt detection.
go test ./...
go build -o ./bin/pty-claude ./cmd/pty-claudeOptional live smoke test:
./bin/pty-claude -p "only output OK" \
--cwd /tmp/pty-claude-live \
--model sonnet \
--tools "" \
--system-prompt "Reply exactly with the requested token and no extra text." \
--output-format jsonMIT