注意
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" 在运行时将决策委托给用户,这对于在循环中需要人工的破坏性操作非常有用。
审核和合规性
结合 onPreToolUse、onPostToolUse 和会话生命周期挂钩,构建一个完整的审核跟踪,以记录代理所执行的每个操作。
结构化审核日志
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" }),
});
提示扩充
使用 onSessionStart 和 onUserPromptSubmitted 自动注入上下文,以便用户不必重复自己。
在会话开始时注入项目元数据
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。