---
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`

Anything outside that set falls through `selection.ts`'s `auto_pi` branch back
to PI.

## 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? }`. When omitted, OpenClaw uses
  the agent's normal model selection and the harness verifies the resolved
  provider is in the supported set.
- `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 only claims the canonical `github-copilot` provider at MVP.
  Additional providers (BYOK or otherwise) should land in follow-up PRs that
  ship the adapter alongside the wire-up.
- 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)
