claude-print/README.md
jedarden 7a8945ae00 feat(bf-1n6): add scripts/check-billing.sh for AS-4 billing conformance
- Implement check-billing.sh script that verifies the most recent
  transcript has entrypoint 'cli' (subscription pool) not 'sdk-cli'
- Script finds newest *.jsonl under ~/.claude/projects/ and scans
  for entrypoint field, exiting 0 iff it equals 'cli'
- Handle no-transcripts and no-directory cases with distinct errors
- Update README with Troubleshooting and Release checklist sections
  referencing the script as the pre-release gate

Acceptance criteria:
- bash -n passes (syntax valid)
- Executable mode 755
- README updated with troubleshooting/release checklist references

Bead-Id: bf-1n6
2026-07-02 14:42:48 -04:00

184 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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](https://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
```bash
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
```bash
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:
```bash
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
```bash
# 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 |
| `--first-output-timeout <SECS>` | | `90` | First-output timeout in seconds (PTY output) |
| `--stream-json-timeout <SECS>` | | `90` | Stream-json first-output timeout in seconds |
| `--stop-hook-timeout <SECS>` | | `120` | Stop hook watchdog 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) |
| `124` | Timeout exceeded |
| `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 authenticated** — `claude-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.
## Troubleshooting
### Billing classification verification
Before deploying to production, verify that sessions are billing against the subscription pool (`cc_entrypoint=cli`):
```bash
# Check the most recent session's billing classification
./scripts/check-billing.sh
```
This script inspects the latest transcript JSONL under `~/.claude/projects/` and asserts the `entrypoint` field is `"cli"` (subscription), not `"sdk-cli"` (credit pool). Exit 0 means correct billing; exit 1 means a billing regression. Run this after every release or Claude Code upgrade.
### Common issues
**PTY open failed** — You may be in a container without `/dev/ptmx`. Run on a bare-metal host or a VM with full PTY support.
**Session never completes** — The Stop hook may not be firing. Check `--verbose` output for "Stop received" and verify your `~/.claude/settings.json` isn't blocking hook execution.
**Empty output despite success** — The transcript reader may have hit a race condition. Run with `--verbose` to see retry attempts; if retries exceed 40×50ms, the Stop hook fired before the JSONL was flushed.
## Release checklist
Before cutting a release tag:
1. Run `./scripts/check-billing.sh` to verify billing conformance (requires credentials)
2. Run `cargo test` to ensure all mocked tests pass
3. Run `claude-print --check` to verify PTY and Stop hook mechanics
4. Update version in `Cargo.toml`
5. Commit and push: `git tag v0.x.y && git push origin v0.x.y`
6. Monitor the `claude-print-ci` Argo Workflow for successful build and GitHub release
## Structure
- `docs/notes/` — design decisions, constraints, integration details
- `docs/plan/plan.md` — complete implementation plan
- `scripts/check-billing.sh` — AS-4 billing conformance script (run before every release)
- `scripts/` — integration test scripts