Обзор
Крюк — это обратный вызов, который вы регистрируете один раз при создании сессии. SDK вызывает его в определённой точке жизненного цикла разговора, передаёт контекстный ввод и, по желанию, принимает выводы, изменяющие поведение сессии.

| Хук | Когда он срабатывает | Что можно сделать |
|---|---|---|
| Крючки жизненного цикла сессии | Сессия начинается (новая или возобновлённая) | Инжекция контекста, настройки загрузки |
| Хук, присланный пользовательским запросом | Пользователь отправляет сообщение | Переписывайте подсказки, добавляйте контекст, фильтруйте вводные данные |
| Запрос пользователя, преобразованный перехватчик | Среда выполнения создает запрос модели | Проверка или замена содержимого для модели |
| Крючок для использования до использования инструмента | До запуска инструмента | Разрешить / отклонить / изменить вызов |
| Крючок после использования инструмента | После возвращения инструмента (только успех) | Трансформировать результаты, редактировать секреты, аудитировать |
| Крючок после использования инструмента | После того как инструмент возвращает отказ | Инжекция повторных инструкций, сбои журнала |
| Крючки жизненного цикла сессии | Сессия завершена | Очистить, зафиксировать метрики |
| Крюк для обработки ошибок | Возникает ошибка | Пользовательский логинг, логика повторных попыток, оповещения |
Все крючки необязательны — регистрируйте только те, которые вам нужны. Возврат null (или языковой эквивалент) с любого хука говорит SDK продолжать поведение по умолчанию.
Перехватчики репозитория на основе файлов и параметры перехватчика репозитория загружаются только после того, как рабочая область является доверенной. Перехватчики пользователей остаются доступными до предоставления доверия рабочей области.
Перехватчики репозитория из дополнительных каталогов
Перехватчики файловой системы репозитория отделены от обратных вызовов пакета SDK, описанных в этом руководстве. Чтобы загрузить их из дополнительных каталогов во время создания локального сеанса или холодного возобновления, укажите additionalDirectories; Интерфейс командной строки --add-dir также участвует по умолчанию.
DISABLE_ADDITIONAL_DIRECTORY_HOOKS=true восстанавливает основную область; enableFileHooks может включать или отключать перехватчики репозитория через session.rpc.options.update; явные каталоги перехватчиков пакета SDK и обратные вызовы являются независимыми.
Обнаружение использует каждый принятый каталог, содержащий корневой каталог Git, или сам каталог, если он находится за пределами репозитория Git. Например, предоставление внутри репозитория B\packages\core``B допускает и B\.github\hooks параметры перехватчика репозитория B, а не только конфигурацию внутри B\packages\core.
Предупреждение
Укажите только доверенные каталоги запуска. Это разрешает команды перехватчиков и запросы, предоставляемые перехватчиком, из всех подходящих каталогов запуска без дополнительного утверждения для каждого каталога или на перехватчик. Более поздние разрешения на доступ к файлам не разрешают перехватчики. Существующая управляемая политика, элементы управления перехватчика и правила песочницы по-прежнему применяются; они не делают содержимое вредоносных перехватчиков безопасным.
Позже предоставление каталогов не добавляет источники перехватчиков. Холодное возобновление должно перезапустить каталоги; более поздние разрешения или обновления escape-хэтча не могут изменить сохраненную авторизацию.
Регистрационные крючки
Обратный вызов hooks остается отдельным от файлового резервного копирования hookDirectories. Сведения о конфигурации упаковаемого перехватчика и смежных ресурсах см. в разделе Расположения обнаружения настроек.
Декларативные перехватчики с областью действия агента
Пользовательские агенты могут объявлять hooks карту событий в .agent.md frontmatter или в конфигурации пользовательского агента пакета SDK. Это декларативные команды или действия HTTP, а не функции обратного вызова ПАКЕТА SDK. Они выполняются только во время выбора этого агента, помимо применимых сеансов, пользователей, репозитория, подключаемого модуля и перехватчиков управляемой политики. Изменение или очистка выбранного агента изменяет области перехватчиков для следующего вызова без изменения других сеансов или параллельных вложенных элементов.
---
name: formatter
description: Formats changed files
hooks:
PostToolUse:
- type: command
command: ./scripts/format-changed-files.sh
windows: ./scripts/format-changed-files.ps1
---
Format the files you change.
Такие имена событий среды выполнения, как и поддерживаемые псевдонимы VS Code, такие как postToolUse``PostToolUse приняты.
command предоставляет команду резервной оболочки; windows, linuxи osx переопределите его на соответствующей платформе. Команды перехватчика относятся к корневому каталогу репозитория исполняемого сеанса, если только они не задают явный рабочий каталог. Полезные данные и решения перехватчика следуют существующим контрактам крюка среды выполнения.
Если локальный агент запускается как подагент, его действия с областью Stop действияagentStop выполняются SubagentStop вместо обычной остановки поворота. Явное SubagentStop действие для этого агента по-прежнему наблюдает собственные дочерние элементы. Остановите обратные вызовы stop_hook_active: true после предыдущего блока. После восьми последовательных продолжений остановки среда выполнения игнорирует дополнительные блоки с областью действия и принимает окончательный ответ дочернего, если родительский обработчик жизненного цикла по-прежнему не блокирует завершение.
Для репозитория и перехватчиков наследуемых агентов требуется прием рабочих областей. Обнаружение или выбор агента не предоставляет доверия. Существующие параметры отключения перехватчика, ограничения предприятия, разрешения песочницы и время ожидания также применяются к действиям с областью действия. Агенты, предоставляемые пакетом SDK, не могут претендовать на привилегии управляемого подключаемого модуля. Определения удаленных или размещенных в организации агентов не выполняют перехватчики с областью действия, а для локальных субагентов среды выполнения поддерживается не внешние участники AHP.
Неподдерживаемые имена событий передаются до выбора события. Ошибки подготовки действий влияют только на объявленное событие; Перехватчики общих и управляемых политик по-прежнему выполняются.
Передайте объект hooks при создании (или возобновлении) сессии. Каждый приведённый пример следует этой схеме.
Языки кода navigation
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: "approve-once" }),
});
from copilot import CopilotClient, PermissionDecisionApproveOnce
client = CopilotClient()
await client.start()
session = await client.create_session(
on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(),
hooks={
"on_session_start": on_session_start,
"on_pre_tool_use": on_pre_tool_use,
"on_post_tool_use": on_post_tool_use,
# ... add only the hooks you need
},
)
client := copilot.NewClient(nil)
session, err := client.CreateSession(ctx, &copilot.SessionConfig{
Hooks: &copilot.SessionHooks{
OnSessionStart: onSessionStart,
OnPreToolUse: onPreToolUse,
OnPostToolUse: onPostToolUse,
// ... add only the hooks you need
},
OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) {
return &rpc.PermissionDecisionApproveOnce{}, nil
},
})
var client = new CopilotClient();
var session = await client.CreateSessionAsync(new SessionConfig
{
Hooks = new SessionHooks
{
OnSessionStart = onSessionStart,
OnPreToolUse = onPreToolUse,
OnPostToolUse = onPostToolUse,
// ... add only the hooks you need
},
OnPermissionRequest = (req, inv) =>
Task.FromResult(PermissionDecision.ApproveOnce()),
});
import com.github.copilot.CopilotClient;
import com.github.copilot.rpc.*;
import java.util.concurrent.CompletableFuture;
try (var client = new CopilotClient()) {
client.start().get();
var hooks = new SessionHooks()
.setOnSessionStart((input, inv) -> CompletableFuture.completedFuture(null))
.setOnPreToolUse((input, inv) -> CompletableFuture.completedFuture(null))
.setOnPostToolUse((input, inv) -> CompletableFuture.completedFuture(null));
// ... add only the hooks you need
var session = client.createSession(
new SessionConfig()
.setHooks(hooks)
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
).get();
}
Совет
Каждый обработчик хука получает invocation параметр, содержащий sessionId, что полезно для корреляции логов и поддержания состояния за сессию.
Сценарий использования: контроль разрешений
Используйте onPreToolUse для создания слоя разрешений, который определяет, какие инструменты агент может запускать, какие аргументы разрешены и следует ли запросить пользователя перед выполнением.
Список разрешений на безопасный набор инструментов
Языки кода navigation
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: "approve-once" }),
});
from copilot import PermissionDecisionApproveOnce
READ_ONLY_TOOLS = ["read_file", "glob", "grep", "view"]
async def on_pre_tool_use(input_data, invocation):
if input_data["toolName"] not in READ_ONLY_TOOLS:
return {
"permissionDecision": "deny",
"permissionDecisionReason":
f'Only read-only tools are allowed. "{input_data["toolName"]}" was blocked.',
}
return {"permissionDecision": "allow"}
session = await client.create_session(
on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(),
hooks={"on_pre_tool_use": on_pre_tool_use},
)
readOnlyTools := map[string]bool{"read_file": true, "glob": true, "grep": true, "view": true}
session, _ := client.CreateSession(ctx, &copilot.SessionConfig{
Hooks: &copilot.SessionHooks{
OnPreToolUse: func(input copilot.PreToolUseHookInput, inv copilot.HookInvocation) (*copilot.PreToolUseHookOutput, error) {
if !readOnlyTools[input.ToolName] {
return &copilot.PreToolUseHookOutput{
PermissionDecision: "deny",
PermissionDecisionReason: fmt.Sprintf("Only read-only tools are allowed. %q was blocked.", input.ToolName),
}, nil
}
return &copilot.PreToolUseHookOutput{PermissionDecision: "allow"}, nil
},
},
})
var readOnlyTools = new HashSet<string> { "read_file", "glob", "grep", "view" };
var session = await client.CreateSessionAsync(new SessionConfig
{
Hooks = new SessionHooks
{
OnPreToolUse = (input, invocation) =>
{
if (!readOnlyTools.Contains(input.ToolName))
{
return Task.FromResult<PreToolUseHookOutput?>(new PreToolUseHookOutput
{
PermissionDecision = "deny",
PermissionDecisionReason = $"Only read-only tools are allowed. \"{input.ToolName}\" was blocked.",
});
}
return Task.FromResult<PreToolUseHookOutput?>(
new PreToolUseHookOutput { PermissionDecision = "allow" });
},
},
});
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import com.github.copilot.rpc.PermissionHandler;
import com.github.copilot.rpc.SessionConfig;
import com.github.copilot.rpc.SessionHooks;
import com.github.copilot.rpc.PreToolUseHookOutput;
var readOnlyTools = Set.of("read_file", "glob", "grep", "view");
var hooks = new SessionHooks()
.setOnPreToolUse((input, invocation) -> {
if (!readOnlyTools.contains(input.getToolName())) {
return CompletableFuture.completedFuture(
PreToolUseHookOutput.deny(
"Only read-only tools are allowed. \"" + input.getToolName() + "\" was blocked.")
);
}
return CompletableFuture.completedFuture(PreToolUseHookOutput.allow());
});
var session = client.createSession(
new SessionConfig()
.setHooks(hooks)
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
).get();
Ограничить доступ к файлам определёнными каталогами
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: "approve-once" }),
});
Спросите пользователя перед деструктивными операциями
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: "approve-once" }),
});
Возврат "ask" делегирует решение пользователю во время выполнения — это полезно для разрушительных действий, когда вы хотите, чтобы человек был в цикле.
Сценарий использования: аудит и соответствие требованиям
Объедините onPreToolUse, onPostToolUse, и жизненный цикл сессии зацепляется, чтобы создать полный аудит, который фиксирует каждое действие агента.
Журнал структурированного аудита
Языки кода navigation
interface AuditEntry {
timestamp: Date;
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: "approve-once" }),
});
import json, aiofiles
from copilot import PermissionDecisionApproveOnce
audit_log = []
async def on_session_start(input_data, invocation):
audit_log.append({
"timestamp": input_data["timestamp"].isoformat(),
"session_id": invocation["session_id"],
"event": "session_start",
})
return None
async def on_user_prompt_submitted(input_data, invocation):
audit_log.append({
"timestamp": input_data["timestamp"].isoformat(),
"session_id": invocation["session_id"],
"event": "user_prompt",
"prompt": input_data["prompt"],
})
return None
async def on_pre_tool_use(input_data, invocation):
audit_log.append({
"timestamp": input_data["timestamp"].isoformat(),
"session_id": invocation["session_id"],
"event": "tool_call",
"tool_name": input_data["toolName"],
"tool_args": input_data["toolArgs"],
})
return {"permissionDecision": "allow"}
async def on_post_tool_use(input_data, invocation):
audit_log.append({
"timestamp": input_data["timestamp"].isoformat(),
"session_id": invocation["session_id"],
"event": "tool_result",
"tool_name": input_data["toolName"],
"tool_result": input_data["toolResult"],
})
return None
async def on_session_end(input_data, invocation):
audit_log.append({
"timestamp": input_data["timestamp"].isoformat(),
"session_id": invocation["session_id"],
"event": "session_end",
})
async with aiofiles.open(f"audit-{invocation['session_id']}.json", "w") as f:
await f.write(json.dumps(audit_log, indent=2))
return None
session = await client.create_session(
on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(),
hooks={
"on_session_start": on_session_start,
"on_user_prompt_submitted": on_user_prompt_submitted,
"on_pre_tool_use": on_pre_tool_use,
"on_post_tool_use": on_post_tool_use,
"on_session_end": on_session_end,
},
)
Скрыть секреты из результатов инструментов
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: "approve-once" }),
});
Сценарий использования: уведомления и звуки
Крючки срабатывают в процессе вашего приложения, так что вы можете вызвать любые побочные эффекты — уведомления на рабочем столе, звуки, сообщения в Slack или звонки через вебхуки.
Уведомления на рабочем столе о событиях сессии
Языки кода navigation
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: "approve-once" }),
});
import subprocess
from copilot import PermissionDecisionApproveOnce
async def on_session_end(input_data, invocation):
sid = invocation["session_id"][:8]
reason = input_data["reason"]
subprocess.Popen([
"notify-send", "Copilot Session Complete",
f"Session {sid} finished ({reason}).",
])
return None
async def on_error_occurred(input_data, invocation):
subprocess.Popen([
"notify-send", "Copilot Error",
input_data["error"][:200],
])
return None
session = await client.create_session(
on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(),
hooks={
"on_session_end": on_session_end,
"on_error_occurred": on_error_occurred,
},
)
Воспроизведите звук, когда инструмент заканчивается
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: "approve-once" }),
});
Опубликовать об ошибках в 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: "approve-once" }),
});
Пример использования: быстрое обогащение
Используйте onSessionStart и onUserPromptSubmitted автоматически вводите контекст, чтобы пользователям не приходилось повторяться.
Ввести метаданные проекта при начале сессии
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}`,
`Working directory: ${input.workingDirectory}`,
].join("\n"),
};
},
},
onPermissionRequest: async () => ({ kind: "approve-once" }),
});
Расширяйте команды стенографии в подсказках
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: "approve-once" }),
});
Сценарий использования: обработка ошибок и восстановление
Крючок 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: "approve-once" }),
});
Дружественные сообщения об ошибках
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: "approve-once" }),
});
Сценарий использования: метрики сессии
Отслеживайте, сколько длятся сессии, сколько инструментов запускается и почему сессии заканчиваются — это полезно для дашбордов и мониторинга затрат.
Языки кода navigation
const metrics = new Map<
string,
{ start: Date; 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.getTime() - m.start.getTime()) / 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: "approve-once" }),
});
from copilot import PermissionDecisionApproveOnce
session_metrics = {}
async def on_session_start(input_data, invocation):
session_metrics[invocation["session_id"]] = {
"start": input_data["timestamp"],
"tool_calls": 0,
"prompts": 0,
}
return None
async def on_user_prompt_submitted(input_data, invocation):
session_metrics[invocation["session_id"]]["prompts"] += 1
return None
async def on_pre_tool_use(input_data, invocation):
session_metrics[invocation["session_id"]]["tool_calls"] += 1
return {"permissionDecision": "allow"}
async def on_session_end(input_data, invocation):
m = session_metrics.pop(invocation["session_id"])
duration = (input_data["timestamp"] - m["start"]).total_seconds()
sid = invocation["session_id"][:8]
print(
f"Session {sid}: {duration:.1f}s, {m['prompts']} prompts, "
f"{m['tool_calls']} tool calls, ended: {input_data['reason']}"
)
return None
session = await client.create_session(
on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(),
hooks={
"on_session_start": on_session_start,
"on_user_prompt_submitted": on_user_prompt_submitted,
"on_pre_tool_use": on_pre_tool_use,
"on_session_end": on_session_end,
},
)
Комбинирование крюков
Хуки составляются естественно. Один hooks объект может обрабатывать права доступа, аудит и уведомления — каждый крючок выполняет свою функцию.
const session = await client.createSession({
hooks: {
onSessionStart: async (input) => {
console.log(`[audit] session started in ${input.workingDirectory}`);
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: "approve-once" }),
});
Лучшие практики
-
Держите крючки быстрыми. Каждый крючок идёт по очереди — медленные хуки задерживают разговор. По возможности переложите тяжёлую работу (запись в базу данных, HTTP-вызовы) в фоновую очередь.
-
Возвращайся
null, когда тебе нечего менять. Это указывает SDK продолжать по умолчанию и избегает ненужного распределения объектов. -
Будьте чёткие в решениях о разрешении. Возврат
{ permissionDecision: "allow" }яснее, чем возвратnull, хотя оба позволяют использовать инструмент. -
Не принимайте критические ошибки. Можно подавить ошибки восстанавливаемых инструментов, но всегда логируйте или оповещайте о невосстановимых ошибках.
-
Используйте
additionalContextвместо того,modifiedPromptчтобы иметь возможность. Добавление контекста сохраняет изначальный замысел пользователя, при этом направляя модель. -
Состояние области действия по идентификатору сессии. Если вы отслеживаете данные за сессию, включайте
invocation.sessionIdих и очищайте вonSessionEnd.
Reference
Полные определения типов, таблицы полей ввода/вывода и дополнительные примеры для каждого хука смотрите ссылку на API:
- Сессионные хуки
- Крючок для использования до использования инструмента
- Крючок после использования инструмента
- Хук, присланный пользовательским запросом
- Запрос пользователя, преобразованный перехватчик
- Крючки жизненного цикла сессии
- Крюк для обработки ошибок