# Session Isolation Patterns for Multi-Tenant Copilot SDK Deployments: Three Secure Strategies

> Secure multi-tenant Copilot SDK deployments using session isolation patterns. Learn three strategies to prevent data leakage and errors with unique session IDs and auth handlers.

- Repository: [GitHub/copilot-sdk](https://github.com/github/copilot-sdk)
- Tags: best-practices
- Published: 2026-08-02

---

**To isolate tenants in multi-tenant Copilot SDK deployments, generate unique tenant-scoped `sessionId` values via `SessionConfig`, attach per-tenant authentication handlers such as `McpAuthHandler`, and serialize concurrent access using an external lock to prevent the `SendWhileWaiting` error and data leakage between customers.**

When building SaaS platforms that embed GitHub Copilot, ensuring that prompts, tool calls, and generated artifacts remain strictly partitioned between customers is critical. The **session isolation patterns for multi-tenant deployments with Copilot SDK** provide three architectural strategies ranging from full process isolation to shared memory models. This guide examines the implementation details found in the `github/copilot-sdk` repository, including the `Session` lifecycle in [`rust/src/session.rs`](https://github.com/github/copilot-sdk/blob/main/rust/src/session.rs) and configuration patterns from [`docs/setup/scaling.md`](https://github.com/github/copilot-sdk/blob/main/docs/setup/scaling.md).

## Understanding the Three Isolation Patterns

The Copilot SDK supports three distinct isolation patterns, each trading resource overhead against security guarantees:

| Pattern | Isolation Level | Resource Cost | Typical Use-Case |
|---|---|---|---|
| **Per-user CLI instance** | Full OS-process isolation (dedicated Copilot-CLI binary per tenant) | High | Highly regulated environments requiring strict data separation |
| **Shared CLI, isolated sessions** | Single CLI process serves many tenants via unique `sessionId` values | Low | Most SaaS workloads where tenant data lives in-memory or on shared storage |
| **Shared session (collaborative)** | Multiple tenants share the same `sessionId` | Very low | Collaborative code-review or pair-programming rooms |

The SDK does **not** provide internal locking mechanisms. Callers must serialize access to a given session when using the shared patterns.

## Session Lifecycle and Event Loop Architecture

A **session** is created via `Client::create_session` or resumed with `Client::resume_session`. The method constructs a wire payload from `SessionConfig` and optionally registers the session before the RPC is sent when the client supplies its own `sessionId`.

**Session registration** occurs in `Client::register_session`, which stores a channel pair (`SessionChannels`) in the router at [`rust/src/session.rs#L891-L894`](https://github.com/github/copilot-sdk/blob/main/rust/src/session.rs#L891-L894). This allows the SDK’s internal event loop to route RPC replies back to the correct `Session` object.

When the client does **not** provide a `sessionId`, an inline callback parses the `session.create` response, registers the new server-generated ID, and saves the channels in an `inline_stash` ([`rust/src/session.rs#L959-L981`](https://github.com/github/copilot-sdk/blob/main/rust/src/session.rs#L959-L981)).

After registration, `spawn_event_loop` creates a Tokio task that continuously reads inbound messages, dispatches them to per-event handlers, and broadcasts `SessionEvent`s via a `tokio::sync::broadcast::Sender`. The loop is cancelled cooperatively via a `CancellationToken` ([`rust/src/session.rs#L1023-L1030`](https://github.com/github/copilot-sdk/blob/main/rust/src/session.rs#L1023-L1030)).

## Enforcing Tenant Isolation Guarantees

The SDK provides several mechanisms to enforce separation, but requires explicit developer implementation:

**Unique Session IDs**
The `SessionId` type is a thin wrapper around a string. Developers must generate IDs with a tenant prefix to ensure global uniqueness:

```rust
format!("tenant-{}-{}", tenant_id, uuid::Uuid::new_v4())

```

**Authorization Handlers**
Per-session handlers such as `McpAuthHandler` and `PermissionHandler` (defined in [[`rust/src/handler.rs`](https://github.com/github/copilot-sdk/blob/main/rust/src/handler.rs)](https://github.com/github/copilot-sdk/blob/main/rust/src/handler.rs)) are attached via `SessionConfig`. Register tenant-specific auth providers to ensure credentials remain scoped.

**State Storage**
By default, the CLI stores session state under `~/.copilot/session-state/`. For multi-tenant deployments, configure a **shared storage** backend (NFS, Redis, or S3) via a custom `SessionFsProvider` implementation ([[`rust/src/session_fs.rs`](https://github.com/github/copilot-sdk/blob/main/rust/src/session_fs.rs)](https://github.com/github/copilot-sdk/blob/main/rust/src/session_fs.rs)).

**Concurrent Access Serialization**
The SDK does **not** serialize calls internally. The `send_and_wait` method uses an `IdleWaiter` slot protected by a `parking_lot::Mutex`. If two callers issue concurrent sends, the second receives `ErrorKind::Session(SessionErrorKind::SendWhileWaiting)`. You must implement application-layer serialization, such as a per-session `tokio::sync::Mutex` or distributed lock.

## Implementation Guide

### Creating Isolated Sessions with Tenant-Scoped IDs

Generate deterministic session IDs that encode the tenant identifier to prevent collisions:

```rust
use github_copilot_sdk::{
    client::Client,
    types::{SessionConfig, SessionId},
};

/// Returns a session whose ID is namespaced by the tenant.
pub async fn session_for_tenant(client: &Client, tenant_id: u64) -> Result<Session, Error> {
    // Build a deterministic session ID: "tenant-42-<random>"
    let session_id = SessionId::new(format!("tenant-{}-{}", tenant_id, uuid::Uuid::new_v4()));

    // Attach a per-tenant token provider for MCP auth
    let config = SessionConfig::default()
        .with_session_id(session_id.clone())
        .with_mcp_oauth_handler(MyTenantMcpHandler::new(tenant_id));

    client.create_session(config).await
}

```

The `SessionConfig::with_mcp_oauth_handler` registers a tenant-specific `McpAuthHandler`, ensuring that authorization tokens remain isolated to the tenant's session.

### Serializing Access to Prevent Session Conflicts

Wrap shared sessions in a struct that enforces one-at-a-time access to avoid the `SendWhileWaiting` error:

```rust
use std::sync::Arc;
use tokio::sync::Mutex;
use github_copilot_sdk::session::Session;

struct SharedSession {
    session: Arc<Session>,
    lock: Arc<Mutex<()>>,         // Guarantees one-at-a-time send_and_wait
}

impl SharedSession {
    pub async fn ask(&self, prompt: &str) -> Result<Option<SessionEvent>, Error> {
        let _guard = self.lock.lock().await;     // Serialize
        self.session.send_and_wait(prompt).await
    }
}

```

Any concurrent request to the same session queues behind the mutex, preventing race conditions in the SDK's internal `IdleWaiter`.

### Configuring Persistent Shared Storage

For sessions that must survive CLI restarts or be accessible from multiple service instances, provide a custom `SessionFsProvider` backed by shared storage:

```rust
use github_copilot_sdk::session_fs::{SessionFsProvider, SessionFsSqliteProvider};
use std::path::PathBuf;

fn shared_fs_provider(root: PathBuf) -> SessionFsProvider {
    // Example: SQLite-backed provider stored on a network volume
    let sqlite = SessionFsSqliteProvider::new(root.join("session.sqlite"));
    SessionFsProvider::new().with_sqlite(sqlite)
}

// When constructing a session:
let config = SessionConfig::default()
    .with_session_fs_provider(shared_fs_provider(PathBuf::from("/mnt/copilot-shared")));
client.create_session(config).await?;

```

The provider ensures session state (history, plan files, etc.) persists across process restarts and remains accessible from any server instance mounting the shared volume.

## Summary

- **Generate tenant-scoped `sessionId` values** when calling `Client::create_session` to ensure logical isolation in shared CLI deployments.
- **Attach tenant-specific auth handlers** via `SessionConfig` to keep authorization credentials partitioned per customer.
- **Implement external locking** (e.g., `tokio::sync::Mutex`) around `send_and_wait` calls to prevent `SendWhileWaiting` errors in concurrent environments.
- **Use custom `SessionFsProvider` implementations** for shared storage when session state must persist across CLI processes or service instances.
- **Reference [`docs/setup/scaling.md`](https://github.com/github/copilot-sdk/blob/main/docs/setup/scaling.md)** for architectural guidance on selecting between process isolation and shared session patterns.

## Frequently Asked Questions

### How does the Copilot SDK prevent session bleeding between tenants?

The SDK itself does not automatically enforce tenant isolation. According to the `github/copilot-sdk` source code, you must generate unique `sessionId` values with tenant prefixes and attach tenant-specific `McpAuthHandler` instances via `SessionConfig`. The SDK routes messages based on these IDs but relies on your application to supply properly scoped identifiers and authentication logic.

### What is the difference between `create_session` and `resume_session`?

`Client::create_session` initializes a new session and optionally registers a client-supplied `sessionId` immediately (or defers registration for server-generated IDs via an inline callback at [`rust/src/session.rs#L959-L981`](https://github.com/github/copilot-sdk/blob/main/rust/src/session.rs#L959-L981)). `Client::resume_session` reconnects to an existing session using a previously established ID, allowing you to restore state from a shared `SessionFsProvider` without starting a new conversation context.

### How should I handle concurrent requests to the same session?

You must serialize access at the application layer. The SDK's `send_and_wait` implementation uses a `parking_lot::Mutex` protected `IdleWaiter`, and concurrent calls return `ErrorKind::Session(SessionErrorKind::SendWhileWaiting)`. Wrap your `Session` in a struct with a `tokio::sync::Mutex` or use a distributed lock service to ensure only one request interacts with the session at a time.

### Where should I store session state in a multi-tenant environment?

Avoid the default `~/.copilot/session-state/` location. Instead, implement a custom `SessionFsProvider` (as defined in [[`rust/src/session_fs.rs`](https://github.com/github/copilot-sdk/blob/main/rust/src/session_fs.rs)](https://github.com/github/copilot-sdk/blob/main/rust/src/session_fs.rs)) that writes to a multi-tenant storage bucket such as S3, NFS, or Redis. This allows any instance of your service to resume a tenant's session regardless of which CLI process or server node handles the request.