---
summary: "Run OpenClaw embedded agent turns through the external GitHub Copilot SDK harness"
title: "Copilot SDK harness"
read_when:
  - You want to use the GitHub Copilot SDK harness for an agent
  - You need configuration examples for the `copilot` runtime
  - You are wiring an agent to subscription Copilot (github / openclaw / copilot) and want it to run through the Copilot CLI
---

The external `@openclaw/copilot` plugin lets OpenClaw run embedded subscription
Copilot agent turns through the GitHub Copilot CLI (`@github/copilot-sdk`)
instead of the built-in PI harness.

Use the Copilot SDK harness when you want the Copilot CLI session to own the
low-level agent loop: native tool execution, native compaction
(`infiniteSessions`), and CLI-managed thread state under `copilotHome`.
OpenClaw still owns chat channels, session files, model selection, OpenClaw
dynamic tools (bridged), approvals, media delivery, the visible transcript
mirror, `/btw` side questions (handled by the in-tree PI fallback — see
[Side questions (`/btw`)](#side-questions-btw)), and `openclaw doctor`.

For the broader model/provider/runtime split, start with
[Agent runtimes](/concepts/agent-runtimes).

## Requirements

- OpenClaw with the `@openclaw/copilot` plugin installed.
- If your config uses `plugins.allow`, include `copilot` (the manifest
  id declared by the plugin). A restrictive
  allowlist that uses the npm-style `@openclaw/copilot` package name
  will leave the plugin blocked and the runtime will not load
  even with `agentRuntime.id: "copilot"`.
- A GitHub Copilot subscription that can drive the Copilot CLI (or a
  `gitHubToken` env / auth-profile entry for headless / cron runs).
- A writable `copilotHome` directory. The harness defaults to
  `<agentDir>/copilot` when OpenClaw provides an agent directory, otherwise
  `~/.openclaw/agents/<agentId>/copilot` for full per-agent isolation.

`openclaw doctor` runs the plugin
[doctor contract](#doctor) for declarative session-state ownership and future
compatibility migrations. It does not run Copilot CLI environment probes.

## Plugin install

The Copilot runtime is an external plugin so the core `openclaw` package does
not carry the `@github/copilot-sdk` dependency or its platform-specific
`@github/copilot-<platform>-<arch>` CLI binary. Together they add roughly
260 MB, so install them only for agents that opt into this runtime:

```bash
openclaw plugins install @openclaw/copilot
```

The wizard installs the plugin the first time you select a
`github-copilot/*` model **and** your config opts the model (or its
provider) into the Copilot agent runtime via
`agentRuntime: { id: "copilot" }` (see [Quickstart](#quickstart) below).
Without the opt-in, openclaw uses its built-in GitHub Copilot provider
and never installs the runtime plugin.

The runtime resolves the SDK in this order:

1. `import("@github/copilot-sdk")` from the installed `@openclaw/copilot`
   package.
2. The well-known fallback dir `~/.openclaw/npm-runtime/copilot/` (the
   legacy on-demand install target).

A missing SDK surfaces a single error with code `COPILOT_SDK_MISSING`
and the plugin reinstall command above.

## Quickstart

Pin one model (or one provider) to the harness:

```json5
{
  agents: {
    defaults: {
      model: "github-copilot/auto",
      models: {
        "github-copilot/auto": {
          agentRuntime: { id: "copilot" },
        },
      },
    },
  },
}
```

Both routes are equivalent. Use `agentRuntime.id` on a single model entry
when only that model should be routed through the harness; set
`agentRuntime.id` on a provider when every model under that provider should
use it.

`github-copilot/auto` is the portable starting point. Named Copilot models are
account- and organization-policy-dependent, so only pin one after confirming
that the authenticated Copilot CLI exposes it.

## Supported providers

The harness advertises support for the canonical `github-copilot` provider
(the same id owned by `extensions/github-copilot`):

- `github-copilot`

It also supports custom `models.providers` entries when the selected model has
a non-empty `baseUrl` and one of these API shapes:

- `openai-responses`
- `openai-completions`
- `ollama` (OpenAI-compatible completions)
- `azure-openai-responses`
- `anthropic-messages`

Native provider ids such as `openai`, `anthropic`, `google`, and `ollama` remain
owned by their native runtimes. Use a distinct custom provider id when routing
an endpoint through Copilot BYOK.

Copilot BYOK endpoints must be public-network HTTPS URLs. The harness gives the
Copilot SDK a per-attempt loopback proxy URL, then forwards provider traffic
through OpenClaw's guarded fetch path so DNS pinning and SSRF policy stay
owned by OpenClaw. Use the native OpenClaw runtime for local Ollama, LM Studio,
or LAN model servers.

## BYOK

Copilot BYOK uses the SDK's session-level custom provider contract. OpenClaw
passes the resolved model endpoint, API key, bearer-token mode, headers, model
id, and context/output limits without moving provider transport logic into
core.

For example:

```json5
{
  agents: {
    defaults: {
      model: "custom-proxy/llama-3.1-8b",
      models: {
        "custom-proxy/llama-3.1-8b": {
          agentRuntime: { id: "copilot" },
        },
      },
    },
  },
  models: {
    mode: "merge",
    providers: {
      "custom-proxy": {
        baseUrl: "https://api.example.com/v1",
        apiKey: "${CUSTOM_PROXY_API_KEY}",
        api: "openai-responses",
        authHeader: true,
        models: [{ id: "llama-3.1-8b", name: "Llama 3.1 8B" }],
      },
    },
  },
}
```

BYOK sessions are separately keyed from subscription sessions and from other
endpoints or credential fingerprints. Rotating the key, headers, model, or
endpoint creates a fresh Copilot SDK session instead of resuming incompatible
state.

## Auth

Per-agent precedence, applied during `runCopilotAttempt`:

1. **Explicit `useLoggedInUser: true`** on the attempt input. Uses the Copilot
   CLI's logged-in user resolved under the agent's `copilotHome`.
2. **Explicit `gitHubToken`** on the attempt input (with `profileId` +
   `profileVersion`). Useful for direct CLI invocations and tests where the
   caller wants to bypass auth-profile resolution.
3. **Contract-resolved `resolvedApiKey` + `authProfileId`** from the
   `EmbeddedRunAttemptParams` shape. This is the **production main path**:
   core resolves the agent's configured `github-copilot` auth profile
   (via `src/infra/provider-usage.auth.ts:resolveProviderAuths`) before
   invoking the harness, and the harness consumes both fields directly.
   This makes a `github-copilot:<profile>` auth profile work end-to-end
   for headless / cron / multi-profile setups without env vars.
4. **Env-var fallback** for direct CLI / dogfood runs where no auth
   profile is configured. The runtime checks the following vars in
   precedence order, mirroring the shipped `github-copilot` provider
   (`extensions/github-copilot/auth.ts`) and the documented Copilot SDK
   setup:
   1. `OPENCLAW_GITHUB_TOKEN` -- harness-specific override; set this
      to pin a token for the OpenClaw harness without disturbing
      system-wide `gh` / Copilot CLI config.
   2. `COPILOT_GITHUB_TOKEN` -- standard Copilot SDK / CLI env var.
   3. `GH_TOKEN` -- standard `gh` CLI env var (matches the existing
      `github-copilot` provider precedence).
   4. `GITHUB_TOKEN` -- generic GitHub token fallback.

   The first non-empty value wins; empty strings are treated as
   absent. The synthesised pool profile id is `env:<NAME>` and the
   profileVersion is a non-reversible sha256 fingerprint of the
   token, so rotating the env value cleanly busts the client pool.

5. **Default `useLoggedInUser`** when no token signal is available.

Each agent gets a dedicated `copilotHome` so Copilot CLI tokens, sessions, and
config do not leak between agents on the same machine. The default is
`<agentDir>/copilot` when the host hands the harness an agent directory
(isolating SDK state from OpenClaw's `models.json` / `auth-profiles.json` in
the same directory), or `~/.openclaw/agents/<agentId>/copilot` otherwise.
Override with `copilotHome: <path>` on the attempt input when you need a
custom location (for example, a shared mount for migration).

Live harness tests use `OPENCLAW_COPILOT_AGENT_LIVE_TOKEN` when a direct token
is needed. The shared live-test setup intentionally scrubs
`COPILOT_GITHUB_TOKEN`, `GH_TOKEN`, and `GITHUB_TOKEN` after staging real auth
profiles into the isolated test home, so passing a `gh auth token` value
through the dedicated live-test variable avoids false skips without exposing
the token to unrelated suites.

## Configuration surface

The harness reads its config from per-attempt input
(`runCopilotAttempt({...})`) plus a small set of env defaults inside
`extensions/copilot/src/`:

- `copilotHome` — per-agent CLI state directory (defaults documented above).
- `model` — string or `{ provider, id, api?, baseUrl?, headers?, authHeader? }`.
  When omitted, OpenClaw uses the agent's normal model selection and the
  harness verifies the resolved provider is supported.
- `reasoningEffort` — `"low" | "medium" | "high" | "xhigh"`. Maps from
  OpenClaw's `ThinkLevel` / `ReasoningLevel` resolution in
  `auto-reply/thinking.ts`.
- `infiniteSessionConfig` — optional override for the SDK
  `infiniteSessions` block driven by `harness.compact`. Defaults are safe to
  leave as-is.
- `hooksConfig` — optional native Copilot SDK `SessionHooks` compatibility
  config for tool/MCP, user-prompt, session, and error callbacks.
  It is separate from OpenClaw's portable lifecycle hooks.
- `permissionPolicy` — optional override for the SDK's
  `onPermissionRequest` handler used for built-in SDK tool kinds
  (`shell`, `write`, `read`, `url`, `mcp`, `memory`, `hook`). Defaults
  to `rejectAllPolicy` as a safety net; in practice the SDK never
  invokes any of those kinds because every bridged OpenClaw tool is
  registered with `overridesBuiltInTool: true` and
  `skipPermission: true` so 100% of tool calls flow through OpenClaw's
  wrapped `execute()`. See [Permissions and ask_user](#permissions-and-ask_user).
- `enableSessionTelemetry` — optional SDK session telemetry flag.

OpenClaw plugin hooks do not need Copilot-specific attempt configuration. The
harness runs `before_prompt_build` (and the legacy `before_agent_start`
compatibility hook), `llm_input`, `llm_output`, and `agent_end` through the
standard harness helpers. Successful SDK compactions also run
`before_compaction` and `after_compaction`. Bridged OpenClaw tools continue to
run `before_tool_call` and report `after_tool_call`; `hooksConfig` remains for
native SDK-only callbacks that have no portable equivalent.

Nothing in the rest of OpenClaw needs to know about these fields. Other
plugins, channels, and core code only see the standard
`AgentHarnessAttemptParams` / `AgentHarnessAttemptResult` shape.

## Compaction

When `harness.compact` runs, the Copilot SDK harness:

1. Resumes the tracked SDK session without continuing pending work.
2. Calls the SDK's session-scoped history compaction RPC.
3. Returns the SDK compaction outcome without writing compatibility marker
   files under the workspace.

The OpenClaw side transcript mirror (see below) continues to receive the
post-compaction messages, so user-facing chat history stays consistent.

## Transcript mirroring

`runCopilotAttempt` dual-writes each turn's mirrorable messages into the
OpenClaw audit transcript via
`extensions/copilot/src/dual-write-transcripts.ts`. The mirror is
per-session scoped (`copilot:${sessionId}`) and uses a per-message
identity (`${role}:${sha256_16(role,content)}`) so re-emits of prior-turn
entries collide with existing on-disk keys and do not duplicate.

The mirror is wrapped in two layers of failure containment so a transcript
write failure cannot fail the attempt: an internal best-effort wrapper and a
defense-in-depth `.catch(...)` at the attempt level. Failures are logged but
not surfaced.

## Side questions (`/btw`)

`/btw` is **not** native on this harness. `createCopilotAgentHarness()`
deliberately leaves `harness.runSideQuestion` undefined, so OpenClaw's `/btw`
dispatcher (`src/agents/btw.ts`) falls through to the same in-tree PI fallback
path it uses for every non-Codex runtime: the configured model provider is
called directly with a short side-question prompt and streamed back via
`streamSimple` (no CLI session, no extra pool slot).

This keeps Copilot CLI sessions reserved for the agent's main turn loop, and
keeps `/btw` behavior identical to other PI-backed runtimes. The contract is
asserted in
[`extensions/copilot/harness.test.ts`](https://github.com/openclaw/openclaw/blob/main/extensions/copilot/harness.test.ts)
under `describe("runSideQuestion")`.

## Doctor

`extensions/copilot/doctor-contract-api.ts` is auto-loaded by
`src/plugins/doctor-contract-registry.ts`. It contributes:

- An empty `legacyConfigRules` (no retired fields at MVP).
- A no-op `normalizeCompatibilityConfig` (kept so future field retirements
  have a stable in-tree home).
- One `sessionRouteStateOwners` entry claiming provider `github-copilot`;
  runtime `copilot`; CLI session key `copilot`; auth profile
  prefix `github-copilot:`.

## Limitations

- The harness claims `github-copilot` plus unowned custom BYOK provider ids.
  Manifest-owned native provider ids stay on their owning runtime even when
  `agentRuntime.id` is forced to `copilot`.
- The harness does not deliver TUI; PI's TUI is unaffected and remains the
  fallback for whatever runtimes do not have a peer surface.
- PI session state is not migrated when an agent switches to `copilot`.
  Selection is per attempt; existing PI sessions remain valid.
- `ask_user` uses the same OpenClaw prompt-and-reply path as the Codex
  harness. When the Copilot SDK asks for user input, OpenClaw posts a
  blocking prompt to the active channel/TUI and the next queued user
  message resolves the SDK request.

## Permissions and ask_user

Permission enforcement for bridged OpenClaw tools happens **inside the
tool wrapper**, not via the SDK's `onPermissionRequest` callback. The
same `wrapToolWithBeforeToolCallHook` that PI uses
(`src/agents/pi-tools.before-tool-call.ts`) is applied by
`createOpenClawCodingTools` to every coding tool: loop detection,
trusted plugin policies, before-tool-call hooks, and two-phase plugin
approvals via the gateway (`plugin.approval.request`) all run with the
exact same code path as native PI attempts.

To let that wrapper own the decision, the SDK Tool returned by
`convertOpenClawToolToSdkTool` is marked with:

- `overridesBuiltInTool: true` — replaces the Copilot CLI's built-in
  tool of the same name (edit, read, write, bash, …) so every tool
  invocation routes back to OpenClaw.
- `skipPermission: true` — tells the SDK not to fire
  `onPermissionRequest({kind: "custom-tool"})` before invoking the tool.
  The wrapped `execute()` performs the richer OpenClaw policy check
  internally; an SDK-level prompt would either short-circuit OpenClaw's
  enforcement (if we allow-all) or block every tool call (if we
  reject-all) — neither matches PI parity.

The in-tree codex harness uses the same split: bridged OpenClaw tools
are wrapped (`extensions/codex/src/app-server/dynamic-tools.ts`) and
the codex-app-server's _own_ native approval kinds
(`item/commandExecution/requestApproval`,
`item/fileChange/requestApproval`,
`item/permissions/requestApproval`) are routed through
`plugin.approval.request`
(`extensions/codex/src/app-server/approval-bridge.ts`). The Copilot SDK
equivalent — fail-closed `rejectAllPolicy` for any non-`custom-tool`
kind that ever reaches `onPermissionRequest` — is the same safety net,
and it does not fire in practice because `overridesBuiltInTool: true`
displaces every built-in.

For the wrapped-tool layer to make policy decisions equivalent to PI,
the harness forwards the full PI attempt-tool context to
`createOpenClawCodingTools` — identity (`senderIsOwner`,
`memberRoleIds`, `ownerOnlyToolAllowlist`, …), channel/routing
(`groupId`, `currentChannelId`, `replyToMode`, message-tool toggles),
auth (`authProfileStore`), run identity
(`sessionKey`/`runSessionKey` derived from `sandboxSessionKey`,
`runId`), model context (`modelApi`, `modelContextWindowTokens`,
`modelCompat`, `modelHasVision`), and run hooks (`onToolOutcome`,
`onYield`). Without those fields, owner-only allowlists silently
behave as deny-by-default, plugin-trust policies cannot resolve to the
right scope, and `session_status: "current"` resolves to a stale
sandbox key. The bridge builder is in
`extensions/copilot/src/tool-bridge.ts` and mirrors the PI
authoritative call at
`src/agents/pi-embedded-runner/run/attempt.ts:1029-1117`. `runAttempt`
already resolves sandbox context through the shared
`resolveSandboxContext` seam, passes the SDK an effective working
directory, and forwards `sandbox` plus the subagent-spawn workspace into
the tool bridge. The bridge also forwards the bounded tool-construction
controls it can enforce at the SDK boundary: `includeCoreTools`, the
runtime tool allowlist, and `toolConstructionPlan`.

The bridge also uses the shared harness tool-surface helper from
`openclaw/plugin-sdk/agent-harness-tool-runtime` for PI parity. When
tool-search is enabled, the SDK sees compact control tools plus a hidden
catalog executor instead of every OpenClaw tool schema. When code mode is
enabled, the helper builds the same code-mode control surface and catalog
lifecycle used by other agent harnesses. Local-model lean defaults,
runtime-compatible schema filtering, directory hydration, and catalog
cleanup all stay in the shared helper so Copilot and Codex-adjacent
harnesses do not drift.

### Session-level GitHub token

The Copilot SDK contract distinguishes the **client-level** GitHub
token (`CopilotClientOptions.gitHubToken`, used to authenticate the
CLI process itself) from the **session-level** token
(`SessionConfig.gitHubToken`, which determines content exclusion,
model routing, and quota for that session and is honored on both
`createSession` and `resumeSession`). The harness resolves auth once
via `resolveCopilotAuth` and sets both fields when the auth mode is
`gitHubToken` (an explicit `auth.gitHubToken` or a contract-resolved
`resolvedApiKey` from a configured `github-copilot` auth profile).
When the resolved mode is `useLoggedInUser`, the session-level field
is omitted so the SDK keeps deriving identity from the logged-in
identity.

`ask_user` uses `SessionConfig.onUserInputRequest`. The bridge accepts
choice indexes or labels for fixed-choice requests, accepts free-form
answers when the SDK request allows them, and cancels a pending request
when the OpenClaw attempt is aborted.

## Related

- [Agent runtimes](/concepts/agent-runtimes)
- [Codex harness](/plugins/codex-harness)
- [Agent harness plugins (SDK reference)](/plugins/sdk-agent-harness)
