// blog

Subprocess providers: reusing a subscription you're already paying for.

How claude-cli and gemini-cli wrap a host CLI as a subprocess instead of calling an API, what they give up to do that, and why stdin transport matters when the diff gets large.

· CommitBrief

The most common feedback I got after the four-API-provider matrix shipped was a variation of the same question: “I already pay for Claude Code. Why am I getting a second bill from Anthropic for the API?” The honest answer was that there wasn’t a way to avoid it, and that I knew about the gap.

claude-cli and gemini-cli are the fix that landed in v0.9.0 and got promoted to stable at v1.0. They’re a new class of provider — not an HTTPS API integration, but a subprocess wrapper around a host CLI that’s already authenticated against the same backend. This post is about why they exist, what they trade away to exist, and the engineering detail that makes them work on real-world diff sizes.

What they actually are

A subprocess provider doesn’t make any network call from CommitBrief. The flow is:

  1. CommitBrief builds the combined system+user prompt the same way it does for the API providers — <project_rules> XML wrap, language directive, prompt-injection guard, then the diff.
  2. CommitBrief launches the host CLI as a subprocess: claude -p - for claude-cli, gemini -p <prompt> for gemini-cli.
  3. The prompt is fed to the host CLI (via stdin for claude-cli, argv for gemini-cli — more on the asymmetry below).
  4. The host CLI does its own auth, its own model selection, its own network call, and writes the response to its stdout.
  5. CommitBrief reads that stdout and emits it to your terminal verbatim (or to --output <file> since v0.9.2).

From CommitBrief’s perspective the host CLI is opaque. It doesn’t know which model the host is using, doesn’t see the network call, doesn’t get usage tokens back. The host CLI handles all of that against the user’s existing subscription.

When to reach for them

The honest pitch is: pick a subprocess provider when you’d rather pay through your existing subscription than manage a second API key. If you’ve got Claude Pro or a Claude Code subscription, --cli claude reuses that bill. Same for Gemini Advanced via --cli gemini. No second key in ~/.commitbrief/config.yml, no second monthly invoice, no second budget conversation with your finance team if you’re expensing it.

The shape of the user this lands well for, in my experience:

  • Solo developers and indie hackers who’re already paying for one of the IDE-class CLIs and want a code-review layer that piggybacks on it.
  • Teams that bought a centralised Claude Code or Gemini CLI subscription for their engineers and don’t want to also provision per-engineer Anthropic or Google API keys.
  • People in the “I want to try this without entering a credit card” stage — --cli works against whatever you’ve already authenticated against, with no friction.

The shape of user this lands badly for: anyone who wants structured findings out of CommitBrief. Which brings us to the trade-offs.

What they give up

The host CLI emits markdown-ish prose. CommitBrief doesn’t get a typed findings: [] JSON to parse, because the host CLI doesn’t have any contract with us about the shape of its output. This is the central trade-off and it ripples through everything downstream:

  • No --json. Cobra rejects the combination at the flag-validation layer before any work runs. You can’t ask for JSON output from a provider that emits prose.
  • No --markdown rendering through the OUTPUT.md template. Same reason. The host CLI’s prose is the output; CommitBrief streams it verbatim. The OUTPUT.md template runs on parsed findings, and there are no parsed findings.
  • No --fail-on severity gate. The gate needs to read the .severity field of each finding. There are no findings. The gate is a no-op in CLI-provider mode.
  • No --copy of structured findings. The clipboard payload is built from the finding list. Same root cause.
  • Cost preflight short-circuits. The host CLI’s pricing is opaque to CommitBrief; it returns Pricing{} (all zero). The estimated cost is always 0, so the threshold is never crossed.

None of these are bugs. They’re consequences of the provider not having a structured-output contract with us. The user opted into prose by choosing the subprocess provider; the structured-output features go dark together.

The bypass for --output works since v0.9.2 (UC-07). Before that, commitbrief --cli claude --output review.md silently dropped the destination and printed to stdout — the plain-text emit path didn’t go through the openOutput helper the structured renderers use. The fix routes both paths through the same helper.

The stdin asymmetry

The most interesting engineering detail in the subprocess provider implementation is that claude-cli reads its prompt from stdin while gemini-cli reads it from argv. That asymmetry exists for a specific reason and matters for anyone running review on large diffs.

The platform ARG_MAX ceiling on Linux and macOS is around 128 KB. That’s the total size of all argv elements concatenated. A typical CommitBrief prompt — system prompt with the project rules plus the actual diff — fits comfortably under that ceiling for normal staged-change reviews. But run commitbrief diff main...feature on a branch with a few weeks of work, or run a review on a generated-file-heavy PR, and the prompt size sails past 128 KB. The subprocess invocation fails with argument list too long before the host CLI runs.

The fix for claude-cli was to switch the prompt transport to stdin: claude -p - reads from stdin, and CommitBrief pipes the prompt in via the subprocess’s stdin handle. There’s no ARG_MAX ceiling on a pipe; you can send arbitrarily large payloads. This shipped as UC-24 in v0.9.2.

gemini-cli stays on argv for now, because upstream doesn’t yet document a reliable stdin shorthand. The day Gemini CLI does, the switch is a one-line change in the clireview.Spec.UseStdin field. Until then, the troubleshooting doc honestly recommends preferring claude-cli or one of the API providers for genuinely large diffs through gemini-cli.

The version-cache subtlety

Cache keys for subprocess providers need to include the host CLI’s version. Otherwise a host CLI upgrade — Anthropic ships a new Claude Code with different formatting, or Google ships a new Gemini CLI with different output conventions — would silently serve cached responses generated by the old version, and the user would see stale output without any signal that something had changed.

The version is captured by shelling out to <binary> --version at cache-key construction time. That’s potentially a hot path; if the host CLI is slow or hung, that probe blocks the review pipeline.

Two safeguards: the result is memoised per-Backend with sync.Once (so the probe runs at most once per CommitBrief invocation), and the probe runs under a 5-second timeout (so a misbehaving host CLI can’t stall the review). This shipped as UC-23 in v0.9.2.

What this fits into

The multi-provider strategy post covered the broader story: six providers, manual switching via --provider, no automatic fallback chain by design. Subprocess providers are the v0.9.0+ addition to that picture. They’re an extension of the same Provider interface every other backend implements, just with a subprocess transport instead of an HTTPS client.

The reason they exist as separate claude-cli / gemini-cli packages rather than as flags on the existing anthropic / gemini providers is implementation honesty: the failure modes, the output contract, the cost story are different enough that conflating them would mislead users. A flag like anthropic --use-host-cli would suggest “this is just Anthropic with a different transport,” when in fact you’re getting a different output shape and a different billing model. Two separate providers, two separate names, two separate doc pages — the user picking one knows what they’re getting.

The directory naming reinforces this: internal/provider/anthropic/ is the HTTPS API; internal/provider/claude-cli/ is the subprocess wrapper. The -cli suffix is a deliberate developer-side signal that these go through a local subprocess, not the API. Easy to confuse without it; harder to confuse with it.

The next post in this series turns to a design decision rather than a feature: why three scope flags (--commit, --branch, --pull-request) collapsed into a single commitbrief diff <args> subcommand at v0.9.0, and what that says about how I think about CLI surface area.

Related reading


← all posts