AgentHarness
AgentHarness is an Elixir library for running locally installed coding agents as supervised OTP sessions. It currently supports Codex CLI and Claude Code, normalizes their streaming output, and gives callers one interface for turns, questions, approvals, cancellation, completion, and event replay.
AgentHarness is deliberately bring your own CLI. It does not proxy credentials, create API keys, or log in on your behalf. The Codex and Claude processes run on your machine with the accounts already configured in their official CLIs.
AgentHarness is an early v0.1 library. Its core lifecycle is tested, but the public API and provider event vocabulary may still evolve.
What it provides
- One supervised
GenServerper logical agent session - Codex app-server and Claude Code streaming adapters
- Ordered
%AgentHarness.Event{}values with normalized data and raw SDK payloads - Session- or turn-scoped subscriptions
- Replay-capable Elixir streams and race-free terminal
await/2 - Native session monitors and live/stored session inventory for orchestrators
- Structured questions, approvals, and MCP elicitation
- Per-session MCP servers, skills, model, workspace, sandbox, and provider options
- An in-memory event and lifecycle store with a behaviour for durable adapters
- Stable session and turn handles that contain IDs rather than PIDs
- Telemetry spans and events for commands, session/turn lifetimes, requests, and Store writes
Each call to start_session/2 opens a provider runtime, each session allows one
active turn, and different sessions may run at the same time. Capacity can be
bounded through supervisor configuration or caller-side admission control. See
Architecture and concurrency before creating a large
number of resident sessions.
Prerequisites
This repository requires Elixir 1.19 and is tested with OTP 28.
Provider-side JSON helpers use Elixir 1.19's built-in JSON module; this
library does not declare its own JSON codec dependency. Provider SDKs may still
bring their own transitive codecs.
Install and authenticate the CLIs you intend to use:
$ codex --version
$ codex login
$ claude --version
$ claude
By default, AgentHarness requires the CLIs' saved ChatGPT or claude.ai login and
rejects known API, cloud-provider, and custom-routing overrides. To
intentionally use another authentication route, set
provider_options: %{auth: :inherit}. See
Billing and authentication for the
provider-policy boundary and current official links.
Installation
Add AgentHarness to your dependencies:
def deps do
[
{:agent_harness, "~> 0.1.0"}
]
end
For local development against a checkout, use path: "../agent_harness"
instead.
Then fetch and compile dependencies:
$ mix deps.get
$ mix compile
AgentHarness starts its Registry, supervisors, and default in-memory store with your application.
Quick start
alias AgentHarness.Event
{:ok, session} =
AgentHarness.start_session(:codex,
cwd: "/absolute/path/to/project",
approval_policy: :never,
sandbox: :workspace_write
)
{:ok, turn} =
AgentHarness.start_turn(
session,
"Read the project, fix the failing test, and explain the change."
)
{:ok, subscription} = AgentHarness.subscribe(turn, from: :start)
receive_events = fn receive_events ->
receive do
{:agent_harness, ref, %Event{type: :message_delta, data: %{text: text}}}
when ref == subscription.ref ->
IO.write(text)
receive_events.(receive_events)
{:agent_harness, ref, %Event{type: :turn_completed} = event}
when ref == subscription.ref ->
event
{:agent_harness, ref, %Event{type: type} = event}
when ref == subscription.ref and
type in [:turn_failed, :turn_cancelled, :turn_interrupted] ->
event
{:agent_harness, ref, %Event{}}
when ref == subscription.ref ->
receive_events.(receive_events)
end
end
terminal_event = receive_events.(receive_events)
AgentHarness.unsubscribe(subscription)
AgentHarness.stop_session(session)
For a simpler blocking workflow:
{:ok, turn} = AgentHarness.start_turn(session, "Summarize this project.")
case AgentHarness.await(turn, timeout: 120_000) do
{:ok, %{status: :completed, result: result}} ->
IO.inspect(result)
{:error, %AgentHarness.Event{} = terminal_event} ->
IO.inspect(terminal_event, label: "agent did not complete")
{:error, :timeout} ->
AgentHarness.cancel(turn)
end
Responding to questions and approvals
Questions and permission checks arrive as :request_created events:
alias AgentHarness.{Event, Request, Response}
receive do
{:agent_harness, ref,
%Event{
type: :request_created,
data: %Request{kind: :question} = request
}}
when ref == subscription.ref ->
# A scalar works for one question. For multiple questions, pass a map
# keyed by each entry's `id` from request.questions.
:ok = AgentHarness.respond(request, Response.answer("Postgres"))
{:agent_harness, ref,
%Event{
type: :request_created,
data: %Request{kind: kind} = request
}}
when ref == subscription.ref and
kind in [:command_approval, :file_change_approval, :permission] ->
:ok = AgentHarness.respond(request, Response.approve(scope: :once))
end
A request can be answered only once. Pending requests become expired when their turn ends.
Per-session MCP and skills
Both providers accept session-scoped MCP configuration:
mcp_servers = %{
"project-tools" => %{
command: "npx",
args: ["-y", "@example/project-mcp", "/absolute/path/to/project"]
}
}
skills = [
%{name: "release", path: "/absolute/path/to/release/SKILL.md"}
]
{:ok, session} =
AgentHarness.start_session(:claude,
cwd: "/absolute/path/to/project",
mcp_servers: mcp_servers,
skills: skills,
provider_options: %{
auth: :subscription,
allowed_tools: ["Read", "Grep", "Glob", "Skill"]
}
)
The providers materialize these settings differently. Claude loads standalone skills through a temporary session plugin; Codex sends explicit skill items with each turn. Details and provider-specific options are in Provider configuration.
Documentation
- Getting started and public API
- Provider configuration, MCP, skills, and authentication
- Driving AgentHarness from a GenServer
- Lifecycle, events, requests, and persistence
- Supervision, concurrency, and scaling
- Billing and authentication
- Tests and live CLI checks
Development
Normal tests use Mox-backed provider clients and do not consume Codex or Claude quota:
$ mix precommit
Live CLI tests are tagged and excluded by default:
$ mix test --include live
Live tests invoke your authenticated CLI and may consume subscription or API usage. Read Testing before enabling them.
License
AgentHarness is available under the MIT License.