# ADR-0003 — Subprocess CLI adapters, not SDK bindings

**Status:** accepted · **Date:** 2026-07-26

## Context

`ablate` must invoke Claude Code and Codex headlessly. Both offer a CLI (`claude -p`,
`codex exec`); Anthropic additionally offers the Claude Agent SDK as Python and TypeScript
packages, which is the same agent loop as a library.

## Decision

Invoke the vendor CLI as a subprocess with `cwd` set to the trial worktree. Parse structured
output (`--output-format json` / `--json`). Adapters implement a three-method protocol:
`preflight()`, `build_command()`, `parse_events()`.

## Rejected: Claude Agent SDK (Python)

Better ergonomics, typed messages, no subprocess parsing, and tool-approval callbacks.

Rejected because:
1. **It is Anthropic-only.** There is no equivalent for Codex, so the abstraction would be
   "SDK for one vendor, subprocess for the other" — meaning the subprocess path exists anyway
   and now there are two code paths to maintain.
2. **It measures a different thing.** The user's real environment is `claude` the CLI, with its
   own config discovery, defaults, and version. Measuring the SDK's behaviour and reporting it as
   the CLI's behaviour is the same class of validity error as [Q2](../12-open-questions.md).
3. **Version skew.** The user has some `claude` version installed; the SDK pin in your
   `pyproject.toml` is a different one. Reporting `agent_version` accurately requires invoking
   what the user actually has.

## Rejected: hitting the model API directly and implementing the agent loop

Total control, perfect telemetry, no subprocess. Also an entirely different product — you'd be
measuring *your* agent harness, not Claude Code, and the finding would generalize to nobody.
Non-starter.

## Consequences

- Output parsing is defensive by necessity: unknown event types ignored, missing fields → `None`,
  no crash on schema drift ([R5](../11-risks.md)).
- `preflight()` must check binary presence, version, and auth *before* the first trial, and fail
  with an actionable message. A run that dies at trial 40 on an expired token is the worst
  possible UX.
- The subprocess `cwd` — not any vendor directory flag — is the universal working-directory
  mechanism. This is what makes the abstraction hold despite being unable to verify a `--cd` flag
  for Codex ([Q1](../12-open-questions.md)).
- SIGTERM handling matters: Claude Code aborts the turn and exits 143. Timeouts must terminate
  the process group and record `error: timeout`, not hang the scheduler.
