Skip to main content
Skip to content

Trabajar con enlaces

Los enlaces permiten conectar lógica personalizada en cada fase de una sesión de Copilot, desde el momento en que se inicia, a través de cada solicitud de usuario y llamada a herramienta, hasta el momento en que finaliza. En esta guía se describen casos de uso prácticos para que pueda enviar permisos, auditorías, notificaciones y mucho más sin modificar el comportamiento del agente principal.

Visión general

Un gancho es un callback que se registra una vez al crear una sesión. El SDK lo invoca en un punto bien definido en el ciclo de vida de la conversación, pasa la entrada contextual y, opcionalmente, acepta la salida que modifica el comportamiento de la sesión.

Diagrama: Diagrama de flujo que muestra el proceso descrito.

EnlaceCuando se activaQué puede hacer
Enlaces de ciclo de vida de sesiónComienza la sesión (nueva o reanudada)Insertar contexto, preferencias de carga
Enlace enviado de la solicitud de usuarioEl usuario envía un mensajeReescribir avisos, añadir contexto, filtrar entrada
Gancho de transformación de la solicitud al usuarioEl entorno de ejecución construye el prompt del modelo.Inspeccionar o sustituir contenido orientado al modelo
Gancho previo al uso de la herramientaAntes de que se ejecute una herramientaPermitir, denegar o modificar la llamada
Enlace de uso posterior a la herramientaDespués de que una herramienta devuelva el resultado (solo en caso de éxito)Transformación de resultados, redacción de secretos, auditoría
Enlace de uso posterior a la herramientaDespués de que una herramienta devuelva un errorInsertar directrices de reintento, registrar fallos
Enlaces de ciclo de vida de sesiónFinaliza la sesiónLimpieza y registro de métricas
Gancho de manejo de erroresSe genera un errorRegistro personalizado, lógica de reintento, alertas

Todos los Enlaces son opcionales: registre solo los que necesite. Al retornar null (o el equivalente en el idioma) desde cualquier enlace, se indica al SDK que continúe con el comportamiento predeterminado.

Los Enlaces de repositorio basados en archivos y la configuración del hook de repositorio solo se cargan después de que el espacio de trabajo se considere de confianza. Los Enlaces de usuario permanecen disponibles antes de que se conceda la confianza del espacio de trabajo.

Ganchos de repositorio de directorios adicionales

Los Enlaces del sistema de archivos del repositorio son independientes de los callbacks del SDK descritos en esta guía. Para cargarlos desde directorios adicionales durante la creación o reanudación en frío de la sesión local, proporcione additionalDirectories; La CLI --add-dir también participa de forma predeterminada.

DISABLE_ADDITIONAL_DIRECTORY_HOOKS=true restaura el ámbito solo principal; enableFileHooks puede habilitar o deshabilitar Enlaces del repositorio a través de session.rpc.options.update; los directorios de hooks de SDK explícitos y los callbacks son independientes.

La detección usa la raíz de Git que contiene cada directorio aceptado, o el propio directorio cuando está fuera de un repositorio de Git. Por ejemplo, proporcionar B\packages\core dentro del repositorio B admite B\.github\hooks y la configuración de los Enlaces del repositorio de B, no solo la configuración dentro de B\packages\core.

Advertencia

Proporcione solo directorios de arranque de confianza. Esto autoriza comandos hook y prompts proporcionados por Enlaces de todos los directorios de inicio elegibles sin aprobación adicional por directorio o por hook. Las concesiones de acceso a archivos posteriores no autorizan Enlaces. La directiva administrada existente, los controles de deshabilitación de hook y las reglas de espacio aislado todavía se aplican; no hacen que el contenido de hook malintencionado sea seguro.

Las concesiones de directorio posteriores no agregan orígenes de gancho. Reanudar en frío debe volver a proporcionar los directorios; las concesiones posteriores o las actualizaciones de vía de escape no pueden cambiar la autorización conservada.

Registro de enlaces

El callback hooks permanece separado de los hookDirectories basados en archivos. Para conocer las configuraciones de Enlaces empaquetadas y los recursos adyacentes, consulte Ubicaciones para detectar personalizaciones.

Enlaces declarativos con ámbito de agente

Los agentes personalizados pueden declarar un hooks mapa de eventos en .agent.md frontmatter o en la configuración del agente personalizado del SDK. Estos son comandos declarativos o acciones HTTP, no funciones de devolución de llamada del SDK. Solo se ejecutan mientras se selecciona ese agente, además de los Enlaces de sesión, usuario, repositorio, complemento y directiva administrada aplicables. Cambiar o borrar el agente seleccionado cambia los Enlaces con alcance para la siguiente invocación sin cambiar otras sesiones ni subagentes simultáneos.

---
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.

Se aceptan nombres de eventos en tiempo de ejecución, como postToolUse y alias de VS Code admitidos, como PostToolUse . command proporciona el comando de shell de reserva; windows, linux y osx lo sobrescriben en la plataforma correspondiente. Los comandos hook son relativos a la raíz del repositorio de la sesión en ejecución, a menos que establezcan un directorio de trabajo explícito. Las cargas útiles de los Enlaces y las decisiones siguen los contratos existentes de los Enlaces del entorno de ejecución.

