# 使用挂钩

挂钩使你能够将自定义逻辑插入到Copilot会话的每个阶段，从启动的那一刻起，到每次用户提示和工具调用，到它结束的那一刻。 本指南将演练实际用例，以便在不修改核心代理行为的情况下交付权限、审核、通知等。

<!-- markdownlint-disable GHD046 GHD005 -->

<!-- Suppressed: GHD046 (outdated release terminology), GHD005 (hardcoded data variable) -->

## 概述

挂钩是创建会话时注册一次的回调。 SDK 在会话生命周期中定义完善的点调用它，传递上下文输入，并选择性地接受修改会话行为的输出。

![图示：显示所述过程的流程图。](/assets/images/help/copilot/copilot-sdk/features-hooks-diagram-0.png)

| 挂钩                                                                                                        | 当它触发时           | 你能做什么           |
| --------------------------------------------------------------------------------------------------------- | --------------- | --------------- |
| [会话生命周期挂钩](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/session-lifecycle#session-start) | 会话开始（新建会话或恢复会话） | 注入上下文，加载首选项     |
| [用户提示提交挂钩](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/user-prompt-submitted)           | 用户发送消息          | 重写提示，添加上下文，筛选输入 |
| [用户提示转换钩子](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/user-prompt-transformed)         | 运行时生成模型提示       | 检查或替换面向模型的内容    |
| [工具使用前挂钩](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/pre-tool-use)                     | 在工具执行之前         | 允许/拒绝/修改调用      |
| [工具使用后挂钩](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/post-tool-use)                    | 工具返回后（仅在成功时）    | 转换结果，屏蔽机密，审核    |
| [工具使用后挂钩](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/post-tool-use#failure-variant)    | 工具返回失败结果后       | 添加重试指引，记录失败日志   |
| [会话生命周期挂钩](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/session-lifecycle#session-end)   | 会话结束            | 清理、记录度量指标       |
| [错误处理挂钩](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/error-handling)                    | 已引发错误           | 自定义日志记录、重试逻辑、警报 |

所有挂钩都是**可选的**，您只需注册所需要的挂钩。 从任何挂钩中返回 `null`（或该语言的等效表达）会告知 SDK 继续执行默认行为。

## 注册挂钩

创建或恢复会话时传递一个`hooks`对象。 下面的每个示例都遵循此模式。

<div class="ghd-codetabs">
<div class="ghd-codetab" data-lang="typescript" data-label="TypeScript"><div class="ghd-codetab-fallback-label" role="heading" aria-level="3">TypeScript</div>

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

</div>

<div class="ghd-codetab" data-lang="python" data-label="Python"><div class="ghd-codetab-fallback-label" role="heading" aria-level="3">Python</div>

```python
from copilot import CopilotClient, PermissionDecisionApproveOnce

client = CopilotClient()
await client.start()

session = await client.create_session(
    on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(),
    hooks={
        "on_session_start": on_session_start,
        "on_pre_tool_use":  on_pre_tool_use,
        "on_post_tool_use": on_post_tool_use,
        # ... add only the hooks you need
    },
)
```

</div>

<div class="ghd-codetab" data-lang="go" data-label="Go"><div class="ghd-codetab-fallback-label" role="heading" aria-level="3">Go</div>

```golang
client := copilot.NewClient(nil)

session, err := client.CreateSession(ctx, &copilot.SessionConfig{
    Hooks: &copilot.SessionHooks{
        OnSessionStart: onSessionStart,
        OnPreToolUse:   onPreToolUse,
        OnPostToolUse:  onPostToolUse,
        // ... add only the hooks you need
    },
    OnPermissionRequest: func(req copilot.PermissionRequest, inv copilot.PermissionInvocation) (rpc.PermissionDecision, error) {
        return &rpc.PermissionDecisionApproveOnce{}, nil
    },
})
```

</div>

<div class="ghd-codetab" data-lang="dotnet" data-label=".NET"><div class="ghd-codetab-fallback-label" role="heading" aria-level="3">.NET</div>

```csharp
var client = new CopilotClient();

var session = await client.CreateSessionAsync(new SessionConfig
{
    Hooks = new SessionHooks
    {
        OnSessionStart = onSessionStart,
        OnPreToolUse   = onPreToolUse,
        OnPostToolUse  = onPostToolUse,
        // ... add only the hooks you need
    },
    OnPermissionRequest = (req, inv) =>
        Task.FromResult(PermissionDecision.ApproveOnce()),
});
```

</div>

<div class="ghd-codetab" data-lang="java" data-label="Java"><div class="ghd-codetab-fallback-label" role="heading" aria-level="3">Java</div>

```java
import com.github.copilot.CopilotClient;
import com.github.copilot.rpc.*;
import java.util.concurrent.CompletableFuture;

try (var client = new CopilotClient()) {
    client.start().get();

    var hooks = new SessionHooks()
        .setOnSessionStart((input, inv) -> CompletableFuture.completedFuture(null))
        .setOnPreToolUse((input, inv) -> CompletableFuture.completedFuture(null))
        .setOnPostToolUse((input, inv) -> CompletableFuture.completedFuture(null));
        // ... add only the hooks you need

    var session = client.createSession(
        new SessionConfig()
            .setHooks(hooks)
            .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
    ).get();
}
```

</div>

</div>

> \[!TIP]
> 每个挂钩处理程序都会接收一个包含 `invocation` 的 `sessionId` 参数，这对关联日志和维护每会话状态很有用。

## 用例：权限控制

用于 `onPreToolUse` 生成一个权限层，该层决定代理可以运行哪些工具、允许哪些参数，以及是否应在执行前提示用户。

### 将一组安全的工具加入允许列表

<div class="ghd-codetabs">
<div class="ghd-codetab" data-lang="typescript" data-label="TypeScript"><div class="ghd-codetab-fallback-label" role="heading" aria-level="3">TypeScript</div>

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

</div>

<div class="ghd-codetab" data-lang="python" data-label="Python"><div class="ghd-codetab-fallback-label" role="heading" aria-level="3">Python</div>

```python
from copilot import PermissionDecisionApproveOnce

READ_ONLY_TOOLS = ["read_file", "glob", "grep", "view"]

async def on_pre_tool_use(input_data, invocation):
    if input_data["toolName"] not in READ_ONLY_TOOLS:
        return {
            "permissionDecision": "deny",
            "permissionDecisionReason":
                f'Only read-only tools are allowed. "{input_data["toolName"]}" was blocked.',
        }
    return {"permissionDecision": "allow"}

session = await client.create_session(
    on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(),
    hooks={"on_pre_tool_use": on_pre_tool_use},
)
```

</div>

<div class="ghd-codetab" data-lang="go" data-label="Go"><div class="ghd-codetab-fallback-label" role="heading" aria-level="3">Go</div>

```golang
readOnlyTools := map[string]bool{"read_file": true, "glob": true, "grep": true, "view": true}

session, _ := client.CreateSession(ctx, &copilot.SessionConfig{
    Hooks: &copilot.SessionHooks{
        OnPreToolUse: func(input copilot.PreToolUseHookInput, inv copilot.HookInvocation) (*copilot.PreToolUseHookOutput, error) {
            if !readOnlyTools[input.ToolName] {
                return &copilot.PreToolUseHookOutput{
                    PermissionDecision:       "deny",
                    PermissionDecisionReason: fmt.Sprintf("Only read-only tools are allowed. %q was blocked.", input.ToolName),
                }, nil
            }
            return &copilot.PreToolUseHookOutput{PermissionDecision: "allow"}, nil
        },
    },
})
```

</div>

<div class="ghd-codetab" data-lang="dotnet" data-label=".NET"><div class="ghd-codetab-fallback-label" role="heading" aria-level="3">.NET</div>

```csharp
var readOnlyTools = new HashSet<string> { "read_file", "glob", "grep", "view" };

var session = await client.CreateSessionAsync(new SessionConfig
{
    Hooks = new SessionHooks
    {
        OnPreToolUse = (input, invocation) =>
        {
            if (!readOnlyTools.Contains(input.ToolName))
            {
                return Task.FromResult<PreToolUseHookOutput?>(new PreToolUseHookOutput
                {
                    PermissionDecision = "deny",
                    PermissionDecisionReason = $"Only read-only tools are allowed. \"{input.ToolName}\" was blocked.",
                });
            }
            return Task.FromResult<PreToolUseHookOutput?>(
                new PreToolUseHookOutput { PermissionDecision = "allow" });
        },
    },
});
```

</div>

<div class="ghd-codetab" data-lang="java" data-label="Java"><div class="ghd-codetab-fallback-label" role="heading" aria-level="3">Java</div>

<!-- docs-validate: skip -->

```java
import java.util.Set;
import java.util.concurrent.CompletableFuture;

import com.github.copilot.rpc.PermissionHandler;
import com.github.copilot.rpc.SessionConfig;
import com.github.copilot.rpc.SessionHooks;
import com.github.copilot.rpc.PreToolUseHookOutput;
var readOnlyTools = Set.of("read_file", "glob", "grep", "view");

var hooks = new SessionHooks()
    .setOnPreToolUse((input, invocation) -> {
        if (!readOnlyTools.contains(input.getToolName())) {
            return CompletableFuture.completedFuture(
                PreToolUseHookOutput.deny(
                    "Only read-only tools are allowed. \"" + input.getToolName() + "\" was blocked.")
            );
        }
        return CompletableFuture.completedFuture(PreToolUseHookOutput.allow());
    });

var session = client.createSession(
    new SessionConfig()
        .setHooks(hooks)
        .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
).get();
```

</div>

</div>

### 限制对特定目录的文件访问

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

### 在破坏性操作之前询问用户

```typescript
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"`，会在运行时将决策委托给用户，这对于需要人工干预的破坏性操作非常有用。

## 用例：审核和符合性

结合 `onPreToolUse`、`onPostToolUse` 和会话生命周期挂钩，构建一个完整的审核线索，以记录代理所执行的每个操作。

### 结构化审核日志

<div class="ghd-codetabs">
<div class="ghd-codetab" data-lang="typescript" data-label="TypeScript"><div class="ghd-codetab-fallback-label" role="heading" aria-level="3">TypeScript</div>

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

</div>

<div class="ghd-codetab" data-lang="python" data-label="Python"><div class="ghd-codetab-fallback-label" role="heading" aria-level="3">Python</div>

<!-- docs-validate: skip -->

```python
import json, aiofiles
from copilot import PermissionDecisionApproveOnce

audit_log = []

async def on_session_start(input_data, invocation):
    audit_log.append({
        "timestamp": input_data["timestamp"].isoformat(),
        "session_id": invocation["session_id"],
        "event": "session_start",
    })
    return None

async def on_user_prompt_submitted(input_data, invocation):
    audit_log.append({
        "timestamp": input_data["timestamp"].isoformat(),
        "session_id": invocation["session_id"],
        "event": "user_prompt",
        "prompt": input_data["prompt"],
    })
    return None

async def on_pre_tool_use(input_data, invocation):
    audit_log.append({
        "timestamp": input_data["timestamp"].isoformat(),
        "session_id": invocation["session_id"],
        "event": "tool_call",
        "tool_name": input_data["toolName"],
        "tool_args": input_data["toolArgs"],
    })
    return {"permissionDecision": "allow"}

async def on_post_tool_use(input_data, invocation):
    audit_log.append({
        "timestamp": input_data["timestamp"].isoformat(),
        "session_id": invocation["session_id"],
        "event": "tool_result",
        "tool_name": input_data["toolName"],
        "tool_result": input_data["toolResult"],
    })
    return None

async def on_session_end(input_data, invocation):
    audit_log.append({
        "timestamp": input_data["timestamp"].isoformat(),
        "session_id": invocation["session_id"],
        "event": "session_end",
    })
    async with aiofiles.open(f"audit-{invocation['session_id']}.json", "w") as f:
        await f.write(json.dumps(audit_log, indent=2))
    return None

session = await client.create_session(
    on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(),
    hooks={
        "on_session_start": on_session_start,
        "on_user_prompt_submitted": on_user_prompt_submitted,
        "on_pre_tool_use": on_pre_tool_use,
        "on_post_tool_use": on_post_tool_use,
        "on_session_end": on_session_end,
    },
)
```

</div>

</div>

### 从工具的结果中删除敏感信息

```typescript
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 调用。

### 会话事件的桌面通知

<div class="ghd-codetabs">
<div class="ghd-codetab" data-lang="typescript" data-label="TypeScript"><div class="ghd-codetab-fallback-label" role="heading" aria-level="3">TypeScript</div>

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

</div>

<div class="ghd-codetab" data-lang="python" data-label="Python"><div class="ghd-codetab-fallback-label" role="heading" aria-level="3">Python</div>

```python
import subprocess
from copilot import PermissionDecisionApproveOnce

async def on_session_end(input_data, invocation):
    sid = invocation["session_id"][:8]
    reason = input_data["reason"]
    subprocess.Popen([
        "notify-send", "Copilot Session Complete",
        f"Session {sid} finished ({reason}).",
    ])
    return None

async def on_error_occurred(input_data, invocation):
    subprocess.Popen([
        "notify-send", "Copilot Error",
        input_data["error"][:200],
    ])
    return None

session = await client.create_session(
    on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(),
    hooks={
        "on_session_end": on_session_end,
        "on_error_occurred": on_error_occurred,
    },
)
```

</div>

</div>

### 工具完成后播放声音

```typescript
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

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

## 用例：提示扩充

使用 `onSessionStart` 和 `onUserPromptSubmitted` 自动注入上下文，以便用户不必重复自己。

### 在会话开始时注入项目元数据

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

### 在提示中展开简短命令

```typescript
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` 挂钩，你可以对失败做出反应，无论你是要重试、通知人员还是正常关闭。

### 重试暂时性模型错误

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

### 友好的错误消息

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

## 用例：会话指标

跟踪会话的运行时间、调用的工具数以及会话结束的原因（对于仪表板和成本监视非常有用）。

<div class="ghd-codetabs">
<div class="ghd-codetab" data-lang="typescript" data-label="TypeScript"><div class="ghd-codetab-fallback-label" role="heading" aria-level="3">TypeScript</div>

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

</div>

<div class="ghd-codetab" data-lang="python" data-label="Python"><div class="ghd-codetab-fallback-label" role="heading" aria-level="3">Python</div>

```python
from copilot import PermissionDecisionApproveOnce

session_metrics = {}

async def on_session_start(input_data, invocation):
    session_metrics[invocation["session_id"]] = {
        "start": input_data["timestamp"],
        "tool_calls": 0,
        "prompts": 0,
    }
    return None

async def on_user_prompt_submitted(input_data, invocation):
    session_metrics[invocation["session_id"]]["prompts"] += 1
    return None

async def on_pre_tool_use(input_data, invocation):
    session_metrics[invocation["session_id"]]["tool_calls"] += 1
    return {"permissionDecision": "allow"}

async def on_session_end(input_data, invocation):
    m = session_metrics.pop(invocation["session_id"])
    duration = (input_data["timestamp"] - m["start"]).total_seconds()
    sid = invocation["session_id"][:8]
    print(
        f"Session {sid}: {duration:.1f}s, {m['prompts']} prompts, "
        f"{m['tool_calls']} tool calls, ended: {input_data['reason']}"
    )
    return None

session = await client.create_session(
    on_permission_request=lambda req, inv: PermissionDecisionApproveOnce(),
    hooks={
        "on_session_start": on_session_start,
        "on_user_prompt_submitted": on_user_prompt_submitted,
        "on_pre_tool_use": on_pre_tool_use,
        "on_session_end": on_session_end,
    },
)
```

</div>

</div>

## 组合挂钩

挂钩会自然组合。 单个 `hooks` 对象即可处理权限 **、** 审计**和**通知 — 每个挂钩各司其职。

```typescript
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 参考：

* [会话挂钩](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/hooks-overview)
* [工具使用前挂钩](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/pre-tool-use)
* [工具使用后挂钩](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/post-tool-use)
* [用户提示提交挂钩](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/user-prompt-submitted)
* [用户提示转换钩子](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/user-prompt-transformed)
* [会话生命周期挂钩](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/session-lifecycle)
* [错误处理挂钩](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/hooks/error-handling)

## 另见

* [构建你的第一个由 Copilot 提供支持的应用](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/getting-started)
* [自定义代理和子代理编排](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/features/custom-agents)
* [流式处理会话事件](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/features/streaming-events)
* [调试指南](/zh/enterprise-cloud@latest/copilot/how-tos/copilot-sdk/troubleshooting/debugging)