{"meta":{"title":"フックを使った作業","intro":"フックを使用すると、Copilot セッションの開始時点から、各ユーザー プロンプトとツール呼び出しを通じて、終了した時点まで、カスタム ロジックをすべてのステージに接続できます。 このガイドでは、主要なエージェントの動作を変更せずにアクセス許可、監査、通知などを送信できるように、実際のユース ケースについて説明します。","product":"GitHub Copilot","breadcrumbs":[{"href":"/ja/copilot","title":"GitHub Copilot"},{"href":"/ja/copilot/how-tos","title":"方法"},{"href":"/ja/copilot/how-tos/copilot-sdk","title":"Copilot SDK"},{"href":"/ja/copilot/how-tos/copilot-sdk/features","title":"機能"},{"href":"/ja/copilot/how-tos/copilot-sdk/features/hooks","title":"フック"}],"documentType":"article"},"body":"# フックを使った作業\n\nフックを使用すると、Copilot セッションの開始時点から、各ユーザー プロンプトとツール呼び出しを通じて、終了した時点まで、カスタム ロジックをすべてのステージに接続できます。 このガイドでは、主要なエージェントの動作を変更せずにアクセス許可、監査、通知などを送信できるように、実際のユース ケースについて説明します。\n\n<!-- markdownlint-disable GHD046 GHD005 -->\n\n<!-- Suppressed: GHD046 (outdated release terminology), GHD005 (hardcoded data variable) -->\n\n## Overview\n\nフックは、セッションの作成時に 1 回登録するコールバックです。 SDK は、会話ライフサイクルの明確に定義されたポイントで呼び出し、コンテキスト入力を渡し、必要に応じてセッションの動作を変更する出力を受け入れます。\n\n![図: 説明されたプロセスを示すフローチャート。](/assets/images/help/copilot/copilot-sdk/features-hooks-diagram-0.png)\n\n| フック                                                                                      | 起動時                       | 実行可能事項                          |\n| ---------------------------------------------------------------------------------------- | ------------------------- | ------------------------------- |\n| [セッションライフサイクルフック](/ja/copilot/how-tos/copilot-sdk/hooks/session-lifecycle#session-start) | セッションが開始 (新規または再開)        | コンテキストの挿入、ユーザー設定の読み込み           |\n| [ユーザー プロンプト送信後フック](/ja/copilot/how-tos/copilot-sdk/hooks/user-prompt-submitted)          | ユーザーがメッセージを送信する           | プロンプトの書き換え、コンテキストの追加、入力のフィルター処理 |\n| [ユーザープロンプト変換用フック](/ja/copilot/how-tos/copilot-sdk/hooks/user-prompt-transformed)         | ランタイムによってモデル プロンプトがビルドされる | モデル向けのコンテンツを検査または置換する           |\n| [ツール使用前のフック](/ja/copilot/how-tos/copilot-sdk/hooks/pre-tool-use)                         | ツールの実行前                   | 呼び出しを許可/拒否/変更する                 |\n| [ツール使用後フック](/ja/copilot/how-tos/copilot-sdk/hooks/post-tool-use)                         | ツールが結果を返した後 (成功のみ)        | 結果の変換、シークレットの編集、監査              |\n| [ツール使用後フック](/ja/copilot/how-tos/copilot-sdk/hooks/post-tool-use#failure-variant)         | ツールがエラーを返した後              | 再試行のガイダンスを挿入し、エラーをログに記録する       |\n| [セッションライフサイクルフック](/ja/copilot/how-tos/copilot-sdk/hooks/session-lifecycle#session-end)   | セッションの終了                  | クリーンアップを行い、指標を記録する              |\n| [エラー処理フック](/ja/copilot/how-tos/copilot-sdk/hooks/error-handling)                         | エラーが発生しました                | カスタム ログ、再試行ロジック、アラート            |\n\nすべてのフックは **省略可能**です。必要なものだけを登録します。 任意のフックから `null` (または同等の言語) を返した場合、SDK は既定の動作を続行するように指示します。\n\n## フックの登録\n\nセッションを作成 (または再開) するときに、 `hooks` オブジェクトを渡します。 次の例はすべて、このパターンに従います。\n\n<div class=\"ghd-codetabs\">\n<div class=\"ghd-codetab\" data-lang=\"typescript\" data-label=\"TypeScript\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">TypeScript</div>\n\n```typescript\nimport { CopilotClient } from \"@github/copilot-sdk\";\n\nconst client = new CopilotClient();\nawait client.start();\n\nconst session = await client.createSession({\n  hooks: {\n    onSessionStart: async (input, invocation) => {\n      /* ... */\n    },\n    onPreToolUse: async (input, invocation) => {\n      /* ... */\n    },\n    onPostToolUse: async (input, invocation) => {\n      /* ... */\n    },\n    // ... add only the hooks you need\n  },\n  onPermissionRequest: async () => ({ kind: \"approve-once\" }),\n});\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"python\" data-label=\"Python\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">Python</div>\n\n```python\nfrom copilot import CopilotClient, PermissionDecisionApproveOnce\n\nclient = CopilotClient()\nawait client.start()\n\nsession = await client.create_session(\n    on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(),\n    hooks={\n        \"on_session_start\": on_session_start,\n        \"on_pre_tool_use\":  on_pre_tool_use,\n        \"on_post_tool_use\": on_post_tool_use,\n        # ... add only the hooks you need\n    },\n)\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"go\" data-label=\"Go\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">Go</div>\n\n```golang\nclient := copilot.NewClient(nil)\n\nsession, err := client.CreateSession(ctx, &copilot.SessionConfig{\n    Hooks: &copilot.SessionHooks{\n        OnSessionStart: onSessionStart,\n        OnPreToolUse:   onPreToolUse,\n        OnPostToolUse:  onPostToolUse,\n        // ... add only the hooks you need\n    },\n    OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) {\n        return &rpc.PermissionDecisionApproveOnce{}, nil\n    },\n})\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"dotnet\" data-label=\".NET\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">.NET</div>\n\n```csharp\nvar client = new CopilotClient();\n\nvar session = await client.CreateSessionAsync(new SessionConfig\n{\n    Hooks = new SessionHooks\n    {\n        OnSessionStart = onSessionStart,\n        OnPreToolUse   = onPreToolUse,\n        OnPostToolUse  = onPostToolUse,\n        // ... add only the hooks you need\n    },\n    OnPermissionRequest = (req, inv) =>\n        Task.FromResult(PermissionDecision.ApproveOnce()),\n});\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"java\" data-label=\"Java\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">Java</div>\n\n```java\nimport com.github.copilot.CopilotClient;\nimport com.github.copilot.rpc.*;\nimport java.util.concurrent.CompletableFuture;\n\ntry (var client = new CopilotClient()) {\n    client.start().get();\n\n    var hooks = new SessionHooks()\n        .setOnSessionStart((input, inv) -> CompletableFuture.completedFuture(null))\n        .setOnPreToolUse((input, inv) -> CompletableFuture.completedFuture(null))\n        .setOnPostToolUse((input, inv) -> CompletableFuture.completedFuture(null));\n        // ... add only the hooks you need\n\n    var session = client.createSession(\n        new SessionConfig()\n            .setHooks(hooks)\n            .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)\n    ).get();\n}\n```\n\n</div>\n\n</div>\n\n> \\[!TIP]\n> すべてのフック ハンドラーは、`invocation`を含む`sessionId` パラメーターを受け取ります。これは、ログの関連付けとセッションごとの状態の維持に役立ちます。\n\n## ユース ケース: アクセス許可の制御\n\n`onPreToolUse`を使用して、エージェントが実行できるツール、許可される引数、実行前にユーザーにプロンプトを表示するかどうかを決定するアクセス許可レイヤーを構築します。\n\n### 安全な一連のツールを許可リストする\n\n<div class=\"ghd-codetabs\">\n<div class=\"ghd-codetab\" data-lang=\"typescript\" data-label=\"TypeScript\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">TypeScript</div>\n\n```typescript\nconst READ_ONLY_TOOLS = [\"read_file\", \"glob\", \"grep\", \"view\"];\n\nconst session = await client.createSession({\n  hooks: {\n    onPreToolUse: async (input) => {\n      if (!READ_ONLY_TOOLS.includes(input.toolName)) {\n        return {\n          permissionDecision: \"deny\",\n          permissionDecisionReason: `Only read-only tools are allowed. \"${input.toolName}\" was blocked.`,\n        };\n      }\n      return { permissionDecision: \"allow\" };\n    },\n  },\n  onPermissionRequest: async () => ({ kind: \"approve-once\" }),\n});\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"python\" data-label=\"Python\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">Python</div>\n\n```python\nfrom copilot import PermissionDecisionApproveOnce\n\nREAD_ONLY_TOOLS = [\"read_file\", \"glob\", \"grep\", \"view\"]\n\nasync def on_pre_tool_use(input_data, invocation):\n    if input_data[\"toolName\"] not in READ_ONLY_TOOLS:\n        return {\n            \"permissionDecision\": \"deny\",\n            \"permissionDecisionReason\":\n                f'Only read-only tools are allowed. \"{input_data[\"toolName\"]}\" was blocked.',\n        }\n    return {\"permissionDecision\": \"allow\"}\n\nsession = await client.create_session(\n    on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(),\n    hooks={\"on_pre_tool_use\": on_pre_tool_use},\n)\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"go\" data-label=\"Go\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">Go</div>\n\n```golang\nreadOnlyTools := map[string]bool{\"read_file\": true, \"glob\": true, \"grep\": true, \"view\": true}\n\nsession, _ := client.CreateSession(ctx, &copilot.SessionConfig{\n    Hooks: &copilot.SessionHooks{\n        OnPreToolUse: func(input copilot.PreToolUseHookInput, inv copilot.HookInvocation) (*copilot.PreToolUseHookOutput, error) {\n            if !readOnlyTools[input.ToolName] {\n                return &copilot.PreToolUseHookOutput{\n                    PermissionDecision:       \"deny\",\n                    PermissionDecisionReason: fmt.Sprintf(\"Only read-only tools are allowed. %q was blocked.\", input.ToolName),\n                }, nil\n            }\n            return &copilot.PreToolUseHookOutput{PermissionDecision: \"allow\"}, nil\n        },\n    },\n})\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"dotnet\" data-label=\".NET\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">.NET</div>\n\n```csharp\nvar readOnlyTools = new HashSet<string> { \"read_file\", \"glob\", \"grep\", \"view\" };\n\nvar session = await client.CreateSessionAsync(new SessionConfig\n{\n    Hooks = new SessionHooks\n    {\n        OnPreToolUse = (input, invocation) =>\n        {\n            if (!readOnlyTools.Contains(input.ToolName))\n            {\n                return Task.FromResult<PreToolUseHookOutput?>(new PreToolUseHookOutput\n                {\n                    PermissionDecision = \"deny\",\n                    PermissionDecisionReason = $\"Only read-only tools are allowed. \\\"{input.ToolName}\\\" was blocked.\",\n                });\n            }\n            return Task.FromResult<PreToolUseHookOutput?>(\n                new PreToolUseHookOutput { PermissionDecision = \"allow\" });\n        },\n    },\n});\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"java\" data-label=\"Java\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">Java</div>\n\n<!-- docs-validate: skip -->\n\n```java\nimport java.util.Set;\nimport java.util.concurrent.CompletableFuture;\n\nimport com.github.copilot.rpc.PermissionHandler;\nimport com.github.copilot.rpc.SessionConfig;\nimport com.github.copilot.rpc.SessionHooks;\nimport com.github.copilot.rpc.PreToolUseHookOutput;\nvar readOnlyTools = Set.of(\"read_file\", \"glob\", \"grep\", \"view\");\n\nvar hooks = new SessionHooks()\n    .setOnPreToolUse((input, invocation) -> {\n        if (!readOnlyTools.contains(input.getToolName())) {\n            return CompletableFuture.completedFuture(\n                PreToolUseHookOutput.deny(\n                    \"Only read-only tools are allowed. \\\"\" + input.getToolName() + \"\\\" was blocked.\")\n            );\n        }\n        return CompletableFuture.completedFuture(PreToolUseHookOutput.allow());\n    });\n\nvar session = client.createSession(\n    new SessionConfig()\n        .setHooks(hooks)\n        .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)\n).get();\n```\n\n</div>\n\n</div>\n\n### 特定のディレクトリへのファイル アクセスを制限する\n\n```typescript\nconst ALLOWED_DIRS = [\"/home/user/projects\", \"/tmp\"];\n\nconst session = await client.createSession({\n  hooks: {\n    onPreToolUse: async (input) => {\n      if ([\"read_file\", \"write_file\", \"edit\"].includes(input.toolName)) {\n        const filePath = (input.toolArgs as { path: string }).path;\n        const allowed = ALLOWED_DIRS.some((dir) => filePath.startsWith(dir));\n\n        if (!allowed) {\n          return {\n            permissionDecision: \"deny\",\n            permissionDecisionReason: `Access to \"${filePath}\" is outside the allowed directories.`,\n          };\n        }\n      }\n      return { permissionDecision: \"allow\" };\n    },\n  },\n  onPermissionRequest: async () => ({ kind: \"approve-once\" }),\n});\n```\n\n### 破壊的操作の前にユーザーに質問する\n\n```typescript\nconst DESTRUCTIVE_TOOLS = [\"delete_file\", \"shell\", \"bash\"];\n\nconst session = await client.createSession({\n  hooks: {\n    onPreToolUse: async (input) => {\n      if (DESTRUCTIVE_TOOLS.includes(input.toolName)) {\n        return { permissionDecision: \"ask\" };\n      }\n      return { permissionDecision: \"allow\" };\n    },\n  },\n  onPermissionRequest: async () => ({ kind: \"approve-once\" }),\n});\n```\n\n`\"ask\"`を返す場合、実行時にユーザーに決定が委任されます。ループ内で人間が必要な破壊的なアクションに役立ちます。\n\n## ユース ケース: 監査とコンプライアンス\n\n`onPreToolUse`、`onPostToolUse`、およびセッション ライフサイクル フックを組み合わせて、エージェントが実行するすべてのアクションを記録する完全な監査証跡を構築します。\n\n### 構造化監査ログ\n\n<div class=\"ghd-codetabs\">\n<div class=\"ghd-codetab\" data-lang=\"typescript\" data-label=\"TypeScript\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">TypeScript</div>\n\n```typescript\ninterface AuditEntry {\n  timestamp: Date;\n  sessionId: string;\n  event: string;\n  toolName?: string;\n  toolArgs?: unknown;\n  toolResult?: unknown;\n  prompt?: string;\n}\n\nconst auditLog: AuditEntry[] = [];\n\nconst session = await client.createSession({\n  hooks: {\n    onSessionStart: async (input, invocation) => {\n      auditLog.push({\n        timestamp: input.timestamp,\n        sessionId: invocation.sessionId,\n        event: \"session_start\",\n      });\n      return null;\n    },\n    onUserPromptSubmitted: async (input, invocation) => {\n      auditLog.push({\n        timestamp: input.timestamp,\n        sessionId: invocation.sessionId,\n        event: \"user_prompt\",\n        prompt: input.prompt,\n      });\n      return null;\n    },\n    onPreToolUse: async (input, invocation) => {\n      auditLog.push({\n        timestamp: input.timestamp,\n        sessionId: invocation.sessionId,\n        event: \"tool_call\",\n        toolName: input.toolName,\n        toolArgs: input.toolArgs,\n      });\n      return { permissionDecision: \"allow\" };\n    },\n    onPostToolUse: async (input, invocation) => {\n      auditLog.push({\n        timestamp: input.timestamp,\n        sessionId: invocation.sessionId,\n        event: \"tool_result\",\n        toolName: input.toolName,\n        toolResult: input.toolResult,\n      });\n      return null;\n    },\n    onSessionEnd: async (input, invocation) => {\n      auditLog.push({\n        timestamp: input.timestamp,\n        sessionId: invocation.sessionId,\n        event: \"session_end\",\n      });\n\n      // Persist the log — swap this with your own storage backend\n      await fs.promises.writeFile(\n        `audit-${invocation.sessionId}.json`,\n        JSON.stringify(auditLog, null, 2),\n      );\n      return null;\n    },\n  },\n  onPermissionRequest: async () => ({ kind: \"approve-once\" }),\n});\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"python\" data-label=\"Python\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">Python</div>\n\n<!-- docs-validate: skip -->\n\n```python\nimport json, aiofiles\nfrom copilot import PermissionDecisionApproveOnce\n\naudit_log = []\n\nasync def on_session_start(input_data, invocation):\n    audit_log.append({\n        \"timestamp\": input_data[\"timestamp\"].isoformat(),\n        \"session_id\": invocation[\"session_id\"],\n        \"event\": \"session_start\",\n    })\n    return None\n\nasync def on_user_prompt_submitted(input_data, invocation):\n    audit_log.append({\n        \"timestamp\": input_data[\"timestamp\"].isoformat(),\n        \"session_id\": invocation[\"session_id\"],\n        \"event\": \"user_prompt\",\n        \"prompt\": input_data[\"prompt\"],\n    })\n    return None\n\nasync def on_pre_tool_use(input_data, invocation):\n    audit_log.append({\n        \"timestamp\": input_data[\"timestamp\"].isoformat(),\n        \"session_id\": invocation[\"session_id\"],\n        \"event\": \"tool_call\",\n        \"tool_name\": input_data[\"toolName\"],\n        \"tool_args\": input_data[\"toolArgs\"],\n    })\n    return {\"permissionDecision\": \"allow\"}\n\nasync def on_post_tool_use(input_data, invocation):\n    audit_log.append({\n        \"timestamp\": input_data[\"timestamp\"].isoformat(),\n        \"session_id\": invocation[\"session_id\"],\n        \"event\": \"tool_result\",\n        \"tool_name\": input_data[\"toolName\"],\n        \"tool_result\": input_data[\"toolResult\"],\n    })\n    return None\n\nasync def on_session_end(input_data, invocation):\n    audit_log.append({\n        \"timestamp\": input_data[\"timestamp\"].isoformat(),\n        \"session_id\": invocation[\"session_id\"],\n        \"event\": \"session_end\",\n    })\n    async with aiofiles.open(f\"audit-{invocation['session_id']}.json\", \"w\") as f:\n        await f.write(json.dumps(audit_log, indent=2))\n    return None\n\nsession = await client.create_session(\n    on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(),\n    hooks={\n        \"on_session_start\": on_session_start,\n        \"on_user_prompt_submitted\": on_user_prompt_submitted,\n        \"on_pre_tool_use\": on_pre_tool_use,\n        \"on_post_tool_use\": on_post_tool_use,\n        \"on_session_end\": on_session_end,\n    },\n)\n```\n\n</div>\n\n</div>\n\n### ツールの結果からシークレットを編集する\n\n```typescript\nconst SECRET_PATTERNS = [\n  /(?:api[_-]?key|token|secret|password)\\s*[:=]\\s*[\"']?[\\w\\-\\.]+[\"']?/gi,\n];\n\nconst session = await client.createSession({\n  hooks: {\n    onPostToolUse: async (input) => {\n      if (typeof input.toolResult !== \"string\") return null;\n\n      let redacted = input.toolResult;\n      for (const pattern of SECRET_PATTERNS) {\n        redacted = redacted.replace(pattern, \"[REDACTED]\");\n      }\n\n      return redacted !== input.toolResult\n        ? { modifiedResult: redacted }\n        : null;\n    },\n  },\n  onPermissionRequest: async () => ({ kind: \"approve-once\" }),\n});\n```\n\n## ユース ケース: 通知とサウンド\n\nフックはアプリケーションのプロセスで発生するため、デスクトップ通知、サウンド、Slack メッセージ、Webhook 呼び出しなどの副作用をトリガーできます。\n\n### セッション イベントに関するデスクトップ通知\n\n<div class=\"ghd-codetabs\">\n<div class=\"ghd-codetab\" data-lang=\"typescript\" data-label=\"TypeScript\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">TypeScript</div>\n\n```typescript\nimport notifier from \"node-notifier\"; // npm install node-notifier\n\nconst session = await client.createSession({\n  hooks: {\n    onSessionEnd: async (input, invocation) => {\n      notifier.notify({\n        title: \"Copilot Session Complete\",\n        message: `Session ${invocation.sessionId.slice(0, 8)} finished (${input.reason}).`,\n      });\n      return null;\n    },\n    onErrorOccurred: async (input) => {\n      notifier.notify({\n        title: \"Copilot Error\",\n        message: input.error.slice(0, 200),\n      });\n      return null;\n    },\n  },\n  onPermissionRequest: async () => ({ kind: \"approve-once\" }),\n});\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"python\" data-label=\"Python\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">Python</div>\n\n```python\nimport subprocess\nfrom copilot import PermissionDecisionApproveOnce\n\nasync def on_session_end(input_data, invocation):\n    sid = invocation[\"session_id\"][:8]\n    reason = input_data[\"reason\"]\n    subprocess.Popen([\n        \"notify-send\", \"Copilot Session Complete\",\n        f\"Session {sid} finished ({reason}).\",\n    ])\n    return None\n\nasync def on_error_occurred(input_data, invocation):\n    subprocess.Popen([\n        \"notify-send\", \"Copilot Error\",\n        input_data[\"error\"][:200],\n    ])\n    return None\n\nsession = await client.create_session(\n    on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(),\n    hooks={\n        \"on_session_end\": on_session_end,\n        \"on_error_occurred\": on_error_occurred,\n    },\n)\n```\n\n</div>\n\n</div>\n\n### ツールの終了時にサウンドを再生する\n\n```typescript\nimport { exec } from \"node:child_process\";\n\nconst session = await client.createSession({\n  hooks: {\n    onPostToolUse: async (input) => {\n      // macOS: play a system sound after every tool call\n      exec(\"afplay /System/Library/Sounds/Pop.aiff\");\n      return null;\n    },\n    onErrorOccurred: async () => {\n      exec(\"afplay /System/Library/Sounds/Basso.aiff\");\n      return null;\n    },\n  },\n  onPermissionRequest: async () => ({ kind: \"approve-once\" }),\n});\n```\n\n### エラー時に Slack に投稿する\n\n```typescript\nconst SLACK_WEBHOOK_URL = process.env.SLACK_WEBHOOK_URL!;\n\nconst session = await client.createSession({\n  hooks: {\n    onErrorOccurred: async (input, invocation) => {\n      if (!input.recoverable) {\n        await fetch(SLACK_WEBHOOK_URL, {\n          method: \"POST\",\n          headers: { \"Content-Type\": \"application/json\" },\n          body: JSON.stringify({\n            text: `🚨 Unrecoverable error in session \\`${invocation.sessionId.slice(0, 8)}\\`:\\n\\`\\`\\`${input.error}\\`\\`\\``,\n          }),\n        });\n      }\n      return null;\n    },\n  },\n  onPermissionRequest: async () => ({ kind: \"approve-once\" }),\n});\n```\n\n## ユース ケース: プロンプト エンリッチメント\n\nユーザーが自分自身を繰り返す必要がないように、 `onSessionStart` と `onUserPromptSubmitted` を使用してコンテキストを自動的に挿入します。\n\n### セッション開始時にプロジェクト メタデータを挿入する\n\n```typescript\nconst session = await client.createSession({\n  hooks: {\n    onSessionStart: async (input) => {\n      const pkg = JSON.parse(\n        await fs.promises.readFile(\"package.json\", \"utf-8\"),\n      );\n      return {\n        additionalContext: [\n          `Project: ${pkg.name} v${pkg.version}`,\n          `Node: ${process.version}`,\n          `Working directory: ${input.workingDirectory}`,\n        ].join(\"\\n\"),\n      };\n    },\n  },\n  onPermissionRequest: async () => ({ kind: \"approve-once\" }),\n});\n```\n\n### プロンプトで短縮形コマンドを展開する\n\n```typescript\nconst SHORTCUTS: Record<string, string> = {\n  \"/fix\": \"Find and fix all errors in the current file\",\n  \"/test\": \"Write comprehensive unit tests for this code\",\n  \"/explain\": \"Explain this code in detail\",\n  \"/refactor\": \"Refactor this code to improve readability\",\n};\n\nconst session = await client.createSession({\n  hooks: {\n    onUserPromptSubmitted: async (input) => {\n      for (const [shortcut, expansion] of Object.entries(SHORTCUTS)) {\n        if (input.prompt.startsWith(shortcut)) {\n          const rest = input.prompt.slice(shortcut.length).trim();\n          return { modifiedPrompt: rest ? `${expansion}: ${rest}` : expansion };\n        }\n      }\n      return null;\n    },\n  },\n  onPermissionRequest: async () => ({ kind: \"approve-once\" }),\n});\n```\n\n## ユース ケース: エラー処理と回復\n\n`onErrorOccurred` フックを使用すると、障害が発生した場合に、再試行、人間への通知、または優雅なシャットダウンを適切に対応できます。\n\n### 一時的なモデル エラーを再試行する\n\n```typescript\nconst session = await client.createSession({\n  hooks: {\n    onErrorOccurred: async (input) => {\n      if (input.errorContext === \"model_call\" && input.recoverable) {\n        return {\n          errorHandling: \"retry\",\n          retryCount: 3,\n          userNotification: \"Temporary model issue — retrying…\",\n        };\n      }\n      return null;\n    },\n  },\n  onPermissionRequest: async () => ({ kind: \"approve-once\" }),\n});\n```\n\n### わかりやすいエラー メッセージ\n\n```typescript\nconst FRIENDLY_MESSAGES: Record<string, string> = {\n  model_call: \"The AI model is temporarily unavailable. Please try again.\",\n  tool_execution: \"A tool encountered an error. Check inputs and try again.\",\n  system: \"A system error occurred. Please try again later.\",\n};\n\nconst session = await client.createSession({\n  hooks: {\n    onErrorOccurred: async (input) => {\n      return {\n        userNotification: FRIENDLY_MESSAGES[input.errorContext] ?? input.error,\n      };\n    },\n  },\n  onPermissionRequest: async () => ({ kind: \"approve-once\" }),\n});\n```\n\n## ユース ケース: セッション メトリック\n\nセッションの実行時間、呼び出されたツールの数、セッションが終了する理由を追跡します。ダッシュボードやコストの監視に役立ちます。\n\n<div class=\"ghd-codetabs\">\n<div class=\"ghd-codetab\" data-lang=\"typescript\" data-label=\"TypeScript\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">TypeScript</div>\n\n```typescript\nconst metrics = new Map<\n  string,\n  { start: Date; toolCalls: number; prompts: number }\n>();\n\nconst session = await client.createSession({\n  hooks: {\n    onSessionStart: async (input, invocation) => {\n      metrics.set(invocation.sessionId, {\n        start: input.timestamp,\n        toolCalls: 0,\n        prompts: 0,\n      });\n      return null;\n    },\n    onUserPromptSubmitted: async (_input, invocation) => {\n      metrics.get(invocation.sessionId)!.prompts++;\n      return null;\n    },\n    onPreToolUse: async (_input, invocation) => {\n      metrics.get(invocation.sessionId)!.toolCalls++;\n      return { permissionDecision: \"allow\" };\n    },\n    onSessionEnd: async (input, invocation) => {\n      const m = metrics.get(invocation.sessionId)!;\n      const durationSec =\n        (input.timestamp.getTime() - m.start.getTime()) / 1000;\n\n      console.log(\n        `Session ${invocation.sessionId.slice(0, 8)}: ` +\n          `${durationSec.toFixed(1)}s, ${m.prompts} prompts, ` +\n          `${m.toolCalls} tool calls, ended: ${input.reason}`,\n      );\n\n      metrics.delete(invocation.sessionId);\n      return null;\n    },\n  },\n  onPermissionRequest: async () => ({ kind: \"approve-once\" }),\n});\n```\n\n</div>\n\n<div class=\"ghd-codetab\" data-lang=\"python\" data-label=\"Python\"><div class=\"ghd-codetab-fallback-label\" role=\"heading\" aria-level=\"3\">Python</div>\n\n```python\nfrom copilot import PermissionDecisionApproveOnce\n\nsession_metrics = {}\n\nasync def on_session_start(input_data, invocation):\n    session_metrics[invocation[\"session_id\"]] = {\n        \"start\": input_data[\"timestamp\"],\n        \"tool_calls\": 0,\n        \"prompts\": 0,\n    }\n    return None\n\nasync def on_user_prompt_submitted(input_data, invocation):\n    session_metrics[invocation[\"session_id\"]][\"prompts\"] += 1\n    return None\n\nasync def on_pre_tool_use(input_data, invocation):\n    session_metrics[invocation[\"session_id\"]][\"tool_calls\"] += 1\n    return {\"permissionDecision\": \"allow\"}\n\nasync def on_session_end(input_data, invocation):\n    m = session_metrics.pop(invocation[\"session_id\"])\n    duration = (input_data[\"timestamp\"] - m[\"start\"]).total_seconds()\n    sid = invocation[\"session_id\"][:8]\n    print(\n        f\"Session {sid}: {duration:.1f}s, {m['prompts']} prompts, \"\n        f\"{m['tool_calls']} tool calls, ended: {input_data['reason']}\"\n    )\n    return None\n\nsession = await client.create_session(\n    on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(),\n    hooks={\n        \"on_session_start\": on_session_start,\n        \"on_user_prompt_submitted\": on_user_prompt_submitted,\n        \"on_pre_tool_use\": on_pre_tool_use,\n        \"on_session_end\": on_session_end,\n    },\n)\n```\n\n</div>\n\n</div>\n\n## フックの組み合わせ\n\nフックは自然に構成されます。 1 つの `hooks` オブジェクトでアクセス許可 **と** 監査 **と** 通知を処理できます。各フックは独自のジョブを実行します。\n\n```typescript\nconst session = await client.createSession({\n  hooks: {\n    onSessionStart: async (input) => {\n      console.log(`[audit] session started in ${input.workingDirectory}`);\n      return { additionalContext: \"Project uses TypeScript and Vitest.\" };\n    },\n    onPreToolUse: async (input) => {\n      console.log(`[audit] tool requested: ${input.toolName}`);\n      if (input.toolName === \"shell\") {\n        return { permissionDecision: \"ask\" };\n      }\n      return { permissionDecision: \"allow\" };\n    },\n    onPostToolUse: async (input) => {\n      console.log(`[audit] tool completed: ${input.toolName}`);\n      return null;\n    },\n    onErrorOccurred: async (input) => {\n      console.error(`[alert] ${input.errorContext}: ${input.error}`);\n      return null;\n    },\n    onSessionEnd: async (input, invocation) => {\n      console.log(\n        `[audit] session ${invocation.sessionId.slice(0, 8)} ended: ${input.reason}`,\n      );\n      return null;\n    },\n  },\n  onPermissionRequest: async () => ({ kind: \"approve-once\" }),\n});\n```\n\n## ベスト プラクティス\n\n1. **フックをしっかりと固定してください。** すべてのフックはインラインで実行されます。低速フックは会話を遅らせることができます。 可能な場合は、大量の作業 (データベースの書き込み、HTTP 呼び出し) をバックグラウンド キューにオフロードします。\n\n2. **何も変更しない場合は、 `null` を返します。** これにより、SDK に既定値を続行するように指示し、不要なオブジェクトの割り当てを回避します。\n\n3. **アクセス許可の決定を明示的に行う。**\n   `{ permissionDecision: \"allow\" }`を返す方が、両方でツールが許可されている場合でも、`null`を返すよりも明確です。\n\n4. **重大なエラーを飲み込まないでください。** 回復可能なツール エラーを抑制しても問題ありませんが、回復不可能なエラーに対して常にログまたはアラートを記録します。\n\n5. **可能な場合は、`additionalContext`の代わりに`modifiedPrompt`を使用します。** コンテキストを追加すると、モデルを引き続きガイドしながら、ユーザーの元の意図が保持されます。\n\n6. **セッション ID で状態をスコープします。** セッションごとのデータを追跡する場合は、 `invocation.sessionId` にキーを設定し、 `onSessionEnd`でクリーンアップします。\n\n## Reference\n\nすべてのフックの完全な型定義、入力/出力フィールド テーブル、およびその他の例については、API リファレンスを参照してください。\n\n* [セッション フック](/ja/copilot/how-tos/copilot-sdk/hooks/hooks-overview)\n* [ツール使用前のフック](/ja/copilot/how-tos/copilot-sdk/hooks/pre-tool-use)\n* [ツール使用後フック](/ja/copilot/how-tos/copilot-sdk/hooks/post-tool-use)\n* [ユーザー プロンプト送信後フック](/ja/copilot/how-tos/copilot-sdk/hooks/user-prompt-submitted)\n* [ユーザープロンプト変換用フック](/ja/copilot/how-tos/copilot-sdk/hooks/user-prompt-transformed)\n* [セッションライフサイクルフック](/ja/copilot/how-tos/copilot-sdk/hooks/session-lifecycle)\n* [エラー処理フック](/ja/copilot/how-tos/copilot-sdk/hooks/error-handling)\n\n## こちらも参照ください\n\n* [初めてのCopilot搭載アプリを構築する](/ja/copilot/how-tos/copilot-sdk/getting-started)\n* [カスタム エージェントとサブエージェント オーケストレーション](/ja/copilot/how-tos/copilot-sdk/features/custom-agents)\n* [ストリーミング セッション イベント](/ja/copilot/how-tos/copilot-sdk/features/streaming-events)\n* [デバッグ ガイド](/ja/copilot/how-tos/copilot-sdk/troubleshooting/debugging)"}