# Harnesses and models

> Supported harness identifiers (claude, codex, cursor, gemini, opencode, custom), model picker strings, permission modes per harness, and harness capability limits for injection and multi-turn.

- Repository: Parcha-ai/build
- GitHub: https://github.com/Parcha-ai/build
- Human docs: https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b
- Complete Markdown: https://grok-wiki.com/public/docs/parcha-ai-build-bea5702b371b/llms-full.txt

## Source Files

- `README.md`
- `src/shared/types/index.ts`
- `src/main/services/harness-capabilities.ts`
- `src/main/services/harness-policy.service.ts`
- `src/renderer/stores/session.store.ts`
- `src/main/services/codex.service.ts`
- `src/main/services/opencode.service.ts`

---

---
title: "Harnesses and models"
description: "Supported harness identifiers (claude, codex, cursor, gemini, opencode, custom), model picker strings, permission modes per harness, and harness capability limits for injection and multi-turn."
---

Build routes every chat turn through a **harness** (the CLI or SDK backend) selected by a **model picker string**. The shared `Harness` union, prefix-based model IDs, `claude:get-models` catalog, per-session permission modes, and `HarnessCapabilities` queue limits are the contracts that tie the renderer, `ClaudeService.streamMessage`, and the message queue together.

## Harness identifiers

The canonical harness type is exported from shared types:

```typescript
export type Harness = 'claude' | 'codex' | 'cursor' | 'gemini' | 'opencode' | 'custom';
```

| Harness | Backend | How Build selects it |
|---------|---------|----------------------|
| `claude` | Claude Agent SDK (`@anthropic-ai/claude-agent-sdk`) | Model ID has **no** `codex:`, `cursor:`, `gemini:`, `opencode:`, or `custom:` prefix (e.g. `claude-sonnet-4-6`) |
| `codex` | OpenAI Codex CLI (`codex` binary) | `codex:<model>` |
| `cursor` | Cursor Agent CLI and/or `@cursor/sdk` | `cursor:<model>` |
| `gemini` | Google Gemini CLI | `gemini:<model>` |
| `opencode` | OpenCode CLI (`opencode` or `npx opencode-ai`) | `opencode:<model>` |
| `custom` | Claude Agent SDK with proxy env overrides | `custom:<settings-id>` from `AppSettings.customModels` |

Resolution is prefix-based in both renderer and main process (`harnessFromModel`):

```typescript
if (model.startsWith('codex:')) return 'codex';
if (model.startsWith('cursor:')) return 'cursor';
if (model.startsWith('gemini:')) return 'gemini';
if (model.startsWith('opencode:')) return 'opencode';
if (model.startsWith('custom:')) return 'custom';
return 'claude';
```

<Note>
`custom` models still execute through the **Claude Agent SDK** path in `ClaudeService.streamMessage`, with `ANTHROPIC_BASE_URL`, `ANTHROPIC_API_KEY`, and `ANTHROPIC_DEFAULT_SONNET_MODEL` overridden from `CustomModelConfig`. The `custom` harness label applies to transcripts, analytics, and queue capability lookup—not a separate runtime.
</Note>

### Special picker value: `auto`

`auto` is the **Auto Build** entry. It is not a harness; routing resolves a concrete model and harness per turn via `AutoRouterService` / Flue meta-router. Resolved harness appears on `RoutingDecision.resolvedHarness` and on assistant messages when `model === 'auto'`.

## Model picker catalog

Models are loaded once per session via IPC `claude:get-models` → `ClaudeService.getAvailableModels()`. Each entry is `{ id, name, description }`.

### Prefix convention

| Prefix | Example `id` | Stripped backend model |
|--------|----------------|------------------------|
| (none) | `claude-sonnet-4-6` | Full string sent to SDK |
| `codex:` | `codex:gpt-5.4` | `gpt-5.4` |
| `cursor:` | `cursor:composer-2.5` | `composer-2.5` |
| `gemini:` | `gemini:gemini-2.5-pro` | `gemini-2.5-pro` |
| `opencode:` | `opencode:deepseek-v4-pro` | `deepseek/deepseek-v4-pro` if no `/` in suffix |
| `custom:` | `custom:kimi-k26` | Resolved to `CustomModelConfig.modelId` for API calls |

