Skip to main content

フックを使った作業

フックを使用すると、Copilot セッションの開始時点から、各ユーザー プロンプトとツール呼び出しを通じて、終了した時点まで、カスタム ロジックをすべてのステージに接続できます。 このガイドでは、主要なエージェントの動作を変更せずにアクセス許可、監査、通知などを送信できるように、実際のユース ケースについて説明します。

Overview

フックは、セッションの作成時に 1 回登録するコールバックです。 SDK は、会話ライフサイクルの明確に定義されたポイントで呼び出し、コンテキスト入力を渡し、必要に応じてセッションの動作を変更する出力を受け入れます。

図: 説明されたプロセスを示すフローチャート。

フック起動時実行可能事項
セッションライフサイクルフックセッションが開始 (新規または再開)コンテキストの挿入、ユーザー設定の読み込み
ユーザー プロンプト送信後フックユーザーがメッセージを送信するプロンプトの書き換え、コンテキストの追加、入力のフィルター処理
ツール使用前のフックツールの実行前呼び出しを許可/拒否/変更する
ツール使用後フックツールが結果を返した後 (成功のみ)結果の変換、シークレットの編集、監査
ツール使用後フックツールがエラーを返した後再試行のガイダンスを挿入し、エラーをログに記録する
セッションライフサイクルフックセッションの終了クリーンアップを行い、指標を記録する
エラー処理フックエラーが発生しましたカスタム ログ、再試行ロジック、アラート

すべてのフックは 省略可能です。必要なものだけを登録します。 任意のフックから null (または同等の言語) を返した場合、SDK は既定の動作を続行するように指示します。

フックの登録

セッションを作成 (または再開) するときに、 hooks オブジェクトを渡します。 次の例はすべて、このパターンに従います。

コード言語 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" }),
});

ヒント

すべてのフック ハンドラーは、invocationを含むsessionId パラメーターを受け取ります。これは、ログの関連付けとセッションごとの状態の維持に役立ちます。

ユース ケース: アクセス許可の制御

onPreToolUseを使用して、エージェントが実行できるツール、許可される引数、実行前にユーザーにプロンプトを表示するかどうかを決定するアクセス許可レイヤーを構築します。

安全な一連のツールを許可リストする

コード言語 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" }),
});

特定のディレクトリへのファイル アクセスを制限する

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"を返す場合、実行時にユーザーに決定が委任されます。ループ内で人間が必要な破壊的なアクションに役立ちます。

ユース ケース: 監査とコンプライアンス

onPreToolUseonPostToolUse、およびセッション ライフサイクル フックを組み合わせて、エージェントが実行するすべてのアクションを記録する完全な監査証跡を構築します。

構造化監査ログ

コード言語 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" }),
});

ツールの結果からシークレットを編集する

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 メッセージ、Webhook 呼び出しなどの副作用をトリガーできます。

セッション イベントに関するデスクトップ通知

コード言語 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" }),
});

ツールの終了時にサウンドを再生する

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

ユース ケース: プロンプト エンリッチメント

ユーザーが自分自身を繰り返す必要がないように、 onSessionStartonUserPromptSubmitted を使用してコンテキストを自動的に挿入します。

セッション開始時にプロジェクト メタデータを挿入する

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

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

フックの組み合わせ

フックは自然に構成されます。 1 つの 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" }),
});

ベスト プラクティス

  1. フックをしっかりと固定してください。 すべてのフックはインラインで実行されます。低速フックは会話を遅らせることができます。 可能な場合は、大量の作業 (データベースの書き込み、HTTP 呼び出し) をバックグラウンド キューにオフロードします。

  2. 何も変更しない場合は、 null を返します。 これにより、SDK に既定値を続行するように指示し、不要なオブジェクトの割り当てを回避します。

  3. アクセス許可の決定を明示的に行う。 { permissionDecision: "allow" }を返す方が、両方でツールが許可されている場合でも、nullを返すよりも明確です。

  4. 重大なエラーを飲み込まないでください。 回復可能なツール エラーを抑制しても問題ありませんが、回復不可能なエラーに対して常にログまたはアラートを記録します。

  5. 可能な場合は、additionalContextの代わりにmodifiedPromptを使用します。 コンテキストを追加すると、モデルを引き続きガイドしながら、ユーザーの元の意図が保持されます。

  6. セッション ID で状態をスコープします。 セッションごとのデータを追跡する場合は、 invocation.sessionId にキーを設定し、 onSessionEndでクリーンアップします。

Reference

すべてのフックの完全な型定義、入力/出力フィールド テーブル、およびその他の例については、API リファレンスを参照してください。

こちらも参照ください