Skip to main content

フックを使った作業

フックを使用して、 Copilot SDK セッションの動作をカスタマイズします。

この機能を使用できるユーザーについて

GitHub Copilot SDK は、すべての Copilot プランで使用できます。

メモ

          Copilot SDK は現在 テクニカル プレビューです。 機能と可用性は変更される場合があります。

フックを使用すると、 Copilot SDK セッションの開始時点から、各ユーザー プロンプトとツール呼び出しを通じて、終了した時点まで、カスタム ロジックをすべてのステージに接続できます。 フックを使用すると、コア エージェントの動作を変更することなく、アクセス許可、監査、通知などを実装できます。

概要

フックは、セッションの作成時に 1 回登録するコールバックです。 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" }),
});

プロンプト エンリッチメント

ユーザーが自分自身を繰り返す必要がないように、 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 リポジトリを参照してください。

フックの組み合わせ

フックは自然に構成されます。 1 つの 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してください。