Prerequisites
Before creating a cloud session, make sure:
- The user has Copilot access with cloud-agent entitlement.
- The session can authenticate to GitHub, either with a user token or a logged-in Copilot CLI identity.
- You can associate the session with a GitHub repository. This is optional in the SDK type, but recommended so Mission Control and the cloud agent have repository context.
- Organization policies allow remote control and viewing sessions from cloud surfaces.
Creating a cloud session
Set the create-session cloud option to create a cloud session. You can include repository metadata to associate the cloud session with a GitHub repository.
TypeScript
import { CopilotClient } from "@github/copilot-sdk";
const client = new CopilotClient();
await client.start();
const session = await client.createSession({
onPermissionRequest: async () => ({ kind: "approve-once" }),
cloud: {
repository: {
owner: "github",
name: "copilot-sdk",
branch: "main",
},
},
});
Python
from copilot import (
CloudSessionOptions,
CloudSessionRepository,
CopilotClient,
PermissionHandler,
)
client = CopilotClient()
await client.start()
session = await client.create_session(
on_permission_request=PermissionHandler.approve_all,
cloud=CloudSessionOptions(
repository=CloudSessionRepository(
owner="github",
name="copilot-sdk",
branch="main",
)
),
)
Go
package main
import (
"context"
copilot "github.com/github/copilot-sdk/go"
"github.com/github/copilot-sdk/go/rpc"
)
func main() {
_ = run(context.Background())
}
func run(ctx context.Context) error {
client := copilot.NewClient(nil)
if err := client.Start(ctx); err != nil {
return err
}
session, err := client.CreateSession(ctx, &copilot.SessionConfig{
Cloud: &copilot.CloudSessionOptions{
Repository: &copilot.CloudSessionRepository{
Owner: "github",
Name: "copilot-sdk",
Branch: "main",
},
},
OnPermissionRequest: func(_ copilot.PermissionRequest, _ copilot.PermissionInvocation) (rpc.PermissionDecision, error) {
return &rpc.PermissionDecisionApproveOnce{}, nil
},
})
_ = session
return err
}
client := copilot.NewClient(nil)
if err := client.Start(ctx); err != nil {
return err
}
session, err := client.CreateSession(ctx, &copilot.SessionConfig{
Cloud: &copilot.CloudSessionOptions{
Repository: &copilot.CloudSessionRepository{
Owner: "github",
Name: "copilot-sdk",
Branch: "main",
},
},
OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) {
return &rpc.PermissionDecisionApproveOnce{}, nil
},
})
_ = session
.NET
await using var client = new CopilotClient();
var session = await client.CreateSessionAsync(new SessionConfig
{
Cloud = new CloudSessionOptions
{
Repository = new CloudSessionRepository
{
Owner = "github",
Name = "copilot-sdk",
Branch = "main",
},
},
OnPermissionRequest = (req, inv) =>
Task.FromResult(PermissionDecision.ApproveOnce()),
});
Java
import com.github.copilot.CopilotClient;
import com.github.copilot.rpc.*;
try (var client = new CopilotClient()) {
client.start().get();
var session = client.createSession(
new SessionConfig()
.setCloud(new CloudSessionOptions()
.setRepository(new CloudSessionRepository()
.setOwner("github")
.setName("copilot-sdk")
.setBranch("main")))
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
).get();
}
Rust
use std::sync::Arc;
use github_copilot_sdk::{CloudSessionOptions, CloudSessionRepository, SessionConfig};
use github_copilot_sdk::handler::ApproveAllHandler;
let session = client.create_session(
SessionConfig::default()
.with_cloud(CloudSessionOptions::with_repository(
CloudSessionRepository::new("github", "copilot-sdk").with_branch("main"),
))
.with_permission_handler(Arc::new(ApproveAllHandler)),
).await?;
Sending the first prompt
Cloud sessions initialize in two phases: createSession resolves as soon as Mission Control has reserved a task, but the remote copilot-agent worker takes another second or two to connect and emit session.start. If you call session.send before that, the runtime's RemoteSession.send throws "Remote session is still starting" — but the schema wrapper is fire-and-forget and silently swallows the error while still returning a fresh messageId to your code. The prompt is dropped on the server and never reaches the worker.
To send reliably, subscribe to events before sending and await the first session.start event whose producer is "copilot-agent":
import { CopilotClient, type CopilotSession } from "@github/copilot-sdk";
const client = new CopilotClient();
await client.start();
const session: CopilotSession = await client.createSession({
streaming: true, // required for assistant.message_delta to fire
cloud: { repository: { owner: "github", name: "copilot-sdk" } },
onPermissionRequest: async () => ({ kind: "approve-once" }),
});
// Subscribe BEFORE sending so you don't miss the start event.
const ready = new Promise<void>((resolve) => {
const off = session.on("session.start", (event) => {
if (event.data?.producer === "copilot-agent") {
off();
resolve();
}
});
});
await ready;
await session.send({ prompt: "Summarize the README" });
A few notes:
- Set
streaming: trueoncreateSessionso the runtime emitsassistant.message_deltaevents. Without it, the only assistant signal you get is the finalassistant.message— fine for batch use, but the chat will look frozen if you're rendering a live UI. See Événements de session de streaming. - Only the first
session.sendis sensitive to this race. Subsequent sends on the same session work normally because the runtime keepshasSessionStartedset for the life of the session. - Apply a timeout (e.g. 60 s) around the
readypromise so a stuck Mission Control provisioning doesn't hang your app forever. - The same pattern works in every SDK language — subscribe to
session.start, checkproducer === "copilot-agent", then callsend.
Accessing the Mission Control URL
Cloud sessions are inherently remote: once the worker connects, Mission Control publishes the session at https://github.com/copilot/tasks/{sessionId} and the runtime emits a session.info event with the URL. You do not need to call remote.enable() — that API is only for promoting a local session to Mission Control.
Capture the URL by subscribing to session.info and filtering by infoType: "remote":
session.on("session.info", (event) => {
if (event.data?.infoType === "remote" && event.data.url) {
console.log("Open from web or mobile:", event.data.url);
// e.g. surface in your UI as a shareable link or QR code.
}
});
The event fires shortly after session.start. If your renderer mounts after the event has already fired, persist the URL alongside the session record in your app's state and rehydrate on remount — the runtime does not re-emit session.info on its own.
For the same wiring on local sessions promoted via remote: true, see Sessions à distance.
Repository association
The cloud.repository object associates the cloud session with a GitHub repository:
| Field | Required | Description |
|---|---|---|
owner | Yes | Repository owner or organization. |
name | Yes | Repository name. |
branch | No | Branch to use for repository context. Omit it to let the runtime choose the default branch or current repository context. |
Repository association is optional in the SDK type, but include it whenever your app knows the target repository. It helps Mission Control display the session in the right context and gives the cloud agent a clearer starting point.
Use branch when the work should start from a specific branch. If your app is creating sessions from pull requests, issue triage flows, or deployment workflows, pass the branch that matches the user-visible task.
Resuming a cloud session
The cloud option only applies when creating a new session. To resume an existing cloud session, use the standard resume API for the SDK language:
import { CopilotClient } from "@github/copilot-sdk";
const client = new CopilotClient();
await client.start();
const session = await client.resumeSession("session-id", {
onPermissionRequest: async () => ({ kind: "approve-once" }),
});
void session;
const session = await client.resumeSession("session-id", {
onPermissionRequest: async () => ({ kind: "approve-once" }),
});
Do not pass cloud again on resume. The saved session metadata determines that the session is cloud-backed, and resume follows the normal session resume path.
Org policies and entitlements
Cloud session creation can fail when the user or organization is not entitled to cloud-agent execution or when organization-level policies block the flow. In particular, policies for cloud sandbox can prevent clients from creating the cloud task.
When this happens, the runtime reports a "policy_blocked" failure reason for cloud task creation. Treat this as an authorization or policy outcome, not as a transient infrastructure failure.
In TypeScript, check for the reason before retrying:
import {
CopilotClient,
type CloudSessionRepository,
} from "@github/copilot-sdk";
const client = new CopilotClient();
await client.start();
const repository: CloudSessionRepository = {
owner: "github",
name: "copilot-sdk",
};
try {
await client.createSession({
cloud: { repository },
onPermissionRequest: async () => ({ kind: "approve-once" }),
});
} catch (error) {
if ((error as { reason?: string }).reason === "policy_blocked") {
// Show an admin-facing message or link to org policy settings.
}
throw error;
}
try {
await client.createSession({ cloud: { repository } });
} catch (error) {
if ((error as { reason?: string }).reason === "policy_blocked") {
// Show an admin-facing message or link to org policy settings.
}
throw error;
}
In languages where SDK errors are represented differently, inspect the surfaced error reason or code and handle "policy_blocked" explicitly. Retrying without a policy change is not expected to succeed.
Integration ID and routing
Cloud sessions are stamped with a Copilot-Integration-Id header derived from the GITHUB_COPILOT_INTEGRATION_ID environment variable. This integration ID is used by Mission Control for routing, attribution, and integration-specific behavior.
For multi-user server guidance and full integration ID details, see Multi-tenancy and server deployments.
Mission Control routes SDK-created cloud sessions to the copilot-developer-sandbox agent slug. The name is an internal routing slug for the cloud agent and does not mean the session uses the local Windows sandbox.
Advanced: COPILOT_MC_BASE_URL
By default, the runtime derives the Mission Control base URL from the configured Copilot API URL. Set COPILOT_MC_BASE_URL only when you need to override that Mission Control endpoint.
This may be required for GitHub Enterprise Server deployments. Confirm the correct value and support status with your GitHub representative before relying on it in production.
COPILOT_MC_BASE_URL="https://example.com/agents"
Cloud sessions vs. remote sessions
| Capability | Remote sessions | Cloud sessions |
|---|---|---|
| Execution location | Local machine or your server | GitHub-hosted compute |
| Mission Control role | Shares a local session to GitHub web/mobile | Creates and routes the hosted session |
| SDK option | remote: true on the client or session | cloud: { ... } on create session |
| Resume path | Standard resume | Standard resume |
| Windows sandbox relation | Unrelated | Unrelated |
Use remote sessions when the session should execute where the SDK runtime is already running, but also be accessible from Mission Control. Use cloud sessions when the session should execute on GitHub-hosted compute.
Troubleshooting
| Symptom | Likely cause | What to check |
|---|---|---|
Cloud session creation returns "policy_blocked" | Organization policy blocks remote control or view from cloud flows | Check org Copilot policies and user entitlement |
| Session creates without repository context | cloud.repository was omitted | Pass owner, name, and optionally branch |
Resume ignores a new cloud option | cloud only applies to new sessions | Resume the existing session normally |
| Confusion with sandbox settings | Windows sandbox and cloud sessions are separate | Do not use SANDBOX=true for cloud execution |
session.send resolves with a messageId but no assistant.* events fire and Mission Control shows no prompt | The session.send raced ahead of session.start from the remote worker; the runtime swallowed the prompt | Await the first session.start event with producer === "copilot-agent" before sending. See Sending the first prompt |
| Live UI never updates even though the cloud worker is processing | streaming was not set on createSession, so only the final assistant.message is emitted | Set streaming: true on createSession and re-launch |
| Cloud session works but no shareable URL appears in your UI | App never subscribed to session.info for the URL | Subscribe to session.info and filter infoType === "remote". See Accessing the Mission Control URL |
See also
- Sessions à distance: share locally hosted sessions through Mission Control
- Événements de session de streaming: subscribe to
assistant.*deltas for live UI rendering - Multi-tenancy and server deployments: integration IDs and server deployment patterns
- Authentification: configure GitHub authentication for SDK sessions