How to Allow-List Specific Tools Using the onPreToolUse Hook in Copilot SDK

You can allow-list specific tools by registering an onPreToolUse hook when calling client.createSession, inspecting toolName (and optionally toolArgs) in the PreToolUseHookInput payload, and returning a PreToolUseHookOutput with permissionDecision set to "allow" or "deny".

The GitHub Copilot SDK enables runtime gatekeeping of model-invoked tools through its pre-tool-use hook architecture. By implementing the onPreToolUse handler, you can selectively allow-list specific tools on a per-call basis rather than relying solely on the static availableTools session option. This guide walks through the hook-based approach using the actual type definitions and dispatcher logic found in the github/copilot-sdk repository.

How to Allow-List Specific Tools at Runtime

Before a tool executes, the SDK constructs a PreToolUseHookInput payload containing the toolName, toolArgs, and contextual metadata. This payload is dispatched to your registered handler via Session.handleHook in nodejs/src/session.ts (lines 1078–1085), which awaits your PreToolUseHookOutput response. The corresponding I/O types are declared in nodejs/src/types.ts (lines 1072–1088), where the output shape requires a permissionDecision field that must be "allow", "deny", or "ask".

If the handler returns "deny", the runtime aborts the tool call before any side effects occur; the model receives a blocked-tool message instead of execution results. This behavior is validated in the end-to-end suite at nodejs/test/e2e/hooks.e2e.test.ts (lines 19–30). Unlike the static availableTools list applied at session creation—documented in docs/setup/multi-tenancy.md (line 415)—the hook evaluates every call dynamically.

Basic Hard-Coded Allow-List

The simplest way to allow-list specific tools is to compare input.toolName against a static set of permitted tools inside the hook.

import { CopilotClient, approveAll } from "@github/copilot-sdk";
import type {
  PreToolUseHookInput,
  PreToolUseHookOutput,
} from "@github/copilot-sdk/src/index.js";

const client = new CopilotClient({ /* credentials */ });

const ALLOWED_TOOLS = new Set(["read_file", "write_file"]);

await client.createSession({
  onPermissionRequest: approveAll,
  hooks: {
    onPreToolUse: async (input: PreToolUseHookInput): Promise<PreToolUseHookOutput> => {
      console.log(`Tool request: ${input.toolName}`);

      if (ALLOWED_TOOLS.has(input.toolName)) {
        return { permissionDecision: "allow" };
      }

      return {
        permissionDecision: "deny",
        permissionDecisionReason: "tool not permitted",
      };
    },
  },
});

In this pattern, the ALLOWED_TOOLS set acts as the authoritative registry. Because the hook runs for every tool invocation, returning { permissionDecision: "deny" } immediately prevents disallowed tools from running.

Conditional Allow-Lists Based on Tool Arguments

For policies that depend on payload contents, inspect input.toolArgs before rendering a decision. The following example permits the search_documents tool only when the query string is 200 characters or fewer.

const SESSION_ALLOWED_TOOL = "search_documents";

await client.createSession({
  onPermissionRequest: approveAll,
  hooks: {
    onPreToolUse: async (input) => {
      if (input.toolName === SESSION_ALLOWED_TOOL) {
        const args = input.toolArgs as { query: string };

        if (args.query.length <= 200) {
          return { permissionDecision: "allow" };
        }

        return {
          permissionDecision: "deny",
          permissionDecisionReason: "query too long",
        };
      }

      return { permissionDecision: "deny" };
    },
  },
});

Your hook can also sanitize arguments rather than rejecting the call. Return modifiedArgs in the output object to overwrite values before the tool executes:

return {
  permissionDecision: "allow",
  modifiedArgs: { query: args.query.slice(0, 200) },
};

Dynamic Allow-Lists from External Data Sources

Because the hook is asynchronous, you can fetch live policies from a database or remote service.

async function fetchAllowedToolsForUser(userId: string): Promise<Set<string>> {
  const rows = await db.query(
    "SELECT tool_name FROM allowed_tools WHERE user_id = ?",
    [userId]
  );
  return new Set(rows.map((r) => r.tool_name));
}

await client.createSession({
  onPermissionRequest: approveAll,
  hooks: {
    onPreToolUse: async (input, inv) => {
      const allowed = await fetchAllowedToolsForUser(inv.sessionId);
      return {
        permissionDecision: allowed.has(input.toolName) ? "allow" : "deny",
      };
    },
  },
});

Here, the second argument (inv) exposes session metadata such as sessionId, letting you resolve per-user or per-organization permissions at runtime. This is the recommended pattern for multi-tenant applications that cannot rely on a static configuration.

Summary

  • Register the onPreToolUse function inside the hooks object passed to client.createSession.
  • Inspect input.toolName and input.toolArgs in your handler to enforce allow-list policies.
  • Return permissionDecision: "allow" to permit execution, or "deny" to block the tool before side effects occur.
  • Use modifiedArgs to sanitize inputs instead of rejecting the call entirely.
  • The hook dispatcher is implemented in Session.handleHook at nodejs/src/session.ts (lines 1078–1085), and the I/O types are declared in nodejs/src/types.ts (lines 1072–1088).

Frequently Asked Questions

What is the difference between availableTools and the onPreToolUse hook?

The availableTools option is a static allow-list evaluated once when the session is created. The onPreToolUse hook runs at runtime before every individual tool invocation, allowing you to enforce dynamic policies based on the current conversation state, user identity, or argument content.

Can I modify tool arguments instead of denying the request?

Yes. Your hook can return modifiedArgs inside the PreToolUseHookOutput object. The SDK applies these mutations before executing the tool, which is useful for sanitizing inputs, truncating strings, or injecting default values.

Does the hook run synchronously or asynchronously?

The hook runs asynchronously. According to the implementation in github/copilot-sdk, the handler may perform asynchronous operations such as database queries or remote API calls before returning the permission decision.

Where is the hook invoked in the Copilot SDK source code?

The hook is invoked from Session.handleHook in nodejs/src/session.ts (lines 1078–1085). This dispatcher deserializes the PreToolUseHookInput, calls the registered onPreToolUse handler, and routes the resulting PreToolUseHookOutput back to the runtime to either proceed with or abort the tool call.

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 →