# How to Implement Custom Tool Permission Control in Copilot SDK Hooks

> Learn how to implement custom tool permission control in Copilot SDK hooks using the deny-by-default security model. Master the onPermissionRequest handler for explicit access control.

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

---

**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`](https://github.com/github/copilot-sdk/blob/main/nodejs/src/types.ts) and invoked by the session manager in [`nodejs/src/session.ts`](https://github.com/github/copilot-sdk/blob/main/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`](https://github.com/github/copilot-sdk/blob/main/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`](https://github.com/github/copilot-sdk/blob/main/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:

```typescript
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:

```typescript
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`](https://github.com/github/copilot-sdk/blob/main/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:

```typescript
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`](https://github.com/github/copilot-sdk/blob/main/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`:

```typescript
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`](https://github.com/github/copilot-sdk/blob/main/nodejs/test/e2e/permissions.e2e.test.ts) and [`nodejs/test/e2e/tools.e2e.test.ts`](https://github.com/github/copilot-sdk/blob/main/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`](https://github.com/github/copilot-sdk/blob/main/nodejs/src/types.ts), [`nodejs/src/session.ts`](https://github.com/github/copilot-sdk/blob/main/nodejs/src/session.ts), and validation tests in [`nodejs/test/e2e/permissions.e2e.test.ts`](https://github.com/github/copilot-sdk/blob/main/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`](https://github.com/github/copilot-sdk/blob/main/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`](https://github.com/github/copilot-sdk/blob/main/nodejs/test/e2e/permissions.e2e.test.ts).