Skip to main content

使用挂钩

使用钩子自定义 Copilot SDK 会话的行为。

谁可以使用此功能?

GitHub Copilot SDK 适用于所有 Copilot 计划。

注意

          Copilot SDK 当前处于 技术预览版. 功能和可用性可能会发生更改。

挂钩使你能够将自定义逻辑插入会话的每个阶段 Copilot SDK (从启动的那一刻起,到用户提示和工具调用)到它结束的那一刻。 可以使用挂钩来实现权限、审核、通知等,而无需修改核心代理行为。

概述

挂钩是在创建会话时注册一次的回调。 SDK 在会话生命周期中定义完善的点调用它,传递上下文输入,并选择性地接受修改会话行为的输出。 有关会话流的详细序列图,请参阅 github/copilot-sdk 存储库

挂钩当它触发时你能做什么
onSessionStart会话开始(新建会话或恢复会话)注入上下文,加载首选项
onUserPromptSubmitted用户发送消息重写提示,添加上下文,筛选输入
onPreToolUse在工具执行之前允许、拒绝或修改调用
onPostToolUse工具返回后转换结果,屏蔽机密,审核
onSessionEnd会话结束清理、记录度量指标
onErrorOccurred错误被引发自定义日志记录、重试逻辑、警报

所有挂钩都是可选的,您只需注册所需要的挂钩。 从任何挂钩中返回 null(或该语言的等效表达)会告知 SDK 继续执行默认行为。

注册钩子

创建或恢复会话时传递一个hooks对象。 下面的每个示例都遵循此模式。

import { CopilotClient } from "@github/copilot-sdk";

const client = new CopilotClient();
await client.start();