OpenCode resolution (`resolveOpenCodeModel`): bare suffix `deepseek-v4-pro` becomes `deepseek/deepseek-v4-pro`.

### Default catalog (when Foundry is disabled)

**Claude / Auto Build**

| `id` | Display name |
|------|----------------|
| `auto` | Auto Build |
| `claude-opus-4-8` | Opus 4.8 |
| `claude-opus-4-7` | Opus 4.7 |
| `claude-opus-4-6` | Opus 4.6 |
| `claude-opus-4-5-20251101` | Opus 4.5 |
| `claude-sonnet-4-6` | Sonnet 4.6 |
| `claude-sonnet-4-5-20250929` | Sonnet 4.5 |
| `claude-sonnet-4-20250514` | Sonnet 4 |
| `claude-haiku-4-5-20251001` | Haiku 4.5 |

**Codex** (always listed)

| `id` |
|------|
| `codex:gpt-5.5` |
| `codex:gpt-5.4` |
| `codex:gpt-5.4-mini` |
| `codex:gpt-5.3-codex` |
| `codex:o3` |

**Cursor** — dynamic list when `cursorApiKey` is set and `@cursor/sdk` `models.list` succeeds; otherwise hardcoded:

| `id` |
|------|
| `cursor:composer-2.5` |
| `cursor:claude-3.5-sonnet` |
| `cursor:gpt-4o` |
| `cursor:gemini-3.5-flash` |
| `cursor:o3` |

**OpenCode (DeepSeek)** — appended only when `deepseekApiKey` is set:

| `id` |
|------|
| `opencode:deepseek-v4-pro` |
| `opencode:deepseek-v4-flash` |
| `opencode:deepseek-reasoner` |

**Gemini** — always listed (CLI uses `GEMINI_API_KEY` / settings):

| `id` |
|------|
| `gemini:gemini-3.5-flash` |
| `gemini:gemini-2.5-pro` |
| `gemini:gemini-2.5-flash` |

**Custom** — one row per `settings.customModels[]` entry: `custom:${id}`.

### Foundry override

When `foundryEnabled` is true and Foundry default model strings are configured, `getAvailableModels()` returns **only** those Foundry IDs (no prefixed harness models in that list).

### Auto Build tier defaults (settings UI)

Default tier bindings in settings (overridable via `autoRouterConfig`):

| Tier | Default model |
|------|----------------|
| plan | `claude-sonnet-4-6` |
| build | `codex:gpt-5.5` |
| verify | `codex:gpt-5.5` |
| refine | `cursor:composer-2.5` |
| fallback | `claude-sonnet-4-6` |

## Routing flow

`ClaudeService.streamMessage` is the single dispatch point. Non-Claude prefixes branch to dedicated services; everything else uses the Claude Agent SDK (including `custom:*` and bare Claude IDs).

```mermaid
flowchart TB
  subgraph UI["Renderer"]
    Picker["Model picker id"]
    Store["session.store selectedModel"]
  end
  subgraph IPC["Main IPC"]
    Send["claude:send-message"]
  end
  subgraph Dispatch["ClaudeService.streamMessage"]
    Auto{"id === auto?"}
    Codex{"codex:*"}
    Cursor{"cursor:*"}
    Gemini{"gemini:*"}
    OC{"opencode:*"}
    SDK["Claude Agent SDK\n(claude + custom)"]
  end
  Picker --> Store --> Send --> Auto
  Auto -->|router| Codex & Cursor & Gemini & OC & SDK
  Codex --> CodexSvc["codex.service"]
  Cursor --> CursorSvc["cursor-cli / cursor.service"]
  Gemini --> GeminiSvc["gemini.service"]
  OC --> OpenCodeSvc["opencode.service"]
```