Cuando un agente local se ejecuta como un subagente, sus acciones de ámbito Stop (agentStop) se ejecutan en SubagentStop en lugar de una parada de turno normal. Una acción explícita SubagentStop en ese agente sigue observando sus propios hijos. Detenga las devoluciones de llamada stop_hook_active: true después de un bloqueo previo. Después de ocho continuaciones de Stop con ámbito consecutivo, el tiempo de ejecución omite más bloques con ámbito y acepta la respuesta final del elemento secundario a menos que un hook del ciclo de vida primario siga bloqueando la finalización.

Los Enlaces de repositorio y de agente heredado requieren la autorización del espacio de trabajo. La detección o selección del agente no otorga confianza. La configuración existente de desactivación de Enlaces, las restricciones empresariales, los permisos de espacio aislado y los tiempos de espera también se aplican a las acciones con ámbito. Los agentes proporcionados por el SDK no pueden tener privilegios de complemento administrado. Las definiciones de agente hospedados en la organización o remota no ejecutan Enlaces con ámbito y el envío Stop con ámbito es compatible con subagentes locales en tiempo de ejecución, no para participantes de AHP externos.

Los nombres de eventos no admitidos se notifican antes de la selección de eventos. Los errores de preparación de acciones afectan solo a su evento declarado; los Enlaces compartidos y de políticas administradas se siguen ejecutando.

Pase un hooks objeto al crear (o reanudar) una sesión. Cada ejemplo siguiente sigue este patrón.

Lenguajes de código navigation

TypeScript
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" }),
});

Sugerencia

Cada controlador de enlace recibe un invocation parámetro que contiene el sessionId, lo cual resulta útil para correlacionar registros y mantener el estado por sesión.

Caso de uso: control de permisos

Use onPreToolUse para crear una capa de permisos que decida qué herramientas puede ejecutar el agente, qué argumentos se permiten y si se debe solicitar al usuario antes de la ejecución.

Permitir una lista segura de herramientas

Lenguajes de código navigation

TypeScript
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" }),
});

Restricción del acceso a archivos a directorios específicos

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" }),
});

Preguntar al usuario antes de las operaciones destructivas

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" }),
});

Al devolver "ask", se delega la decisión al usuario en tiempo de ejecución, lo cual resulta útil para acciones destructivas en las que se desea la intervención directa del usuario.

Caso de uso: auditoría y cumplimiento

Combine onPreToolUse, onPostToolUsey los enlaces del ciclo de vida de la sesión para crear una pista de auditoría completa que registre todas las acciones que realiza el agente.

Registro de auditoría estructurado

Lenguajes de código navigation

TypeScript
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" }),
});

Redactar secretos en los resultados de la herramienta

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" }),
});

Caso de uso: notificaciones y sonidos

Los enlaces se activan en el proceso de su aplicación para que pueda desencadenar cualquier efecto secundario, como notificaciones de escritorio, sonidos, mensajes de Slack o llamadas de webhook.

Notificación de escritorio en eventos de sesión

Lenguajes de código navigation

TypeScript
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" }),
});

Reproducir un sonido cuando una herramienta finaliza

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" }),
});

Enviar a Slack sobre errores

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" }),
});

Caso de uso: enriquecimiento de mensajes

Use onSessionStart y onUserPromptSubmitted para insertar automáticamente el contexto para que los usuarios no tengan que repetirse.

Inserción de metadatos del proyecto en el inicio de sesión

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" }),
});

Expansión de comandos abreviados en mensajes

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" }),
});

Caso de uso: control de errores y recuperación

El onErrorOccurred enlace le da la oportunidad de reaccionar a los errores, ya sea que eso significa reintentar, notificar a un humano o cerrar correctamente.

Reintentar errores de modelo transitorio

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" }),
});

Mensajes de error amigables

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" }),
});

Caso de uso: métricas de sesión

Realice un seguimiento de cuánto tiempo las sesiones se ejecutan, cuántas herramientas se invocan y por qué las sesiones finalizan, lo cual es útil para paneles de control y monitoreo de costos.

Lenguajes de código navigation

TypeScript
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" }),
});

Combinación de enlaces

Los hooks se integran de manera natural. Un único objeto hooks puede gestionar permisos y auditoría y notificaciones: cada hook cumple su propia función.

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" }),
});

procedimientos recomendados

  1. Mantenga los enlaces rápidos. Cada enlace se ejecuta en línea: los enlaces lentos retrasan la conversación. Descargue trabajos pesados (escrituras en bases de datos, llamadas HTTP) a una cola en segundo plano cuando sea posible.

  2. Vuelva null cuando no tenga nada que cambiar. Esto indica al SDK que continúe con los valores predeterminados y evite la asignación de objetos innecesaria.

  3. Sea explícito con las decisiones de permisos. Devolver { permissionDecision: "allow" } es más claro que devolver null, aunque ambos permiten la herramienta.

  4. No trague errores críticos. Está bien suprimir los errores de herramientas recuperables, pero siempre registre o alerte sobre los que no son recuperables.

  5. Use additionalContext en lugar de modifiedPrompt cuando sea posible. Agregar contexto conserva la intención original del usuario mientras sigue guiando el modelo.

  6. Estado de ámbito por identificador de sesión. Si realiza un seguimiento de los datos por sesión, úselo como clave invocation.sessionId y limpie durante onSessionEnd.

Reference

Para obtener definiciones de tipo completas, tablas de campos de entrada y salida y ejemplos adicionales para cada enlace, consulte la referencia de API:

Consulte también