How to Implement Permission Control for Tool Usage in Copilot SDK: 4 Handler Patterns
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 (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).
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 at lines ≈ 1670‑1674:
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
Inside 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:
if (this.permissionHandler) {
void this._executePermissionAndRespond(requestId, permissionRequest);
}
This occurs around lines ≈ 70‑81 of nodejs/src/session.ts.
The PermissionRequest Discriminated Union
The request schema lives in 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) awaits your handler’s result and relays it:
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:
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:
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:
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 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:
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, demonstrating how permission control for custom tools works end-to-end.
Summary
- Register
onPermissionRequestwhen callingCopilotClient.createSessionorresumeSessionto gate every tool invocation. - Return a
PermissionRequestResultwithkind: "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 viahandlePendingPermissionRequest.
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 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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →