Skip to content

CLI Wrapper

Z-M-Huang edited this page Apr 30, 2026 · 3 revisions

CLI-Wrapper provider (reference)

A reference Provider extension that talks to an LLM through a subprocess instead of an HTTP endpoint. The canonical use case is a subscription CLI the user has already authenticated — e.g., a vendor's own claude or codex command-line tool — where the user wants stud-cli to orchestrate conversations that bill against that subscription rather than a separate API key.

This page is a reference extension — it describes one way a Provider extension can work, not a normative contract. The normative surface is Providers (contract). A CLI-wrapper is not bundled with stud-cli in v1.


Why this exists

Two classes of users:

  • API-key users reach providers over HTTPS with openai-compatible, anthropic, or gemini adapters. Auth is an API key; billing flows through the vendor's API dashboard.
  • Subscription users have already installed and authenticated a vendor-owned CLI (claude, codex, etc.) that bills against a seat-based subscription. They want stud-cli's orchestration — state machines, context providers, hooks — to layer on top of the subscription they already pay for, without re-authenticating.

Subscription users cannot use the bundled adapters because those adapters require an API key, not the CLI's own session. The CLI-wrapper is the pattern.


Role in the architecture

flowchart LR
    Session[stud-cli session] --> Adapter[CLI-wrapper adapter]
    Adapter --> Spawn[spawn vendor CLI<br/>with prompt on stdin]
    Spawn --> CLI[vendor CLI process]
    CLI --> Subscription[vendor subscription auth]
    CLI --> Out[stdout + stderr]
    Out --> Adapter
    Adapter --> Core[core's internal event shape]
Loading

The adapter translates core's request shape into a subprocess invocation, reads the subprocess's output, and translates back into core's internal events. The CLI handles auth and billing; stud-cli stays out of that loop.


Shape

The adapter's provider config might look like this (conceptually — exact field names follow Providers). The provider entry sits at settings.json.providers.<id>; the user picks the id (e.g., claude-subscription).

Field Meaning
protocol cli-wrapper.
command The executable to invoke. Must be on PATH or an absolute path.
argsTemplate Template for subprocess arguments. Placeholder for the prompt.
stdinMode prompt-only or streaming.
parseMode text (assume the CLI returns prose) or jsonl (structured events).
models string[] — model names the CLI accepts, passed via --model or the CLI's own flag.
timeoutMs Per-request subprocess timeout.
env Environment variables to pass through. Typically empty — the CLI reads its own auth from $HOME/.<vendor>/.

baseURL is absent — there is no HTTP endpoint. apiKeyRef is absent — the wrapped CLI handles auth. An adapter that requires an API key for a CLI-wrapper protocol is misdesigned.


Capability posture

A subscription CLI typically declares a reduced capability set compared to the API adapter. What a given CLI supports is vendor-specific; conservative defaults apply when unknown:

Capability Typical CLI posture
streaming Vendor-dependent. Many CLIs buffer. A CLI that emits JSONL can stream; one that prints prose at end does not.
toolCalling Often not supported through the CLI path. An SM that depends on tool calling must not switch to a CLI-wrapper without re-validation.
structuredOutput Usually false unless the CLI advertises a structured-output flag.
multimodalInputs Text-only unless the CLI accepts file references.
contextWindow Inherits the vendor's model default; not adapter-negotiated.
reasoning Vendor-dependent.
parallelToolCalls False by default.

A session that depends on toolCalling: strict cannot switch to a CLI-wrapper whose models declare toolCalling: false — the switch fails fast per Capability-Mismatch-Switch. This is the point: the user sees the mismatch before a turn silently degrades.


Request translation

For each turn:

  1. Core assembles the request (messages, tools, parameters) as usual.
  2. The adapter serializes the request into a prompt string per the CLI's input contract. A CLI that accepts only a single prompt flattens the conversation; one that accepts JSONL preserves structure.
  3. The adapter spawns the subprocess with stdinMode controlling how the prompt is fed (one-shot on stdin, or a streamed pipe).
  4. The adapter reads stdout (and optionally stderr) and applies parseMode to produce events.

Tool-call translation is out of scope for simple CLI-wrappers. A CLI that emits structured tool-call events may be parseable; most do not. Sessions that need tool calls should use an HTTP adapter or a CLI that itself is tool-aware.


Auth

The adapter does not handle auth. The wrapped CLI is assumed to be installed and authenticated through its own flow (e.g., the user has already run claude login or codex auth).

Concern Posture
Session token Stored by the CLI under its own config directory (~/.<vendor>/). Not stud-cli's surface.
Refresh Handled by the CLI.
Re-auth prompts The CLI may print an auth-needed message to stderr. The adapter surfaces it as a ProviderTerminal / AuthFailure; the session does not auto-resolve — the user runs the CLI's auth command manually.
stud-cli's env surface Not used. The CLI reads its own config.

See First Run — the CLI-wrapper's first-run experience is "install and authenticate the vendor CLI first," then add the provider to settings.json.


Error translation

Subprocess condition Core class / code
Exit 0, non-empty stdout Success.
Exit 0, empty stdout ProviderTerminal / EmptyResponse.
Non-zero exit with stderr "not authenticated" ProviderTerminal / AuthFailure. User must run CLI auth.
Non-zero exit with stderr "rate limited" / "quota" ProviderTransient / RateLimited. Adapter honors the core default retry schedule unless vendor guidance suggests otherwise.
Non-zero exit, generic ProviderTerminal / SubprocessFailed. Details in audit.
Subprocess exceeded timeoutMs ProviderTransient / NetworkTimeout. Subprocess is sent a terminate signal per Platform Integration § Signal handling.
Command not on PATH Fails adapter init; session does not start with this provider; diagnostic emitted.

Retry policy mirrors HTTP adapters: transient classes retry under Error Model; terminal classes do not.


Subprocess lifecycle

Phase Behavior
init Verify command is reachable. If not, fail init; session can start with other providers.
activate No persistent subprocess — each request spawns fresh.
Per request Spawn, feed prompt, read output, reap.
deactivate Reap any outstanding subprocess; send terminate then kill.
dispose Final reap; idempotent.

A long-running subprocess is not kept across turns in the reference design. Each turn is a fresh spawn — simpler, more predictable, avoids the stateful-subprocess failure modes. An adapter that keeps a subprocess alive for warm performance declares that behavior and documents its crash semantics.


Security considerations

  • Subprocess inherits stud-cli's env by default. Per Platform Integration § Subprocess invocation, core does not filter. A CLI-wrapper extension is trusted not to leak env to its wrapped CLI in harmful ways.
  • The wrapped CLI runs with the user's privileges. A malicious CLI in PATH would have the same blast radius as running it manually. Trust is the user's problem to have solved before adding the provider.
  • Prompt injection surface. The adapter builds a prompt from core's assembled request. If the wrapped CLI supports magic strings that escape the prompt (some do), those strings can reach the subscription LLM. The reference adapter disallows unescaped control sequences and rejects prompts that contain them with a diagnostic rather than silently forwarding.
  • No shell wrapping. The adapter invokes command directly, not through a shell, to avoid shell-injection footguns. A CLI that needs shell semantics opts in explicitly in argsTemplate.
  • Audit. ProviderRequestStarted carries the provider id and model; the argv is redacted by default because a CLI may accept the prompt on argv in some modes. Loggers that need full argv must explicitly declare so.

Typical configurations

Two conceptual examples (exact vendor CLI flags change — check each vendor's documentation):

A vendor CLI that accepts prompt on stdin

{
  "id": "claude-subscription",
  "protocol": "cli-wrapper",
  "command": "claude",
  "argsTemplate": ["-p", "--model", "{model}"],
  "stdinMode": "prompt-only",
  "parseMode": "text",
  "models": ["claude-opus-4-7"]
}

A vendor CLI that emits JSONL events

{
  "id": "codex-subscription",
  "protocol": "cli-wrapper",
  "command": "codex",
  "argsTemplate": ["exec", "--jsonl"],
  "stdinMode": "streaming",
  "parseMode": "jsonl",
  "models": ["gpt-5.4"]
}

Flag names are illustrative; check each vendor's CLI for current flags.


When not to use a CLI-wrapper

  • You have an API key — use the bundled HTTP adapter. Faster, streaming-ready, tool-call-capable.
  • You need tool calling — most CLIs do not forward tool-use protocol cleanly. The HTTP adapter is more reliable.
  • You need prompt caching — only the HTTP adapter can mark cache breakpoints per Anthropic § Prompt caching.
  • You need parallel tool calls — subprocess output rarely conveys them.

The CLI-wrapper is a pragmatic bridge for subscription users. It is not a general-purpose replacement for the HTTP adapter.


Related pages

Introduction

Reading

Core runtime

Contracts

Category contracts

Context

Security

Runtime behavior

Operations

Providers (bundled)

Integrations

Reference extensions

Tools

UI

Session Stores

Loggers

Providers

Hooks

Context Providers

Commands

Case studies

Flows

Maintainers

Clone this wiki locally