# 후크를 사용한 작업

후크를 사용하면 시작되는 순간부터 각 사용자 프롬프트와 도구 호출을 거쳐 종료되는 순간까지 Copilot 세션의 모든 단계에 사용자 지정 논리를 연결할 수 있습니다. 이 가이드에서는 핵심 에이전트 동작을 수정하지 않고 사용 권한, 감사, 알림 등을 제공할 수 있도록 실제 사용 사례를 안내합니다.

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

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

## Overview

후크는 세션을 만들 때 한 번 등록하는 콜백입니다. SDK는 대화 수명 주기의 잘 정의된 지점에서 호출하고, 상황별 입력을 전달하며, 필요에 따라 세션의 동작을 수정하는 출력을 수락합니다.

![다이어그램: 설명된 프로세스를 보여 주는 순서도입니다.](/assets/images/help/copilot/copilot-sdk/features-hooks-diagram-0.png)

| 후크                                                                                   | 실행 시                 | 수행 가능한 작업                   |
| ------------------------------------------------------------------------------------ | -------------------- | --------------------------- |
| [세션 수명 주기 후크](/ko/copilot/how-tos/copilot-sdk/hooks/session-lifecycle#session-start) | 세션 시작(신규 또는 다시 시작)   | 컨텍스트 삽입, 부하 기본 설정           |
| [사용자 프롬프트 제출 후크](/ko/copilot/how-tos/copilot-sdk/hooks/user-prompt-submitted)        | 사용자가 메시지를 보냅니다.      | 프롬프트 다시 작성, 컨텍스트 추가, 입력 필터링 |
| [변환된 사용자 프롬프트 훅](/ko/copilot/how-tos/copilot-sdk/hooks/user-prompt-transformed)      | 런타임이 모델 프롬프트를 빌드합니다. | 모델용 콘텐츠 검사 또는 교체            |
| [사전 도구 사용 후크](/ko/copilot/how-tos/copilot-sdk/hooks/pre-tool-use)                    | 도구를 실행하기 전에          | 호출 허용/거부/수정                 |
| [사후 도구 사용 후크](/ko/copilot/how-tos/copilot-sdk/hooks/post-tool-use)                   | 도구가 반환된 후(성공에만 해당)   | 결과 변환, 기밀 편집, 감사            |
| [사후 도구 사용 후크](/ko/copilot/how-tos/copilot-sdk/hooks/post-tool-use#failure-variant)   | 도구에서 오류를 반환한 후       | 재시도 지침 삽입, 로그 오류            |
| [세션 수명 주기 후크](/ko/copilot/how-tos/copilot-sdk/hooks/session-lifecycle#session-end)   | 세션 종료                | 클린업, 메트릭 기록하기               |
| [오류 처리 후크](/ko/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 메시지 또는 웹후크 호출과 같은 후속 작업을 실행할 수 있습니다.

### 세션 이벤트에 대한 데스크톱 알림

<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 참조를 참조하세요.

* [세션 후크](/ko/copilot/how-tos/copilot-sdk/hooks/hooks-overview)
* [사전 도구 사용 후크](/ko/copilot/how-tos/copilot-sdk/hooks/pre-tool-use)
* [사후 도구 사용 후크](/ko/copilot/how-tos/copilot-sdk/hooks/post-tool-use)
* [사용자 프롬프트 제출 후크](/ko/copilot/how-tos/copilot-sdk/hooks/user-prompt-submitted)
* [변환된 사용자 프롬프트 훅](/ko/copilot/how-tos/copilot-sdk/hooks/user-prompt-transformed)
* [세션 수명 주기 후크](/ko/copilot/how-tos/copilot-sdk/hooks/session-lifecycle)
* [오류 처리 후크](/ko/copilot/how-tos/copilot-sdk/hooks/error-handling)

## 참고하십시오

* [첫 번째 Copilot 기반 앱 빌드](/ko/copilot/how-tos/copilot-sdk/getting-started)
* [사용자 정의 에이전트 및 하위 에이전트 오케스트레이션](/ko/copilot/how-tos/copilot-sdk/features/custom-agents)
* [스트리밍 세션 이벤트](/ko/copilot/how-tos/copilot-sdk/features/streaming-events)
* [디버깅 가이드](/ko/copilot/how-tos/copilot-sdk/troubleshooting/debugging)