# How to Add Custom Hooks to Modify Session Behavior or Transform Tool Results in the Copilot SDK

> Learn how to add custom hooks in the Copilot SDK to modify session behavior or transform tool results. Intercept lifecycle events and register handlers with your session.

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

---

**The Copilot SDK exposes a typed hook system that lets you intercept every major lifecycle event—from tool execution to session termination—by implementing handler methods and registering them with your session.**

The github/copilot-sdk repository provides a multi-language toolkit for building AI-assisted workflows. Understanding how to add custom hooks to modify session behavior or transform tool results in Copilot SDK allows you to enforce security policies, rewrite prompts, and inject contextual metadata at runtime.

## Understanding the Hook Architecture

The SDK implements a unified dispatcher pattern centered in [`rust/src/hooks.rs`](https://github.com/github/copilot-sdk/blob/main/rust/src/hooks.rs). When a lifecycle event occurs, the system deserializes the JSON payload into a `HookEvent` enum, invokes `SessionHooks::on_hook`, and validates that the returned `HookOutput` matches the requested hook type. If your handler returns `HookOutput::None` or a mismatched variant, the SDK treats the hook as unregistered and proceeds with default behavior.

You can intercept nine distinct stages:

- **preToolUse** – Intercept before a tool executes to allow/deny or modify arguments
- **preMcpToolCall** – Intercept before an MCP tool call is sent to the server
- **postToolUse** – Transform successful tool results before they reach the model
- **postToolUseFailure** – Add hidden guidance after a tool fails
- **userPromptSubmitted** – Rewrite the user’s raw message before processing
- **sessionStart** – Inject startup context when a session is created or resumed
- **sessionEnd** – Execute cleanup actions when a session terminates
- **errorOccurred** – Define retry policies or user notifications for SDK errors
- **agentStop** – Block or allow the agent’s natural stop decision

Each hook follows a strict input-output contract. For example, `preToolUse` accepts a `PreToolUseInput` struct containing the tool name, arguments, and working directory, and expects a `PreToolUseOutput` struct containing permission decisions, modified arguments, or suppression flags.

## Implementing Session Hooks in Rust

In Rust, you implement the `SessionHooks` trait from `github_copilot_sdk::hooks` using `async_trait`. Define your handlers, then attach them via `SessionConfig::with_hooks`.

```rust
use github_copilot_sdk::hooks::{
    HookOutput, PreToolUseInput, PreToolUseOutput,
    SessionStartInput, SessionStartOutput,
    SessionHooks,
};
use async_trait::async_trait;
use serde_json::json;

#[derive(Default)]
struct MyHooks;

#[async_trait]
impl SessionHooks for MyHooks {
    async fn on_pre_tool_use(
        &self,
        input: PreToolUseInput,
        _ctx: github_copilot_sdk::hooks::HookContext,
    ) -> Option<PreToolUseOutput> {
        if input.tool_name == "dangerous_tool" {
            Some(PreToolUseOutput {
                permission_decision: Some("deny".into()),
                permission_decision_reason: Some("policy block".into()),
                ..Default::default()
            })
        } else {
            None
        }
    }

    async fn on_session_start(
        &self,
        _input: SessionStartInput,
        _ctx: github_copilot_sdk::hooks::HookContext,
    ) -> Option<SessionStartOutput> {
        Some(SessionStartOutput {
            additional_context: Some(
                "You are now in a secure environment. Remember to avoid privileged commands."
                    .into(),
            ),
            ..Default::default()
        })
    }
}

```

Register the hooks when creating the session:

```rust
let config = SessionConfig::default().with_hooks(Arc::new(MyHooks));
let session = client.create_session(config).await?;

```

The trait definition and dispatcher logic reside in [`rust/src/hooks.rs`](https://github.com/github/copilot-sdk/blob/main/rust/src/hooks.rs), while session creation is handled in [`rust/src/session.rs`](https://github.com/github/copilot-sdk/blob/main/rust/src/session.rs).

## Registering Hooks in Node.js

The Node.js SDK accepts a plain JavaScript object with camelCase method names. The `registerHooks` method in [`nodejs/src/session.ts`](https://github.com/github/copilot-sdk/blob/main/nodejs/src/session.ts) serializes your handlers to the same JSON contract used by the Rust core.

```typescript
import { CopilotClient } from "copilot-sdk";

const myHooks = {
  onPreToolUse: async (input, { sessionId }) => {
    if (input.toolName === "secret_reader") {
      return { 
        permissionDecision: "deny", 
        permissionDecisionReason: "no secrets allowed" 
      };
    }
    return undefined;
  },

  onPostToolUse: async (input, { sessionId }) => {
    const note = "**NOTE:** the following information was fetched automatically.";
    return { 
      modifiedResult: { 
        value: `${note}\n${input.toolResult.value}` 
      } 
    };
  },

  onUserPromptSubmitted: async (input) => {
    const prefixed = `[Team‑Assistant] ${input.prompt}`;
    return { modifiedPrompt: prefixed };
  },
};

async function main() {
  const client = new CopilotClient({ token: "…" });
  const session = await client.createSession({ hooks: myHooks });
}

```

The SDK invokes `_handleHooksInvoke` internally to route these calls through the CLI.

## Adding Hooks in Python

Python implementations use a `TypedDict` structure defined in [`python/copilot/session.py`](https://github.com/github/copilot-sdk/blob/main/python/copilot/session.py). Pass a dictionary with snake_case keys to `CopilotClient.create_session`.

```python
from copilot import CopilotClient, SessionHooks

def deny_secret_tool(input, ctx):
    if input["toolName"] == "read_secret":
        return {
            "permissionDecision": "deny",
            "permissionDecisionReason": "access to secrets prohibited"
        }
    return None

def add_failure_guidance(input, ctx):
    return {
        "additionalContext": f"The tool `{input['toolName']}` failed because: {input['error']}. "
                            "Consider checking the file path."
    }

my_hooks: SessionHooks = {
    "on_pre_tool_use": deny_secret_tool,
    "on_post_tool_use_failure": add_failure_guidance,
    "on_session_start": lambda i, c: {
        "additionalContext": "Welcome! Your session logs are encrypted."
    },
}

client = CopilotClient(token="…")
session = client.create_session(hooks=my_hooks)

```

The Python client stores hooks in a thread-safe field and forwards invocations via `_handle_hooks_invoke`.

## Transforming System Messages with Section Callbacks

For advanced use cases requiring mutation of the system message itself, the Node.js SDK supports section transform callbacks. Register these via `registerTransformCallbacks` to dynamically inject content into specific sections like `projectSummary`.

```typescript
session.registerTransformCallbacks(new Map([
  ["projectSummary", async (content) => {
    const dynamic = await computeProjectSummary();
    return `${dynamic}\n\n${content}`;
  }],
]));

```

This pattern allows you to prepend computed metadata to the model’s context without modifying individual tool results.

## Summary

- **Hook types** cover the full session lifecycle: tool execution (pre/post), failures, prompt submission, session start/end, errors, and agent stops.
- **Rust** requires implementing the `SessionHooks` trait and attaching it via `SessionConfig::with_hooks`.
- **Node.js** accepts plain objects with camelCase handlers via `createSession({ hooks })` or `session.registerHooks()`.
- **Python** uses a `TypedDict` with snake_case keys passed to `create_session(hooks=...)`.
- **Validation** occurs in the core dispatcher at [`rust/src/hooks.rs`](https://github.com/github/copilot-sdk/blob/main/rust/src/hooks.rs); returning `None` or `null` skips the hook and continues with default behavior.
- **Section transforms** provide a separate mechanism for modifying system message content in Node.js applications.

## Frequently Asked Questions

### Can I block a tool from executing using hooks?

Yes. Implement the `preToolUse` hook (or `on_pre_tool_use` in Python) and return a `permissionDecision` of `"deny"` with an optional `permissionDecisionReason`. The SDK will halt execution before the tool runs and return the denial reason to the model.

### How do I modify the output of a failed tool call?

Use the `postToolUseFailure` hook, which receives the error message via `PostToolUseFailureInput`. Return `additionalContext` containing guidance or corrected instructions. This context is hidden from the user but visible to the model for recovery planning.

### Are hooks supported in all Copilot SDK languages?

Yes. The SDK provides hooks for Rust, Node.js, and Python, though the implementation patterns differ. Rust uses a trait-based approach, Node.js uses plain objects with camelCase methods, and Python uses `TypedDict` with snake_case keys. All languages ultimately serialize to the same JSON contract handled by the Rust core in [`rust/src/hooks.rs`](https://github.com/github/copilot-sdk/blob/main/rust/src/hooks.rs).

### What happens if my hook returns None or null?

Returning `None` in Rust, `undefined` or `null` in Node.js, or `None` in Python signals that the hook is not handling this specific invocation. The SDK proceeds with default behavior, allowing the session to continue unmodified. This is useful for conditional logic where you only intercept specific tools or error types.