claude-print/AGENTS.md
2026-06-13 23:32:50 -04:00

6.7 KiB

AGENTS.md — claude-print

Repo purpose

claude-print is a drop-in replacement for claude -p that drives the Claude Code interactive TUI via PTY, preserving subscription billing after the June 15, 2026 cc_entrypoint split. It spawns claude in a pseudo-terminal, auto-dismisses the trust dialog, injects the user's prompt, waits for the Stop hook, reads the transcript, and emits clean output — all without --print or --output-format.

Build commands

# Debug build
cargo build

# Musl release (static binary for deployment)
cargo build --target x86_64-unknown-linux-musl --release

# Tests  (intercepted by ~/.local/bin/cargo — submits to iad-ci when repo is clean)
cargo test

# Unit tests only (no binary compilation required)
cargo test --lib

# Integration tests (requires compiled binary)
cargo test --test '*'

# Smoke check (verifies PTY, hooks, and environment prerequisites)
./target/debug/claude-print --check

The cargo wrapper at ~/.local/bin/cargo auto-submits to the rust-verify WorkflowTemplate on iad-ci when there are no uncommitted changes and the repo has a remote. It falls back to a cgroup-limited local run otherwise.

Test structure

Location What it tests
src/*.rs inline (#[cfg(test)]) Unit tests — pure logic, no I/O
tests/integration.rs High-level integration; uses mock_claude
tests/integration/ Sub-module helpers for integration tests
tests/cli.rs CLI argument parsing and flag validation
tests/emitter.rs Output formatting (text / json / stream-json)
tests/startup.rs Trust-dialog detection and prompt injection
tests/terminal.rs Terminal probe parsing
tests/transcript.rs JSONL transcript parsing
tests/hooks.rs Stop hook FIFO install / read
tests/stop_poller.rs Stop payload polling logic
tests/pty_integration.rs PTY spawn + round-trip (requires PTY capability)
tests/version_compat.rs --version output parsing
tests/fixtures/ Shared fixture helpers

mock_claude

test-fixtures/mock-claude/ is a workspace member compiled as a separate binary. It impersonates claude for integration tests and is controlled via environment variables (see its own README / source). No real credentials are needed.

To rebuild mock_claude explicitly:

cargo build -p mock-claude

Module map

File Role
src/lib.rs Crate root — re-exports public modules for integration tests
src/main.rs Entry point: CLI parse, claude binary resolution, calls session::Session::run()
src/cli.rs Clap argument definitions (Cli, OutputFormat)
src/config.rs Loads ~/.claude/claude-print.toml (model default, etc.)
src/session.rs Session orchestrator: installs hooks, spawns PTY child, runs event loop, reads transcript. Session::run() is the top-level entry point for a single prompt→response cycle.
src/pty.rs Forks child, opens PTY pair, calls login_tty, unsets CLAUDE_CODE_SESSION_ID in child, forwards SIGWINCH/SIGINT
src/startup.rs State machine: reads PTY output until trust dialog or idle; auto-dismisses (sends CR), injects prompt via bracketed paste; hard timeout after 45s with <200 bytes
src/event_loop.rs Single-threaded poll(2) loop (50ms timeout for timer ticks) over PTY master + self-pipe + stop FIFO; calls callback on each chunk
src/hook.rs Installs Stop hook via temp dir settings.json; creates FIFO; cleans up on drop
src/poller.rs Opens FIFO non-blocking (read + keeper write ends), parses Stop hook payload, derives transcript path from session_id + cwd
src/transcript.rs Reads .jsonl transcript; extracts last assistant message + token usage
src/emitter.rs Formats and writes output (text, json, stream-json)
src/terminal.rs Absorbs and discards terminal probe sequences (DA1/DA2/DSR/xtversion) from Ink TUI
src/error.rs Error enum and Result alias
src/check.rs --check mode: verifies PTY, FIFO, hooks, and cc_entrypoint env

Key invariants

These must hold across all changes:

  1. Do not set CLAUDE_CONFIG_DIR — transcripts must land in ~/.claude/projects/ (the real config dir). The temp dir is only used for the Stop hook settings injection, and it must not redirect the config dir.

  2. Clean up the temp dir on all exit paths — no claude-print-<pid>-* directories may be left in $TMPDIR. The TempDir handle in HookInstaller must remain owned until after the child exits.

  3. Forward SIGINT to the child process — pressing Ctrl-C must reach claude, not just terminate claude-print.

  4. Never pass --print or --output-format to the child — those flags activate the API billing path. The entire point is to stay on the PTY/TUI path.

  5. cc_entrypoint=cli is the correctness invariant — verify that CLAUDE_CC_ENTRYPOINT (or equivalent) is cli via --check before each release. AS-4 in the plan documents the acceptance criterion.

  6. Unset CLAUDE_CODE_SESSION_ID in child — the child must not inherit the parent's session ID or it will write events into the parent's transcript and may skip Stop hook dispatch. Only CLAUDE_CODE_SESSION_ID is unset; CLAUDECODE=1 and CLAUDE_CODE_ENTRYPOINT=cli must be preserved.

  7. Keep both FIFO ends alive for the full event loopopen_fifo_nonblock() returns (read_fd, keeper_write_fd). Both must be stored until after the event loop exits. Dropping read_fd closes the fd the event loop is polling; dropping keeper causes ENXIO when the hook writes to the FIFO.

Key implementation notes

  • Event loop ticks on empty slices — the event loop uses poll(50ms) (not blocking) and emits an empty-slice tick to the callback on timeout. The callback must guard startup.feed() and terminal.feed() from empty slices — feeding empty data resets the idle timer in StartupSeq.

  • Timeout thread is detached, not joinedsession.rs detaches the timeout thread via drop() instead of joining it. The timeout thread sleeps for cli.timeout seconds (default 3600); joining before handling the exit reason would block the main thread for the full duration on early exit.

  • Child cleanup uses kill_child(pid)kill_child(pid) sends SIGTERM, waits up to 2s, then SIGKILL. Use this for all child cleanup paths, not bare waitpid.

Bead workflow

Beads use the bf prefix. Config is at .beads/config.yaml.

# List open beads
br list

# Claim a bead
br claim <id>

# Close a bead (requires a commit first)
br close <id>

See CLAUDE.md (root workspace) for full br CLI docs and FrankenSQLite recovery procedures.