{"meta":{"title":"Working with hooks","intro":"Hooks let you plug custom logic into every stage of a Copilot session—from the moment it starts, through each user prompt and tool call, to the moment it ends. This guide walks through practical use cases so you can ship permissions, auditing, notifications, and more without modifying the core agent behavior.","product":"GitHub Copilot","breadcrumbs":[{"href":"/en/enterprise-cloud@latest/copilot","title":"GitHub Copilot"},{"href":"/en/enterprise-cloud@latest/copilot/how-tos","title":"How-tos"},{"href":"/en/enterprise-cloud@latest/copilot/how-tos/copilot-sdk","title":"Copilot SDK"},{"href":"/en/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/features","title":"Features"},{"href":"/en/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/features/hooks","title":"Hooks"}],"documentType":"article"},"body":"# Working with hooks\n\nHooks let you plug custom logic into every stage of a Copilot session—from the moment it starts, through each user prompt and tool call, to the moment it ends. This guide walks through practical use cases so you can ship permissions, auditing, notifications, and more without modifying the core agent behavior.\n\n<!-- markdownlint-disable GHD046 GHD005 -->\n\n<!-- Suppressed: GHD046 (outdated release terminology), GHD005 (hardcoded data variable) -->\n\n## Overview\n\nA hook is a callback you register once when creating a session. The SDK invokes it at a well-defined point in the conversation lifecycle, passes contextual input, and optionally accepts output that modifies the session's behavior.\n\n![Diagram: Flowchart showing the described process.](/assets/images/help/copilot/copilot-sdk/features-hooks-diagram-0.png)\n\n| Hook                                                                                                                     | When it fires                       | What you can do                            |\n| ------------------------------------------------------------------------------------------------------------------------ | ----------------------------------- | ------------------------------------------ |\n| [Session lifecycle hooks](/en/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/session-lifecycle#session-start) | Session begins (new or resumed)     | Inject context, load preferences           |\n| [User prompt submitted hook](/en/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/user-prompt-submitted)        | User sends a message                | Rewrite prompts, add context, filter input |\n| [User prompt transformed hook](/en/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/user-prompt-transformed)    | Runtime builds the model prompt     | Inspect or replace model-facing content    |\n| [Pre-tool use hook](/en/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/pre-tool-use)                          | Before a tool executes              | Allow / deny / modify the call             |\n| [Post-tool use hook](/en/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/post-tool-use)                        | After a tool returns (success only) | Transform results, redact secrets, audit   |\n| [Post-tool use hook](/en/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/post-tool-use#failure-variant)        | After a tool returns a failure      | Inject retry guidance, log failures        |\n| [Session lifecycle hooks](/en/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/session-lifecycle#session-end)   | Session ends                        | Clean up, record metrics                   |\n| [Error handling hook](/en/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/error-handling)                      | An error is raised                  | Custom logging, retry logic, alerts        |\n\nAll hooks are **optional**—register only the ones you need. Returning `null` (or the language equivalent) from any hook tells the SDK to continue with default behavior.\n\n## Registering hooks\n\nPass a `hooks` object when you create (or resume) a session. Every example below follows this pattern.\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> Every hook handler receives an `invocation` parameter containing the `sessionId`, which is useful for correlating logs and maintaining per-session state.\n\n## Use case: permission control\n\nUse `onPreToolUse` to build a permission layer that decides which tools the agent may run, what arguments are allowed, and whether the user should be prompted before execution.\n\n### Allow-list a safe set of tools\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### Restrict file access to specific directories\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### Ask the user before destructive operations\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\nReturning `\"ask\"` delegates the decision to the user at runtime—useful for destructive actions where you want a human in the loop.\n\n## Use case: auditing and compliance\n\nCombine `onPreToolUse`, `onPostToolUse`, and the session lifecycle hooks to build a complete audit trail that records every action the agent takes.\n\n### Structured audit log\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### Redact secrets from tool results\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## Use case: notifications and sounds\n\nHooks fire in your application's process, so you can trigger any side-effect—desktop notifications, sounds, Slack messages, or webhook calls.\n\n### Desktop notification on session events\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### Play a sound when a tool finishes\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### Post to Slack on errors\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## Use case: prompt enrichment\n\nUse `onSessionStart` and `onUserPromptSubmitted` to automatically inject context so users don't have to repeat themselves.\n\n### Inject project metadata at session start\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### Expand shorthand commands in prompts\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## Use case: error handling and recovery\n\nThe `onErrorOccurred` hook gives you a chance to react to failures—whether that means retrying, notifying a human, or gracefully shutting down.\n\n### Retry transient model errors\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### Friendly error messages\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## Use case: session metrics\n\nTrack how long sessions run, how many tools are invoked, and why sessions end—useful for dashboards and cost monitoring.\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## Combining hooks\n\nHooks compose naturally. A single `hooks` object can handle permissions **and** auditing **and** notifications—each hook does its own job.\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## Best practices\n\n1. **Keep hooks fast.** Every hook runs inline—slow hooks delay the conversation. Offload heavy work (database writes, HTTP calls) to a background queue when possible.\n\n2. **Return `null` when you have nothing to change.** This tells the SDK to proceed with defaults and avoids unnecessary object allocation.\n\n3. **Be explicit with permission decisions.** Returning `{ permissionDecision: \"allow\" }` is clearer than returning `null`, even though both allow the tool.\n\n4. **Don't swallow critical errors.** It's fine to suppress recoverable tool errors, but always log or alert on unrecoverable ones.\n\n5. **Use `additionalContext` instead of `modifiedPrompt` when possible.** Appending context preserves the user's original intent while still guiding the model.\n\n6. **Scope state by session ID.** If you track per-session data, key it on `invocation.sessionId` and clean up in `onSessionEnd`.\n\n## Reference\n\nFor full type definitions, input/output field tables, and additional examples for every hook, see the API reference:\n\n* [Session hooks](/en/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/hooks-overview)\n* [Pre-tool use hook](/en/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/pre-tool-use)\n* [Post-tool use hook](/en/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/post-tool-use)\n* [User prompt submitted hook](/en/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/user-prompt-submitted)\n* [User prompt transformed hook](/en/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/user-prompt-transformed)\n* [Session lifecycle hooks](/en/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/session-lifecycle)\n* [Error handling hook](/en/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/error-handling)\n\n## See also\n\n* [Build your first Copilot-powered app](/en/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/getting-started)\n* [Custom agents and sub-agent orchestration](/en/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/features/custom-agents)\n* [Streaming session events](/en/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/features/streaming-events)\n* [Debugging guide](/en/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/troubleshooting/debugging)"}