# How to Implement Permission Control for Tool Usage in Copilot SDK: 4 Handler Patterns

> Implement permission control for tool usage in Copilot SDK using 4 handler patterns. Intercept tool calls with a strongly-typed PermissionHandler and return allow, deny, or no-result.

- Repository: [GitHub/copilot-sdk](https://github.com/github/copilot-sdk)
- Tags: how-to-guide
- Published: 2026-06-05

---

**Register an `onPermissionRequest` handler when creating a session to intercept tool calls through a strongly-typed `PermissionHandler`, return `allow`, `deny`, or `no-result`, and optionally call `session.rpc.permissions.setApproveAll()` for non-interactive environments.**

The Copilot SDK requires every tool call that affects the host environment to pass through a permission gate. To implement permission control for tool usage in Copilot SDK, you supply a handler during session creation that inspects each `PermissionRequest` and decides whether the operation proceeds. This article walks through the exact RPC flow, source file locations, and runnable TypeScript patterns derived from the `github/copilot-sdk` source code.

## How Permission Control Works in Copilot SDK

When the runtime attempts to invoke a tool, it emits a `permission.requested` broadcast event. The SDK routes this event through `Session._handleBroadcastEvent` in [`nodejs/src/session.ts`](https://github.com/github/copilot-sdk/blob/main/nodejs/src/session.ts) (lines ≈ 70‑81), extracts the `requestId` and `PermissionRequest` payload, and delegates to your registered handler. Your handler’s decision is then transmitted back to the server via `session.rpc.permissions.handlePendingPermissionRequest` (defined in [`nodejs/src/generated/rpc.ts`](https://github.com/github/copilot-sdk/blob/main/nodejs/src/generated/rpc.ts)).

### Register a `PermissionHandler` During Session Creation

The public API accepts an optional `onPermissionRequest` callback in `CopilotClient.createSession` and `resumeSession`. The type is declared in [`nodejs/src/types.ts`](https://github.com/github/copilot-sdk/blob/main/nodejs/src/types.ts) at lines ≈ 1670‑1674:

```ts
type PermissionHandler = (
  request: PermissionRequest,
  context: { sessionId: string }
) => Promise<PermissionRequestResult> | PermissionRequestResult;

```

If you omit this option, the SDK still surfaces the `permission.requested` event, but the request remains pending until your application manually calls the RPC `handlePendingPermissionRequest` method.

### SDK Event Routing in [`session.ts`](https://github.com/github/copilot-sdk/blob/main/session.ts)

Inside [`nodejs/src/session.ts`](https://github.com/github/copilot-sdk/blob/main/nodejs/src/session.ts), the `_handleBroadcastEvent` method listens for the broadcast event. When it detects a permission request, it forwards the payload to your handler:

```ts
if (this.permissionHandler) {
  void this._executePermissionAndRespond(requestId, permissionRequest);
}

```

This occurs around lines ≈ 70‑81 of [`nodejs/src/session.ts`](https://github.com/github/copilot-sdk/blob/main/nodejs/src/session.ts).

### The `PermissionRequest` Discriminated Union

The request schema lives in [`nodejs/src/generated/session-events.ts`](https://github.com/github/copilot-sdk/blob/main/nodejs/src/generated/session-events.ts) (lines ≈ 4535‑4550) and uses a discriminated union keyed by `kind`. Common values include `"write"`, `"read"`, `"shell"`, `"url"`, and `"custom-tool"`. You can inspect these fields—along with `toolCallId`, `possiblePaths`, and `intention`—to enforce granular security policies.

### Replying to the Runtime via RPC

The private method `_executePermissionAndRespond` (lines ≈ 83‑94 in [`nodejs/src/session.ts`](https://github.com/github/copilot-sdk/blob/main/nodejs/src/session.ts)) awaits your handler’s result and relays it:

```ts
await this.rpc.permissions.handlePendingPermissionRequest({
  requestId,
  result,
});

```

If your handler throws, the SDK automatically returns `{ kind: "user-not-available" }` so the tool call fails gracefully rather than hanging.

## Practical Implementation Examples

### Example 1: Interactive Prompt Handler

For CLI tools, prompt the user each time a tool requests access:

```ts
import { CopilotClient, PermissionHandler } from "copilot-sdk";

const askUser: PermissionHandler = async (req, ctx) => {
  console.log(`Permission request (${req.kind}): ${req.intention ?? ""}`);
  const answer = await prompt("Allow? (y/N) ");
  return answer.toLowerCase() === "y" 
    ? { kind: "allow" } 
    : { kind: "deny" };
};

const client = new CopilotClient();
const session = await client.createSession({
  model: "gpt-4",
  onPermissionRequest: askUser,
});

```

This handler receives the full request object so you can render detailed prompts or logs before deciding.

### Example 2: Policy-Driven Workspace Sandbox

To deny any file write outside a designated workspace, inspect the `PermissionRequestWrite` shape and compare paths:

```ts
import path from "node:path";
import { PermissionHandler, PermissionRequestWrite } from "copilot-sdk";

const workspaceRoot = "/my/project";

const policyHandler: PermissionHandler = (req) => {
  if (req.kind === "write") {
    const writeReq = req as PermissionRequestWrite;
    const absolute = path.resolve(workspaceRoot, writeReq.fileName);
    
    if (!absolute.startsWith(workspaceRoot)) {
      return { kind: "deny" };
    }
    return { kind: "allow" };
  }
  return { kind: "deny" };
};

const session = await client.createSession({
  onPermissionRequest: policyHandler,
});

```

This pattern ensures that even if the assistant suggests a write outside the project root, the SDK blocks it before the tool executes.

### Example 3: CI/CD Auto-Approve with `setApproveAll`

In automated test suites or CI pipelines where no user is present, bypass the handler entirely:

```ts
const session = await client.createSession({
  onPermissionRequest: () => ({ kind: "no-result" }),
});

await session.rpc.permissions.setApproveAll({ enabled: true });

```

When `setApproveAll` is enabled, the runtime auto-approves every request without invoking your handler, as exercised in the [`nodejs/test/e2e/permissions.e2e.test.ts`](https://github.com/github/copilot-sdk/blob/main/nodejs/test/e2e/permissions.e2e.test.ts) test suite.

### Example 4: Capturing Custom Tool Permissions

For custom tools registered through a `ToolSet`, you can record requests for audit logging while still allowing execution:

```ts
const permissionRequests: PermissionRequest[] = [];

const captureHandler: PermissionHandler = (req) => {
  permissionRequests.push(req);
  return { kind: "allow" };
};

const session = await client.createSession({
  onPermissionRequest: captureHandler,
  availableTools: new ToolSet().addCustom("*"),
});

await session.rpc.tools.invokeCustomTool({ toolName: "my_tool", args: {} });

```

This pattern is drawn directly from the e2e tests in [`nodejs/test/e2e/tools.e2e.test.ts`](https://github.com/github/copilot-sdk/blob/main/nodejs/test/e2e/tools.e2e.test.ts), demonstrating how permission control for custom tools works end-to-end.

## Summary

- **Register `onPermissionRequest`** when calling `CopilotClient.createSession` or `resumeSession` to gate every tool invocation.
- **Return a `PermissionRequestResult`** with `kind: "allow"`, `"deny"`, `"no-result"`, or `"user-not-available"` depending on your policy.
- **Inspect `request.kind`**—such as `"write"`, `"read"`, `"shell"`, or `"custom-tool"`—to apply fine-grained rules.
- **Call `session.rpc.permissions.setApproveAll({ enabled: true })`** for non-interactive environments that require automated approval.
- **Handle errors gracefully**: if your handler throws, the SDK translates the failure into a `"user-not-available"` denial via `handlePendingPermissionRequest`.

## Frequently Asked Questions

### What happens if I omit the `onPermissionRequest` handler?

If you do not provide a handler, the SDK emits the `permission.requested` event but leaves the request pending. Your application must later resolve it by calling `session.rpc.permissions.handlePendingPermissionRequest` with the appropriate `requestId` and result.

### How do I auto-approve every tool request for automated testing?

Call `await session.rpc.permissions.setApproveAll({ enabled: true })`. This instructs the runtime to bypass your handler and grant every permission automatically, which is the pattern used in the [`nodejs/test/e2e/permissions.e2e.test.ts`](https://github.com/github/copilot-sdk/blob/main/nodejs/test/e2e/permissions.e2e.test.ts) test suite for CI scenarios.

### What permission result types does the Copilot SDK support?

Your handler can return `{ kind: "allow" }` to grant access, `{ kind: "deny" }` to reject the tool call, `{ kind: "no-result" }` to leave the request pending for UI-driven flows, or `{ kind: "user-not-available" }` to indicate the decision path failed.

### Can I implement asynchronous, UI-driven permission flows?

Yes. Return `{ kind: "no-result" }` from your handler and listen for the `permission.requested` event separately. When the user finally approves or denies through your UI, send the decision via `session.rpc.permissions.handlePendingPermissionRequest` using the original `requestId`.