Cross-harness context: when switching harnesses, Build injects unified transcript context via `buildUnifiedContextForHarness` (and Cursor may start a fresh CLI chat if the last assistant harness was not `cursor`).

Auto Build delegate stages (`isExecutableDelegateHarness`): `codex`, `cursor`, `gemini`, `opencode` only—not `claude` or `custom`.

## Permission modes

Build-wide permission mode type (renderer `session.store`):

```typescript
export type PermissionMode =
  | 'auto' | 'default' | 'acceptEdits' | 'bypassPermissions' | 'plan' | 'dontAsk';
```

Default persisted mode: `bypassPermissions`.

### Modes offered in the UI

| Model family | Modes in picker |
|--------------|-----------------|
| Claude, Cursor, Gemini, OpenCode, `custom`, `auto` | `auto`, `acceptEdits`, `default`, `bypassPermissions`, `plan`, `dontAsk` |
| Codex (`codex:*`) | `auto`, `acceptEdits`, `bypassPermissions`, `plan` only |

`normalizePermissionModeForModel` clamps unsupported modes to the nearest allowed value for the active model.

Auto Build mutating stages (`canRunMutatingStages`): blocked when permission is `plan` or `dontAsk`.

### Per-harness translation

`translateHarnessPolicy` maps Build permission modes and `MetaHarnessPolicy` into harness-specific execution settings.

**Codex** (`getExecutionMode`):

| Build mode | Codex CLI behavior |
|------------|-------------------|
| `plan` | Read-only sandbox; plan-only preamble; `approval_policy=never` |
| `acceptEdits` | `workspace-write`; approvals never |
| `default` | `workspace-write`; `approval_policy=on-request` |
| `dontAsk` | Read-only sandbox |
| `bypassPermissions` (default) | `--dangerously-bypass-approvals-and-sandbox` |

**Cursor** (policy translation):

| Build mode | Cursor SDK/CLI |
|------------|----------------|
| `plan`, `dontAsk` | `mode: 'plan'`, `sandbox: 'enabled'` |
| `dontAsk` | `mode: 'ask'` |
| Others | No mode override from permission |

**Gemini** (CLI `--approval-mode`):

| Build mode | Gemini `approvalMode` |
|------------|----------------------|
| `plan`, `dontAsk` | `plan` |
| `acceptEdits` | `auto_edit` |
| `default` | `default` |
| `bypassPermissions` | `yolo` (CLI default when no policy) |

**OpenCode** (`buildPermissionConfig` JSON):

| Build mode | Permission JSON |
|------------|-----------------|
| `plan`, `dontAsk` | `edit` and `external_directory` denied |
| Other | Permissive `*` allow; `question: deny` |

**Claude SDK** uses permission mode directly on the Agent SDK query (plus effort/thinking from policy).

### Effort / thinking (Claude and Auto Build policy)

Renderer **effort** levels: `low` | `medium` | `high` | `xhigh` | `max` (legacy `ThinkingMode` values `off` / `thinking` / `ultrathink` migrate via `migrateThinkingMode`).

`HarnessPolicyTranslation` for `claude` maps meta effort to SDK `effort`, adaptive thinking (Opus/Sonnet 4.6+), or `maxThinkingTokens`, and `fastMode` when `speed === 'fast'`.

Codex maps effort to `model_reasoning_effort`: `minimal` | `low` | `medium` | `high` | `xhigh`.

## Harness capabilities (queue and injection)

`HarnessCapabilities` (shared `message-queue` types):

| Field | Meaning |
|-------|---------|
| `supportsAsyncInjection` | Mid-stream inject via `claude:inject-message` / SDK `streamInput` |
| `supportsMultiTurn` | Harness maintains conversation state across queued turns without full context replay |
| `minTurnGapMs` | Delay after stream end before dequeuing next message |
| `maxCoalesceWindowMs` | Declared coalesce window (3000 ms for all harnesses; drain scheduling uses `minTurnGapMs` today) |

