Copilot SDK Hooks: How to Customize Session Behavior with Interceptors
Copilot SDK Hooks are extensibility points that let hosts intercept lifecycle events—such as tool execution and prompt submission—to enforce permissions, transform data, and inject hidden context without modifying the core Copilot runtime.
Copilot SDK Hooks provide a powerful mechanism for customizing how GitHub Copilot sessions behave in VS Code extensions, CLI tools, and other host applications. These interceptors allow developers to implement organization-specific policies, audit logging, and dynamic UI adjustments by registering handler functions that execute at specific points in the session lifecycle. By leveraging the @github/copilot-sdk package, you can control everything from tool permissions to prompt rewriting using typed input and output contracts.
What Are Copilot SDK Hooks?
Copilot SDK Hooks are a set of asynchronous callback functions that the SDK invokes during critical moments of a Copilot session. When you create a session via CopilotClient.createSession(), you provide a SessionHooks object that maps hook types—such as preToolUse or sessionStart—to your custom handler implementations.
Each hook receives a typed *HookInput object (extending BaseHookInput) containing contextual data like sessionId, timestamp, and workingDirectory. Your handler can optionally return a *HookOutput object that modifies the session flow, such as denying a tool call or appending hidden context for the LLM. The SDK's internal dispatcher, CopilotSession._handleHooksInvoke, manages the RPC communication between the runtime and your handlers using HookInvokeRequest and HookInvokeResponse envelopes defined in nodejs/src/generated/rpc.ts.
How Hooks Customize Session Behavior
Hook handlers influence sessions through four primary mechanisms:
- Permission Decisions — Return
permissionDecision: 'deny'to block tool execution, or'ask'to require user confirmation. - Argument and Result Transformation — Modify tool arguments before execution or alter results before they reach the model.
- Additional Hidden Context — Inject guidance text that the LLM receives but the user never sees, useful for recovery from failures.
- Output Suppression — Hide tool results from the UI while still allowing the model to process them.
These capabilities enable use cases ranging from security policy enforcement to automated debugging assistance without forking the Copilot SDK.
Available Hook Types
The Copilot SDK defines seven distinct hook points in nodejs/src/types.ts:
| Hook | When It Fires | Input Type | Customization Options |
|---|---|---|---|
| preToolUse | Before a tool executes | PreToolUseHookInput |
Deny/allow tool, modify arguments, add context |
| postToolUse | After successful tool completion | PostToolUseHookInput |
Transform results, append metadata |
| postToolUseFailure | After tool returns failure | PostToolUseFailureHookInput |
Add recovery guidance via additionalContext |
| userPromptSubmitted | When user sends a message | UserPromptSubmittedHookInput |
Rewrite prompt, inject system context |
| sessionStart | On session creation | SessionStartHookInput |
Provide initial session-wide context |
| sessionEnd | On session close/disconnect | SessionEndHookInput |
Execute cleanup logic, log final state |
| errorOccurred | When internal errors bubble up | ErrorOccurredHookInput |
Record diagnostics, transform error messages |
All input types inherit from BaseHookInput, ensuring consistent access to session metadata across every hook handler.
Implementing Copilot SDK Hooks
Registering Hooks During Session Creation
Define your handlers when calling createSession() on a CopilotClient instance. The following example demonstrates registering multiple hooks to enforce policies and augment context:
import { CopilotClient, CopilotSession } from '@github/copilot-sdk';
const client = new CopilotClient({ /* configuration */ });
const hooks = {
// Block dangerous tools
preToolUse: async (input) => {
if (input.toolName === 'dangerous') {
return {
permissionDecision: 'deny',
permissionDecisionReason: 'Policy violation: dangerous tool blocked'
};
}
},
// Log and prepend reminders to user prompts
userPromptSubmitted: async (input) => {
console.log('Processing prompt:', input.prompt);
return { modifiedPrompt: `[Internal User] ${input.prompt}` };
},
// Provide system context at session start
sessionStart: async () => ({
additionalContext: 'You are assisting an internal development team.'
})
};
const session: CopilotSession = await client.createSession({ hooks });
Source: nodejs/test/e2e/hooks.e2e.test.ts demonstrates similar registration patterns.
Controlling Tool Execution with preToolUse
The preToolUse hook is the primary control point for tool governance. It receives the tool name and arguments before execution, allowing you to implement conditional logic:
it('should block denied tools via preToolUse', async () => {
const hookLog: string[] = [];
const session = await client.createSession({
hooks: {
preToolUse: async (input) => {
hookLog.push(`pre:${input.toolName}`);
if (input.toolName === 'blocked') {
return {
permissionDecision: 'deny',
permissionDecisionReason: 'Blocked by security policy'
};
}
}
}
});
const result = await session.runTool('blocked', { arg: 'value' });
expect(result).toBeUndefined(); // Tool never executed
expect(hookLog).toContain('pre:blocked'); // Hook was invoked
});
When the handler returns permissionDecision: 'deny', the SDK prevents the tool from running and returns undefined to the caller, as verified in nodejs/test/e2e/hooks.e2e.test.ts.
Handling Session Lifecycle Events
Use sessionStart to initialize resources and sessionEnd to guarantee cleanup, even if the session terminates unexpectedly:
import { promises as fs } from 'fs';
import { join } from 'path';
const tempDir = await fs.mkdtemp('/tmp/copilot-session-');
const session = await client.createSession({
hooks: {
sessionStart: async () => {
console.log('Session initialized:', tempDir);
return { additionalContext: 'Working in temporary workspace.' };
},
sessionEnd: async () => {
await fs.rm(tempDir, { recursive: true, force: true });
console.log('Cleanup completed');
}
}
});
// Session work occurs here...
await session.dispose(); // Triggers sessionEnd hook
The sessionEnd hook fires when session.dispose() is called or the connection drops, making it ideal for resource management patterns shown in nodejs/test/e2e/hooks_extended.e2e.test.ts.
Architecture and Dispatch Mechanism
When the Copilot runtime emits a hook event, it serializes the data into a HookInvokeRequest and transmits it over the RPC channel defined in nodejs/src/generated/rpc.ts. The SDK's CopilotSession._handleHooksInvoke method receives this request, matches the string event name (e.g., "preToolUse", "postToolUseFailure") to the corresponding handler in the session's hooks map, and awaits the asynchronous handler.
The handler receives a strongly typed input object—such as PreToolUseHookInput or UserPromptSubmittedHookInput—defined in nodejs/src/types.ts. If the handler returns an object, the dispatcher wraps it in a HookInvokeResponse and applies the modifications to the ongoing session. The unit tests in nodejs/test/client.test.ts verify this dispatcher logic, ensuring that string-typed events correctly map to their respective handler signatures.
Summary
- Copilot SDK Hooks provide interceptors for seven session lifecycle events, from tool execution to error handling.
- Handlers receive typed
*HookInputobjects and return optional*HookOutputobjects to modify behavior. - Permission controls, argument transformation, and hidden context injection occur through the
preToolUse,postToolUse, anduserPromptSubmittedhooks. - Type definitions reside in
nodejs/src/types.ts, while the RPC wire format lives innodejs/src/generated/rpc.ts. - Cleanup and initialization logic belongs in
sessionStartandsessionEndhooks to ensure reliable resource management.
Frequently Asked Questions
What is the difference between preToolUse and postToolUse hooks?
The preToolUse hook fires before the tool executes and can prevent execution entirely by returning permissionDecision: 'deny' or modify arguments before they reach the tool. The postToolUse hook fires after successful completion, allowing you to transform the result or append metadata before the model receives it. Use preToolUse for security policy enforcement and postToolUse for result formatting or logging.
How do I deny a tool execution using Copilot SDK Hooks?
Return an object with permissionDecision: 'deny' and an optional permissionDecisionReason from your preToolUse handler. When the SDK dispatcher processes this output, it prevents the tool from running and returns undefined instead of a result. This pattern appears in nodejs/test/e2e/hooks.e2e.test.ts where tools named "blocked" are rejected programmatically.
Can I modify the user's prompt before it reaches the model?
Yes. Implement the userPromptSubmitted hook and return a modifiedPrompt field in your output object. This allows you to prepend system instructions, sanitize sensitive data, or rewrite queries entirely. The modified prompt replaces the original in the message history sent to the LLM, while the original remains available in the hook input for logging purposes.
Where are hook types defined in the Copilot SDK source code?
All hook input and output interfaces—such as PreToolUseHookInput, PostToolUseFailureHookInput, and BaseHookInput—are defined in nodejs/src/types.ts. The RPC transport contracts (HookInvokeRequest, HookInvokeResponse) are generated in nodejs/src/generated/rpc.ts. Reference these files when implementing custom handlers to ensure type safety across your interceptors.
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 →