How to Implement Custom Tool Permission Control in Copilot SDK Hooks

The Copilot SDK enforces a deny-by-default security model where every custom tool execution pauses until your onPermissionRequest handler returns approved, denied, or approve-once to explicitly control access.

The GitHub Copilot SDK Node.js library requires explicit permission grants before executing any sensitive operation, including file writes, shell commands, and custom tools. To implement custom tool permission control in Copilot SDK hooks, you must provide an onPermissionRequest handler when calling client.createSession() or client.resumeSession(), as the default behavior blocks all operations until your code explicitly authorizes them.

Understanding the Permission Control Architecture

The permission system relies on two core TypeScript interfaces defined in nodejs/src/types.ts and invoked by the session manager in nodejs/src/session.ts.

PermissionRequest and PermissionRequestResult Types

The PermissionRequest object describes the pending action, exposing properties such as kind (e.g., "write", "custom-tool"), toolName, args, and managedApprovalRequired. The PermissionRequestResult is the union type that your handler must return: { kind: "approved" }, { kind: "denied" }, or { kind: "approve-once" }.

The onPermissionRequest Hook

This hook is registered in the configuration object passed to createSession() or resumeSession(). According to nodejs/src/session.ts, the SDK invokes this callback before executing any tool that requires elevated permissions. The handler receives the request object and an invocation context containing the toolCallId, and must resolve with a decision. If you omit the hook, the SDK emits a permission.requested event and pauses execution until resolved via the permission RPC, as demonstrated in nodejs/test/e2e/permissions.e2e.test.ts.

Implementing Custom Permission Handlers

You can implement synchronous or asynchronous handlers to inspect request metadata and apply your own authorization logic.

Approving All Requests (Development Mode)

For testing or trusted environments, the SDK provides the approveAll helper that automatically grants permissions unless managed approval is enforced:

import { createClient, approveAll } from "copilot-sdk";

const client = createClient({ apiKey: process.env.COPILOT_API_KEY });
const session = await client.createSession({
  onPermissionRequest: approveAll
});

Note: approveAll will throw an error if managedApprovalRequired is true, forcing you to implement a custom UI flow that respects user-controlled gating.

Filtering by Tool Type

To restrict access to specific operations, inspect the kind property before returning a decision:

import { PermissionRequest, PermissionRequestResult } from "copilot-sdk";

async function strictPermissionHandler(
  request: PermissionRequest,
  invocation: { toolCallId: string }
): Promise<PermissionRequestResult> {
  // Only allow file writes, deny everything else
  if (request.kind === "write") {
    return { kind: "approved" };
  }
  
  console.warn(`Blocked ${request.kind} request for tool: ${request.toolName}`);
  return { kind: "denied" };
}

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

When the handler returns denied, the SDK skips tool execution and produces an error message similar to "Access denied: insufficient permissions to read secrets", as validated in nodejs/test/e2e/tool_results.e2e.test.ts.

One-Time Approval Pattern

For interactive applications requiring user confirmation, return approve-once to grant temporary access without persisting the decision:

async function interactiveApprovalHandler(
  request: PermissionRequest
): Promise<PermissionRequestResult> {
  if (request.kind === "custom-tool" && request.toolName === "deploy-production") {
    // Display modal and wait for user response
    const confirmed = await showConfirmationDialog(
      `Allow ${request.toolName} with args: ${JSON.stringify(request.args)}?`
    );
    return confirmed ? { kind: "approve-once" } : { kind: "denied" };
  }
  
  // Default deny for unspecified tools
  return { kind: "denied" };
}

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

This pattern is particularly effective for high-risk operations documented in nodejs/docs/examples.md.

Handling Permissions via RPC Events (Fallback Path)

If you do not provide an onPermissionRequest handler during session initialization, the SDK falls back to an event-driven model. You must listen for permission.requested events and resolve them using session.rpc.permissions.handlePendingPermissionRequest:

const session = await client.createSession();

session.rpc.events.on("permission.requested", async (event) => {
  const { requestId, permissionRequest } = event.data;
  
  // Custom logic to evaluate the request
  const decision = await evaluateRequestInUI(permissionRequest);
  
  await session.rpc.permissions.handlePendingPermissionRequest({
    requestId,
    decision, // { kind: "approved" } | { kind: "denied" } | { kind: "approve-once" }
  });
});

This approach is common in UI-driven applications where the permission decision requires user interaction separated from the initial session creation.

Managed Approval and Security Considerations

When users enable managed settings (enterprise policies), the managedApprovalRequired flag on the request object becomes true. In this scenario, automatic handlers like approveAll will fail intentionally. You must implement a handler that respects these settings by requiring explicit human confirmation or consulting an external policy service before returning approved. The end-to-end tests in nodejs/test/e2e/permissions.e2e.test.ts and nodejs/test/e2e/tools.e2e.test.ts validate these security boundaries, ensuring that custom tools cannot bypass organizational policy.

Summary

  • Deny-by-default: The Copilot SDK blocks all tool executions unless explicitly authorized through the onPermissionRequest hook.
  • Three decision types: Return approved for permanent access, denied to block execution, or approve-once for temporary single-use grants.
  • Registration point: Pass your handler to client.createSession() or client.resumeSession(); alternatively, use RPC event handling for deferred decisions.
  • Managed compliance: Respect managedApprovalRequired by bypassing automatic approval helpers and enforcing user confirmation flows.
  • Source references: Implementation details reside in nodejs/src/types.ts, nodejs/src/session.ts, and validation tests in nodejs/test/e2e/permissions.e2e.test.ts.

Frequently Asked Questions

What happens if I don't provide an onPermissionRequest handler?

If you omit the handler, the SDK switches to RPC event mode. It emits permission.requested events through session.rpc.events and pauses tool execution indefinitely until you call session.rpc.permissions.handlePendingPermissionRequest() with a decision. This allows UI components to prompt users asynchronously without blocking session initialization.

How do I implement one-time approval prompts for sensitive operations?

Return { kind: "approve-once" } from your onPermissionRequest handler after obtaining user confirmation through a modal or CLI prompt. This grants permission for the current invocation only; subsequent identical requests will trigger the handler again, requiring re-approval. This pattern is recommended for production deployments with dangerous tools like production deployers or secret accessors.

What is the difference between approved and approve-once?

The approved decision grants permanent permission for the specific request type within the session, while approve-once authorizes only the current invocation. According to the logic in nodejs/src/session.ts, approve-once does not cache the decision in the session's permission store, ensuring that recurring sensitive operations require continuous explicit consent.

How does the managed approval setting affect custom permission handlers?

When managedApprovalRequired is true (indicating enterprise policy enforcement), built-in helpers like approveAll will throw errors to prevent automatic authorization. Your custom handler must detect this flag and route the decision through your organization's approval workflow or user confirmation UI, ensuring compliance with security policies as tested in nodejs/test/e2e/permissions.e2e.test.ts.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →