Drop-in replacement for claude -p that drives the interactive TUI via PTY, preserving subscription billing
Find a file
jedarden 3c43436729 docs(bf-2w7): verify cleanup implementation is complete
Verified that all cleanup mechanisms are properly implemented:
- Orphan cleanup on startup (10-minute threshold)
- CleanupGuard for automatic RAII cleanup
- Global cleanup before process::exit()
- Idempotent cleanup with retry logic

All exit paths covered:
- Normal exit (success/error)
- Timeout exit
- Signal interruption (SIGINT/SIGTERM)
- Watchdog timeout
- Panic
- Early returns

All tests passing. No orphaned temp directories found.

Bead-Id: bf-2w7
2026-06-25 11:17:34 -04:00
.beads chore(bf-2w7): verify cleanup implementation is complete and remove unused imports 2026-06-25 09:32:01 -04:00
.claude/worktrees chore(bf-2w7): verify cleanup implementation is complete and remove unused imports 2026-06-25 09:32:01 -04:00
docs docs(adr): add ADR-004 — no silent agent escalation in NEEDLE workers 2026-06-11 07:31:56 -04:00
notes docs(bf-2w7): verify cleanup implementation is complete 2026-06-25 11:17:34 -04:00
scripts fix(bf-2f5): correct timeout exit code from 3 to 124 2026-06-25 08:33:00 -04:00
src chore(bf-2w7): verify cleanup implementation is complete and remove unused imports 2026-06-25 09:32:01 -04:00
target chore(bf-2w7): verify cleanup implementation is complete and remove unused imports 2026-06-25 09:32:01 -04:00
test-fixtures/mock-claude Add bf-5nr validation notes: claude-print-ci WorkflowTemplate YAML is valid 2026-06-10 02:11:37 -04:00
tests chore(bf-2w7): verify cleanup implementation is complete and remove unused imports 2026-06-25 09:32:01 -04:00
~/.needle/state docs(bf-3ag): verify session implementation complete - all tests pass 2026-06-13 15:56:07 -04:00
.needle-predispatch-sha chore(bf-2w7): verify cleanup implementation is complete and remove unused imports 2026-06-25 09:32:01 -04:00
AGENTS.md docs: update AGENTS.md — add session.rs to module map, document FIFO invariants and timeout-thread detach 2026-06-13 23:32:50 -04:00
build.rs fix(bf-2f5): correct timeout exit code from 3 to 124 2026-06-25 08:33:00 -04:00
Cargo.lock fix(bf-2w7): ensure temp dir and FIFO cleanup on all exit paths 2026-06-25 00:25:17 -04:00
Cargo.toml feat: forward --dangerously-skip-permissions, --allowedTools, --disallowedTools to child; bump v0.2.0 2026-06-14 18:11:53 -04:00
claude-print-ci-sensor.yml fix(bf-2f5): correct timeout exit code from 3 to 124 2026-06-25 08:33:00 -04:00
claude-print-ci-workflowtemplate.yml test(bf-3eq): add cargo test to CI workflow 2026-06-25 01:40:53 -04:00
claude-print-eventsource-stanza.yml fix(bf-2f5): correct timeout exit code from 3 to 124 2026-06-25 08:33:00 -04:00
claude-print.yaml Add Phase 9: NEEDLE integration — install.sh, claude-print.yaml, --check subcommand 2026-06-10 01:36:28 -04:00
install.sh fix(install): use x86_64-linux asset names instead of musl target suffix 2026-06-14 00:10:05 -04:00
README.md docs: improve README for clarity and discoverability 2026-06-24 07:07:47 -04:00
test-cleanup-verification.md docs(bf-2w7): document cleanup implementation verification 2026-06-25 10:38:45 -04:00

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 with text, session_id, model, and usage fields.
  • 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

  1. PTY fork — spawns claude under a PTY so isatty returns true and the session is tagged cc_entrypoint=cli.
  2. Trust dialog dismiss — watches for the one-time "do you trust this project?" prompt and sends the confirmation keypress automatically.
  3. 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.
  4. 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.
  5. 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 authenticatedclaude-print delegates entirely to the claude binary; 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 ~25s — the PTY handshake and Claude Code startup add overhead versus a direct HTTP call.

Structure

  • docs/notes/ — design decisions, constraints, integration details
  • docs/plan/plan.md — complete implementation plan