const session = await client.createSession({
    hooks: {
        onSessionStart: async (input, invocation) => { /* ... */ },
        onPreToolUse:   async (input, invocation) => { /* ... */ },
        onPostToolUse:  async (input, invocation) => { /* ... */ },
        // ... add only the hooks you need
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

有关 Python、Go 和 .NET 中的示例,请参阅 github/copilot-sdk 存储库

提示

每个挂钩处理程序接收一个 invocation 参数,该 sessionId参数可用于关联日志和维护每会话状态。

权限控制

用于 onPreToolUse 生成一个权限层,该层决定代理可以运行哪些工具、允许哪些参数,以及是否应在执行前提示用户。

将一组安全的工具列入允许列表

const READ_ONLY_TOOLS = ["read_file", "glob", "grep", "view"];

const session = await client.createSession({
    hooks: {
        onPreToolUse: async (input) => {
            if (!READ_ONLY_TOOLS.includes(input.toolName)) {
                return {
                    permissionDecision: "deny",
                    permissionDecisionReason:
                        `Only read-only tools are allowed. "${input.toolName}" was blocked.`,
                };
            }
            return { permissionDecision: "allow" };
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

有关 Python、Go 和 .NET 中的示例,请参阅 github/copilot-sdk 存储库

限制对特定目录的文件访问

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

const session = await client.createSession({
    hooks: {
        onPreToolUse: async (input) => {
            if (["read_file", "write_file", "edit"].includes(input.toolName)) {
                const filePath = (input.toolArgs as { path: string }).path;
                const allowed = ALLOWED_DIRS.some((dir) => filePath.startsWith(dir));

                if (!allowed) {
                    return {
                        permissionDecision: "deny",
                        permissionDecisionReason:
                            `Access to "${filePath}" is outside the allowed directories.`,
                    };
                }
            }
            return { permissionDecision: "allow" };
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

在破坏性操作之前询问用户

const DESTRUCTIVE_TOOLS = ["delete_file", "shell", "bash"];

const session = await client.createSession({
    hooks: {
        onPreToolUse: async (input) => {
            if (DESTRUCTIVE_TOOLS.includes(input.toolName)) {
                return { permissionDecision: "ask" };
            }
            return { permissionDecision: "allow" };
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

返回 "ask" 在运行时将决策委托给用户,这对于在循环中需要人工的破坏性操作非常有用。

审核和合规性

结合 onPreToolUseonPostToolUse 和会话生命周期挂钩,构建一个完整的审核跟踪,以记录代理所执行的每个操作。

结构化审核日志

interface AuditEntry {
    timestamp: number;
    sessionId: string;
    event: string;
    toolName?: string;
    toolArgs?: unknown;
    toolResult?: unknown;
    prompt?: string;
}

const auditLog: AuditEntry[] = [];

const session = await client.createSession({
    hooks: {
        onSessionStart: async (input, invocation) => {
            auditLog.push({
                timestamp: input.timestamp,
                sessionId: invocation.sessionId,
                event: "session_start",
            });
            return null;
        },
        onUserPromptSubmitted: async (input, invocation) => {
            auditLog.push({
                timestamp: input.timestamp,
                sessionId: invocation.sessionId,
                event: "user_prompt",
                prompt: input.prompt,
            });
            return null;
        },
        onPreToolUse: async (input, invocation) => {
            auditLog.push({
                timestamp: input.timestamp,
                sessionId: invocation.sessionId,
                event: "tool_call",
                toolName: input.toolName,
                toolArgs: input.toolArgs,
            });
            return { permissionDecision: "allow" };
        },
        onPostToolUse: async (input, invocation) => {
            auditLog.push({
                timestamp: input.timestamp,
                sessionId: invocation.sessionId,
                event: "tool_result",
                toolName: input.toolName,
                toolResult: input.toolResult,
            });
            return null;
        },
        onSessionEnd: async (input, invocation) => {
            auditLog.push({
                timestamp: input.timestamp,
                sessionId: invocation.sessionId,
                event: "session_end",
            });

            // Persist the log — swap this with your own storage backend
            await fs.promises.writeFile(
                `audit-${invocation.sessionId}.json`,
                JSON.stringify(auditLog, null, 2),
            );
            return null;
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

有关 Python 中的示例,请参阅 github/copilot-sdk 存储库

从工具的结果中删除敏感信息

const SECRET_PATTERNS = [
    /(?:api[_-]?key|token|secret|password)\s*[:=]\s*["']?[\w\-\.]+["']?/gi,
];

const session = await client.createSession({
    hooks: {
        onPostToolUse: async (input) => {
            if (typeof input.toolResult !== "string") return null;

            let redacted = input.toolResult;
            for (const pattern of SECRET_PATTERNS) {
                redacted = redacted.replace(pattern, "[REDACTED]");
            }

            return redacted !== input.toolResult
                ? { modifiedResult: redacted }
                : null;
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

通知

挂钩在应用程序的进程中触发,因此可以触发任何副作用,例如桌面通知、声音、Slack 消息或 Webhook 调用。

会话事件的桌面通知

import notifier from "node-notifier"; // npm install node-notifier

const session = await client.createSession({
    hooks: {
        onSessionEnd: async (input, invocation) => {
            notifier.notify({
                title: "Copilot Session Complete",
                message: `Session ${invocation.sessionId.slice(0, 8)} finished (${input.reason}).`,
            });
            return null;
        },
        onErrorOccurred: async (input) => {
            notifier.notify({
                title: "Copilot Error",
                message: input.error.slice(0, 200),
            });
            return null;
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

有关 Python 中的示例,请参阅 github/copilot-sdk 存储库

工具完成后播放声音

import { exec } from "node:child_process";

const session = await client.createSession({
    hooks: {
        onPostToolUse: async (input) => {
            // macOS: play a system sound after every tool call
            exec("afplay /System/Library/Sounds/Pop.aiff");
            return null;
        },
        onErrorOccurred: async () => {
            exec("afplay /System/Library/Sounds/Basso.aiff");
            return null;
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

出现错误时发布到 Slack

const SLACK_WEBHOOK_URL = process.env.SLACK_WEBHOOK_URL!;

const session = await client.createSession({
    hooks: {
        onErrorOccurred: async (input, invocation) => {
            if (!input.recoverable) {
                await fetch(SLACK_WEBHOOK_URL, {
                    method: "POST",
                    headers: { "Content-Type": "application/json" },
                    body: JSON.stringify({
                        text: `🚨 Unrecoverable error in session \`${invocation.sessionId.slice(0, 8)}\`:\n\`\`\`${input.error}\`\`\``,
                    }),
                });
            }
            return null;
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

提示扩充

使用 onSessionStartonUserPromptSubmitted 自动注入上下文,以便用户不必重复自己。

在会话开始时注入项目元数据

import * as fs from "node:fs";

const session = await client.createSession({
    hooks: {
        onSessionStart: async (input) => {
            const pkg = JSON.parse(
                await fs.promises.readFile("package.json", "utf-8"),
            );
            return {
                additionalContext: [
                    `Project: ${pkg.name} v${pkg.version}`,
                    `Node: ${process.version}`,
                    `CWD: ${input.cwd}`,
                ].join("\n"),
            };
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

在提示中展开简短命令

const SHORTCUTS: Record<string, string> = {
    "/fix":      "Find and fix all errors in the current file",
    "/test":     "Write comprehensive unit tests for this code",
    "/explain":  "Explain this code in detail",
    "/refactor": "Refactor this code to improve readability",
};

const session = await client.createSession({
    hooks: {
        onUserPromptSubmitted: async (input) => {
            for (const [shortcut, expansion] of Object.entries(SHORTCUTS)) {
                if (input.prompt.startsWith(shortcut)) {
                    const rest = input.prompt.slice(shortcut.length).trim();
                    return { modifiedPrompt: rest ? `${expansion}: ${rest}` : expansion };
                }
            }
            return null;
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

错误处理和恢复

挂钩 onErrorOccurred 使你有机会对失败做出反应,无论是意味着重试、通知人工还是正常关闭。

重试暂时性模型错误

const session = await client.createSession({
    hooks: {
        onErrorOccurred: async (input) => {
            if (input.errorContext === "model_call" && input.recoverable) {
                return {
                    errorHandling: "retry",
                    retryCount: 3,
                    userNotification: "Temporary model issue—retrying…",
                };
            }
            return null;
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

友好的错误消息

const FRIENDLY_MESSAGES: Record<string, string> = {
    model_call:      "The AI model is temporarily unavailable. Please try again.",
    tool_execution:  "A tool encountered an error. Check inputs and try again.",
    system:          "A system error occurred. Please try again later.",
};

const session = await client.createSession({
    hooks: {
        onErrorOccurred: async (input) => {
            return {
                userNotification: FRIENDLY_MESSAGES[input.errorContext] ?? input.error,
            };
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

会话指标

跟踪会话的运行时间、调用的工具数以及会话结束的原因(对于仪表板和成本监视非常有用)。

const metrics = new Map<string, { start: number; toolCalls: number; prompts: number }>();

const session = await client.createSession({
    hooks: {
        onSessionStart: async (input, invocation) => {
            metrics.set(invocation.sessionId, {
                start: input.timestamp,
                toolCalls: 0,
                prompts: 0,
            });
            return null;
        },
        onUserPromptSubmitted: async (_input, invocation) => {
            metrics.get(invocation.sessionId)!.prompts++;
            return null;
        },
        onPreToolUse: async (_input, invocation) => {
            metrics.get(invocation.sessionId)!.toolCalls++;
            return { permissionDecision: "allow" };
        },
        onSessionEnd: async (input, invocation) => {
            const m = metrics.get(invocation.sessionId)!;
            const durationSec = (input.timestamp - m.start) / 1000;

            console.log(
                `Session ${invocation.sessionId.slice(0, 8)}: ` +
                `${durationSec.toFixed(1)}s, ${m.prompts} prompts, ` +
                `${m.toolCalls} tool calls, ended: ${input.reason}`,
            );

            metrics.delete(invocation.sessionId);
            return null;
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

有关 Python 中的示例,请参阅 github/copilot-sdk 存储库

组合挂钩

钩子自然地组合。 单个 hooks 对象可以处理权限、审核和通知 - 每个挂钩都执行自己的作业。

const session = await client.createSession({
    hooks: {
        onSessionStart: async (input) => {
            console.log(`[audit] session started in ${input.cwd}`);
            return { additionalContext: "Project uses TypeScript and Vitest." };
        },
        onPreToolUse: async (input) => {
            console.log(`[audit] tool requested: ${input.toolName}`);
            if (input.toolName === "shell") {
                return { permissionDecision: "ask" };
            }
            return { permissionDecision: "allow" };
        },
        onPostToolUse: async (input) => {
            console.log(`[audit] tool completed: ${input.toolName}`);
            return null;
        },
        onErrorOccurred: async (input) => {
            console.error(`[alert] ${input.errorContext}: ${input.error}`);
            return null;
        },
        onSessionEnd: async (input, invocation) => {
            console.log(`[audit] session ${invocation.sessionId.slice(0, 8)} ended: ${input.reason}`);
            return null;
        },
    },
    onPermissionRequest: async () => ({ kind: "approved" }),
});

最佳做法

  •         **保持挂钩紧固。** 每个钩子都内联运行 — 缓慢的钩子会延迟对话。 尽可能将大量工作(数据库写入、HTTP 调用)卸载到后台队列。
    
  •         **在没有任何更改时返回 `null` 。** 这会告知 SDK 继续执行默认值,并避免不必要的对象分配。
    
  •         **明确权限决策。** 返回 `{ permissionDecision: "allow" }` 比返回 `null` 更明了,尽管两者都可以使用该工具。
    
  •         **不要忽略关键错误。** 可以抑制可恢复的工具错误,但始终记录或针对无法恢复的工具错误发出警报。
    
  •         **尽可能使用 `additionalContext` 而不是 `modifiedPrompt`。** 追加上下文会保留用户的原始意向,同时仍指导模型。
    
  •         **根据会话 ID 确定范围状态。** 如果跟踪每会话数据,请将其设为关键`invocation.sessionId`并在`onSessionEnd`中清理。
    

延伸阅读

有关详细信息,请参阅存储库中的挂钩参考github/copilot-sdk