跳至主要内容

使用钩子

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

谁可以使用此功能?

GitHub Copilot SDK 在所有 Copilot 计划中均可使用。

注意

Copilot SDK 目前处于公开预览阶段。功能和可用性可能会更改。

钩子让你在 Copilot 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 仓库。有关 Java 的示例,请参阅 github/copilot-sdk-java 仓库

提示

每个钩子处理函数都会收到一个包含 sessionIdinvocation 参数,可用于关联日志和维护每个会话的状态。

权限控制

使用 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 仓库。Java 示例请参阅 github/copilot-sdk-java 仓库

将文件访问限制在特定目录

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 仓库。Java 示例请参阅 github/copilot-sdk-java 仓库

从工具结果中隐藏机密

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 仓库。Java 示例请参阅 github/copilot-sdk-java 仓库

工具完成时播放声音

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 仓库。Java 示例请参阅 github/copilot-sdk-java 仓库

组合钩子

钩子可以自然组合。一个 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 仓库中的 钩子参考

© . This site is unofficial and not affiliated with GitHub, Inc.