The watchdog mechanism was complete but had an inconsistency: main.rs used exit code 3 for timeout errors while ClaudePrintError::Timeout.exit_code() returned 124 (GNU timeout convention). Now uses the proper exit code from the error type. This ensures timeout errors exit with the standard code 124, matching GNU timeout behavior and making error handling consistent for callers (marathon loop/NEEDLE). |
||
|---|---|---|
| .beads | ||
| docs | ||
| notes | ||
| scripts | ||
| src | ||
| test-fixtures/mock-claude | ||
| tests | ||
| ~/.needle/state | ||
| .needle-predispatch-sha | ||
| AGENTS.md | ||
| build.rs | ||
| Cargo.lock | ||
| Cargo.toml | ||
| claude-print-ci-sensor.yml | ||
| claude-print-ci-workflowtemplate.yml | ||
| claude-print-eventsource-stanza.yml | ||
| claude-print.yaml | ||
| install.sh | ||
| README.md | ||
claude-print
Drop-in replacement for claude -p (print/headless mode) that drives the Claude Code interactive TUI via PTY — keeping sessions on the unlimited subscription pool rather than the per-token Agent SDK credit pool.
Why this exists
Anthropic routes claude -p (headless/SDK mode) through a separate Agent SDK credit pool ($100–$200/month on Max plans). Only the interactive TUI (cc_entrypoint=cli) draws from the unlimited subscription.
The billing path is determined by an isatty check inside the claude binary: when stdout is a TTY, the session is tagged cc_entrypoint=cli and billed against the subscription. When stdout is a pipe (as with claude -p), it becomes cc_entrypoint=sdk-cli and draws from the credit pool instead.
claude-print allocates a PTY, drives the interactive TUI over it, auto-dismisses the trust dialog, injects the user prompt via bracketed paste, waits for the Stop hook via a FIFO, reads the JSONL transcript, and emits clean stdout output — giving callers claude -p wire-compatible output while billing against the subscription.
Prerequisites
- Claude Code must be installed and authenticated. See claude.ai/code.
- An active Claude subscription (Pro or Max plan) is required. The whole point is to bill against subscription, not credits.
- Linux only. PTY support requires POSIX — no Windows ConPTY.
Install
sh install.sh
install.sh downloads a pre-built static musl binary from GitHub Releases (jedarden/claude-print), runs --check to verify the setup, and copies claude-print.yaml to ~/.needle/agents/ if NEEDLE is present.
Set SKIP_MOCK_CLAUDE=1 to skip the mock_claude test fixture download.
Build from source
git clone https://github.com/jedarden/claude-print
cd claude-print
cargo build --release
# binary at target/release/claude-print
# fully static binary (recommended for deployment):
cargo build --target x86_64-unknown-linux-musl --release
Architectures: x86_64 and aarch64.
Self-check
After install, verify the PTY, Stop hook, and billing entrypoint:
claude-print --check
This confirms cc_entrypoint=cli appears in the session JSONL. install.sh runs this automatically, but it's worth running manually after upgrades.
Usage
claude-print [OPTIONS] [PROMPT]
Reads the prompt from a positional argument, --input-file, or stdin (when not a TTY). These are mutually exclusive.
Examples
# Positional prompt
claude-print "Summarize this in one sentence"
# Stdin pipe
echo "what is the capital of France?" | claude-print
# File input
claude-print --input-file prompt.txt
# Specify a model
claude-print --model claude-opus-4-8 "Write a haiku about Rust"
# JSON output
claude-print --output-format json "what is 2+2?" | jq .text
# Stream-JSON — real-time JSONL event replay
claude-print --output-format stream-json "Write a story"
# Agentic task with tool use
claude-print --max-turns 5 "List files in current dir and summarize"
# Short timeout for quick questions
claude-print --timeout 30 "quick question"
Flags
| Flag | Short | Default | Description |
|---|---|---|---|
[PROMPT] |
Prompt string (mutually exclusive with --input-file and stdin) |
||
--input-file <FILE> |
-f |
Read prompt from file | |
--model <MODEL> |
-m |
claude-sonnet-4-6 |
Model to use |
--max-turns <N> |
30 |
Maximum agentic turns | |
--output-format <FORMAT> |
-o |
text |
Output format: text, json, stream-json |
--allowedTools <LIST> |
Comma-separated list of allowed tools | ||
--disallowedTools <LIST> |
Comma-separated list of disallowed tools | ||
--dangerously-skip-permissions |
Skip permission prompts (dangerous) | ||
--timeout <SECS> |
3600 |
Wall-clock timeout in seconds | |
--claude-binary <PATH> |
PATH lookup | Path to claude binary | |
--no-inherit-hooks |
Disable user hook inheritance | ||
--verbose |
Write timing traces to stderr | ||
--check |
Run installation self-test and exit | ||
--version |
-V |
Print version and exit | |
--help |
-h |
Print help |
Output formats
text(default): plain text response, printed to stdout.json: one-line JSON object withtext,session_id,model, andusagefields.stream-json: JSONL replay of the raw transcript events in real time, one event per line.
Exit codes
| Code | Meaning |
|---|---|
0 |
Success |
1 |
Assistant error (is_error: true in transcript) |
2 |
Internal error (PTY spawn, hook setup, parse failure) |
3 |
Timeout exceeded |
4 |
Bad input (missing prompt, unreadable file) |
130 |
Interrupted (SIGINT) |
How it works
- PTY fork — spawns
claudeunder a PTY soisattyreturns true and the session is taggedcc_entrypoint=cli. - Trust dialog dismiss — watches for the one-time "do you trust this project?" prompt and sends the confirmation keypress automatically.
- Bracketed paste injection — sends the prompt wrapped in bracketed-paste escape sequences (
\x1b[200~/\x1b[201~), which Claude Code's TUI accepts as user input without triggering shell interpretation. - Stop hook FIFO — installs a temporary Claude Code Stop hook that writes a payload to a FIFO when the response is complete; the process blocks on the FIFO read.
- Transcript read — reads the JSONL session transcript Claude writes to
~/.claude/projects/, extracts the assistant turn, and emits it in the requested format.
NEEDLE integration
If you use NEEDLE for LLM fleet dispatch, install.sh automatically copies claude-print.yaml to ~/.needle/agents/. This registers claude-print as the adapter for Anthropic subscription models (sonnet/opus/haiku) so NEEDLE workers bill against the subscription rather than the Agent SDK credit pool. See claude-print.yaml in the repo root for the full adapter config, including --no-inherit-hooks isolation mode and the use_or_lose cost type.
Limitations
- Linux only — PTY allocation is POSIX. No Windows ConPTY support.
- Claude Code must be authenticated —
claude-printdelegates entirely to theclaudebinary; it cannot authenticate on its own. - One prompt per invocation — there is no multi-turn session mode; each call starts a fresh session.
- Startup latency ~2–5s — the PTY handshake and Claude Code startup add overhead versus a direct HTTP call.
Structure
docs/notes/— design decisions, constraints, integration detailsdocs/plan/plan.md— complete implementation plan