Drop-in replacement for claude -p that drives the interactive TUI via PTY, preserving subscription billing
Find a file
jedarden d344e9553c fix: five PTY→FIFO pipeline bugs that prevented end-to-end operation
Bug 1 (event_loop.rs): poll(-1) blocked forever when TUI went silent;
startup timer never fired so prompt was never injected. Fixed: poll(50ms)
+ empty-slice tick so poll_timers() runs on every iteration.

Bug 2 (session.rs): read_fd from open_fifo_nonblock() was dropped
immediately after as_raw_fd(), closing the fd the event loop was polling.
Fixed: store both (_fifo_read, _fifo_keeper) to keep both alive.

Bug 3 (pty.rs): child inherited CLAUDE_CODE_SESSION_ID from parent, so
it wrote events into the parent transcript and skipped Stop hook dispatch.
Fixed: unsetenv(CLAUDE_CODE_SESSION_ID) in child after fork; preserve
CLAUDECODE=1 and CLAUDE_CODE_ENTRYPOINT=cli.

Bug 4 (session.rs): empty-slice timer ticks were fed to startup.feed()
which reset last_output_at, preventing idle timer from ever firing.
Fixed: guard startup.feed() and terminal.feed() from empty slices.

Bug 5 (session.rs): handle.join() blocked main thread for up to
cli.timeout (default 3600s) on any early exit, because the timeout thread
sleeps for the full duration. Also, waitpid blocked forever if child
ignored SIGTERM. Fixed: drop(timeout_thread) to detach; add kill_child()
helper (SIGTERM → 2s wait → SIGKILL) used on all cleanup paths.

All five confirmed fixed: claude-print "what is 2+2?" → "4", exit 0,
cc_entrypoint=cli in session JSONL (subscription billing verified).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-13 23:32:50 -04:00
.beads docs(bf-3ag): verify session implementation complete - all tests pass 2026-06-13 15:56:07 -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-3ag): verify session implementation complete - all tests pass 2026-06-13 14:52:20 -04:00
src fix: five PTY→FIFO pipeline bugs that prevented end-to-end operation 2026-06-13 23:32:50 -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 Add bf-5nr validation notes: claude-print-ci WorkflowTemplate YAML is valid 2026-06-10 02:11:37 -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 docs(bf-3ag): verify session implementation complete - all tests pass 2026-06-13 15:56:07 -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
Cargo.lock Add mock-claude fixture, test_pty_spawns_tty integration test, and hook module export 2026-06-08 08:56:36 -04:00
Cargo.toml Phase 2: implement PTY open and fork in pty.rs 2026-06-07 16:38:14 -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 Add Phase 9: NEEDLE integration — install.sh, claude-print.yaml, --check subcommand 2026-06-10 01:36:28 -04:00
README.md docs: expand README with usage examples, build-from-source, and limitations 2026-06-13 23:32:50 -04:00

claude-print

Drop-in replacement for claude -p (print/headless mode) that drives the Claude Code interactive TUI via PTY — preserving subscription billing after the June 15, 2026 Agent SDK credit split.

Why this exists

Starting June 15, 2026, Anthropic separates claude -p (headless) into a separate Agent SDK credit pool ($100$200/month on Max plans). Only the interactive TUI (cc_entrypoint=cli) continues drawing 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