Static table in `harness-capabilities.ts`:

| Harness | Async injection | Multi-turn | `minTurnGapMs` |
|---------|-----------------|------------|----------------|
| `claude` | yes | yes | 0 |
| `cursor` | no | yes | 500 |
| `codex` | no | no | 500 |
| `gemini` | no | no | 500 |
| `opencode` | no | no | 500 |
| `custom` | no* | no* | 500 |

\*Capability row is `custom`; runtime still uses Claude SDK. Renderer treats `custom:*` like Claude for injection (`isNonClaudeHarness` is false), so **queued messages during an active `custom` stream can use `injectMessage`** when a Claude query is active.

### Queue behavior

1. `messageQueueService.enqueue` — if not streaming, schedules immediate drain.
2. `onStreamStart(sessionId, harness)` — marks streaming; records active harness.
3. `onStreamEnd` — schedules drain after `getHarnessCapabilities(harness).minTurnGapMs`.
4. `drain-ready` → renderer sends next queued message through normal `sendMessage`.

**Injection path (Claude only):** After a tool completes, if the queue has messages and the active stream is **not** a non-Claude harness (`codex:`, `cursor:`, `gemini:`, `opencode:`), the store calls `claude.injectMessage`. Non-Claude streams wait until `stream end`, then send the next message as a **new turn** (often with rebuilt cross-harness context).

### Multi-turn semantics by harness

| Harness | Multi-turn mechanism |
|---------|---------------------|
| `claude` | SDK session resume (`sdkSessionId`); Auto Build may skip resume if last harness ≠ `claude` |
| `cursor` | CLI `chatId` resume when last assistant harness was `cursor`; else new chat + context blob; local SDK agent map when API key and no chatId |
| `codex`, `gemini`, `opencode` | Effectively single-turn CLI invocations; prior turns rehydrated via `buildUnifiedContextForHarness` |
| `custom` | Same as Claude SDK session semantics with proxy env |

## Persistence and messages

- Per-session `model` and `permissionMode` persist on `sessions.update` and `claude.setPermissionMode`.
- `ChatMessage.harness` tags which backend produced assistant output.
- Preferred compaction fallbacks: Claude models list vs Codex list in `session.store` depending on source model prefix.

## SSH remote harness availability

`RemoteCliCapabilities` tracks which CLIs are installed on the remote host: `claude`, `codex`, `cursor`, `gemini`, `opencode`. SSH resume candidates are typed for `backend: 'claude'` only; other harnesses run over SSH when the remote binary exists and the session routes to that prefix.

## Verification checklist

<Steps>
<Step title="Load model list">
Open a session and confirm the picker shows expected groups (Claude, Codex, Cursor, Gemini, OpenCode, Custom) after keys are configured.
</Step>
<Step title="Confirm harness routing">
Select `codex:gpt-5.4` and send a message; stream should log Codex routing and assistant messages should carry `harness: 'codex'`.
</Step>
<Step title="Permission clamping">
Switch to a Codex model and cycle permission modes; `default` and `dontAsk` should not appear.
</Step>
<Step title="Queue vs injection">
With Claude selected, queue a second message while streaming; after a tool completes, injection should run. Repeat with `gemini:*`; message should send only after stream ends.
</Step>
</Steps>

## Related pages

<CardGroup>
<Card title="Configure API keys and providers" href="/configure-providers">
Keys and CLI detection that gate Codex, Cursor, Gemini, OpenCode, and custom proxy models in the picker.
</Card>
<Card title="Auto Build routing" href="/auto-build-routing">
How `auto` resolves tier, harness, and orchestration plans across the same model strings.
</Card>
<Card title="Shared types reference" href="/shared-types-reference">
`Harness`, `HarnessCapabilities`, `RoutingDecision`, and `CustomModelConfig` field definitions.
</Card>
<Card title="Manage coding sessions" href="/manage-sessions">
Session model persistence, permission dialogs, and message queue UX.
</Card>
</CardGroup>
