Skip to main content

Pre-tool use hook

The onPreToolUse hook is called before a tool executes. Use it to:

  • Approve or deny tool execution
  • Modify tool arguments
  • Add context for the tool
  • Suppress tool output from the conversation

Hook signature

Code languages navigation

TypeScript
import type { PreToolUseHookInput, HookInvocation, PreToolUseHookOutput } from "@github/copilot-sdk";
type PreToolUseHandler = (
  input: PreToolUseHookInput,
  invocation: HookInvocation
) => Promise<PreToolUseHookOutput | null | undefined>;
type PreToolUseHandler = (
  input: PreToolUseHookInput,
  invocation: HookInvocation
) => Promise<PreToolUseHookOutput | null | undefined>;

Input

FieldTypeDescription
timestampnumberUnix timestamp when the hook was triggered
cwdstringCurrent working directory
toolNamestringName of the tool being called
toolArgsobjectArguments passed to the tool

Output

Return null or undefined to allow the tool to execute with no changes. Otherwise, return an object with any of these fields:

FieldTypeDescription
permissionDecision"allow" | "deny" | "ask"Whether to allow the tool call
permissionDecisionReasonstringExplanation shown to user (for deny/ask)
modifiedArgsobjectModified arguments to pass to the tool
additionalContextstringExtra context injected into the conversation
suppressOutputbooleanIf true, tool output won't appear in conversation

Permission decisions

DecisionBehavior
"allow"Tool executes normally
"deny"Tool is blocked, reason shown to user
"ask"User is prompted to approve (interactive mode)

Skipping permission prompts for trusted custom tools

If you define a custom tool that is safe to run without prompting, set skipPermission: true on the tool definition. Use this for trusted, app-owned tools whose inputs are already constrained by your application; use onPreToolUse when you need per-call policy checks or argument validation.

const getWeather = defineTool("get_weather", {
  description: "Get weather for a location.",
  parameters: {
    type: "object",
    properties: { location: { type: "string" } },
    required: ["location"],
  },
  skipPermission: true,
  handler: async ({ location }) => ({ forecast: `Sunny in ${location}` }),
});

Examples

Allow all tools (logging only)

Code languages navigation

TypeScript
const session = await client.createSession({
  hooks: {
    onPreToolUse: async (input, invocation) => {
      console.log(`[${invocation.sessionId}] Calling ${input.toolName}`);
      console.log(`  Args: ${JSON.stringify(input.toolArgs)}`);
      return { permissionDecision: "allow" };
    },
  },
});

Block specific tools

const BLOCKED_TOOLS = ["shell", "bash", "write_file", "delete_file"];

const session = await client.createSession({
  hooks: {
    onPreToolUse: async (input) => {
      if (BLOCKED_TOOLS.includes(input.toolName)) {
        return {
          permissionDecision: "deny",
          permissionDecisionReason: `Tool '${input.toolName}' is not permitted in this environment`,
        };
      }
      return { permissionDecision: "allow" };
    },
  },
});

Modify tool arguments

const session = await client.createSession({
  hooks: {
    onPreToolUse: async (input) => {
      // Add a default timeout to all shell commands
      if (input.toolName === "shell" && input.toolArgs) {
        const args = input.toolArgs as { command: string; timeout?: number };
        return {
          permissionDecision: "allow",
          modifiedArgs: {
            ...args,
            timeout: args.timeout ?? 30000, // Default 30s timeout
          },
        };
      }
      return { permissionDecision: "allow" };
    },
  },
});

Restrict file access to specific directories

const ALLOWED_DIRECTORIES = ["/home/user/projects", "/tmp"];

const session = await client.createSession({
  hooks: {
    onPreToolUse: async (input) => {
      if (input.toolName === "read_file" || input.toolName === "write_file") {
        const args = input.toolArgs as { path: string };
        const isAllowed = ALLOWED_DIRECTORIES.some(dir => 
          args.path.startsWith(dir)
        );
        
        if (!isAllowed) {
          return {
            permissionDecision: "deny",
            permissionDecisionReason: `Access to '${args.path}' is not permitted. Allowed directories: ${ALLOWED_DIRECTORIES.join(", ")}`,
          };
        }
      }
      return { permissionDecision: "allow" };
    },
  },
});

Suppress verbose tool output

const VERBOSE_TOOLS = ["list_directory", "search_files"];

const session = await client.createSession({
  hooks: {
    onPreToolUse: async (input) => {
      return {
        permissionDecision: "allow",
        suppressOutput: VERBOSE_TOOLS.includes(input.toolName),
      };
    },
  },
});

Add context based on tool

const session = await client.createSession({
  hooks: {
    onPreToolUse: async (input) => {
      if (input.toolName === "query_database") {
        return {
          permissionDecision: "allow",
          additionalContext: "Remember: This database uses PostgreSQL syntax. Always use parameterized queries.",
        };
      }
      return { permissionDecision: "allow" };
    },
  },
});

Best practices

  1. Always return a decision - Returning null allows the tool, but being explicit with { permissionDecision: "allow" } is clearer.

  2. Provide helpful denial reasons - When denying, explain why so users understand:

    return {
      permissionDecision: "deny",
      permissionDecisionReason: "Shell commands require approval. Please describe what you want to accomplish.",
    };
    
  3. Be careful with argument modification - Ensure modified args maintain the expected schema for the tool.

  4. Consider performance - Pre-tool hooks run synchronously before each tool call. Keep them fast.

  5. Use suppressOutput judiciously - Suppressing output means the model won't see the result, which may affect conversation quality.

See also