diff --git a/docs/NEXT_ROADMAP.md b/docs/NEXT_ROADMAP.md
index 5914a2e..eea8f8b 100644
--- a/docs/NEXT_ROADMAP.md
+++ b/docs/NEXT_ROADMAP.md
@@ -5001,5 +5001,33 @@ ThemeResourceHelper에 5개 정적 필드 추가:
---
-최종 업데이트: 2026-04-03 (Phase 22~52 + Phase 17-UI-A~E + Phase 17-A~B 구현 완료)
+---
+
+## Phase 17-C — 훅 시스템 고도화 (v1.8.0) ✅ 완료
+
+> **목표**: ExtendedHookRunner를 에이전트 라이프사이클 전 구간에 연결 + Prompt 모드 구현
+
+### 변경 파일
+
+| 파일 | 변경 내용 |
+|------|----------|
+| `AppSettings.AgentConfig.cs` | `ExtendedHooksConfig`에 4개 이벤트 추가: `preToolUse`, `postToolUse`, `postToolUseFailure`, `agentStop` |
+| `AgentLoopService.ExtendedHooks.cs` (신규, 150줄) | `RunExtendedEventAsync()` — 이벤트 훅 실행·결과 적용. `GetExtendedHooks()` — 설정에서 런타임 엔트리 조회. `ConvertToRuntimeEntries()` — ExtendedHookEntryConfig → ExtendedHookEntry 변환. `ApplyExtendedHookResult()` — additionalContext 시스템 메시지 in-place 주입. |
+| `ExtendedHookRunner.cs` | `RunEventAsync()` + `ExecuteSingleAsync()`에 `LlmService? llm` 파라미터 추가. `RunPromptHookAsync()` 신규 구현: `{{tool_name}}` 등 변수 치환, LLM 호출, 차단 신호 감지. `using AxCopilot.Models` 추가. |
+| `AgentLoopService.cs` | TaskState init 후: `SessionStart` 훅(fire-and-forget) + `UserPromptSubmit` 훅(차단 시 즉시 반환). 적극적 압축 직전: `PreCompact` 훅. 압축 완료 후: `PostCompact` 훅(fire-and-forget). `finally` 블록: `SessionEnd` + `AgentStop` 훅(fire-and-forget). |
+| `AgentLoopService.Execution.cs` | `RunToolHooksAsync()` 개선: 레거시 AgentHookRunner 유지 + ExtendedHookRunner `PreToolUse`/`PostToolUse`/`PostToolUseFailure` 이벤트 추가 실행. |
+
+### 구현 세부사항
+
+- **이벤트 커버리지**: SessionStart, SessionEnd, AgentStop, UserPromptSubmit, PreToolUse, PostToolUse, PostToolUseFailure, PreCompact, PostCompact, FileChanged, PermissionRequest — 11종 이벤트 완전 연결
+- **실행 모드**: Command(bat/ps1), Http(POST webhook), Prompt(LLM 평가) 구현. Agent 모드는 Phase 18-A 예정.
+- **Prompt 모드**: `{{tool_name}}`, `{{tool_input}}`, `{{tool_output}}`, `{{user_message}}`, `{{event}}`, `{{session_id}}` 변수 치환. 응답에 block/deny/차단 포함 시 Block=true.
+- **차단 처리**: UserPromptSubmit 훅 Block=true 시 즉시 `"⚠ 요청이 훅 정책에 의해 차단되었습니다."` 반환
+- **HookFired 이벤트 로그**: 훅 실행 시 JSONL에 eventKind, hookCount, blocked 기록
+- **하위 호환**: 레거시 AgentHooks(command 스크립트) 병행 유지
+- **빌드**: 경고 0, 오류 0
+
+---
+
+최종 업데이트: 2026-04-04 (Phase 22~52 + Phase 17-UI-A~E + Phase 17-A~C 구현 완료)
diff --git a/src/AxCopilot/Models/AppSettings.AgentConfig.cs b/src/AxCopilot/Models/AppSettings.AgentConfig.cs
index fad328c..b39d808 100644
--- a/src/AxCopilot/Models/AppSettings.AgentConfig.cs
+++ b/src/AxCopilot/Models/AppSettings.AgentConfig.cs
@@ -59,24 +59,40 @@ public class ReflexionConfig
/// Phase 17-C: 확장 훅 설정.
public class ExtendedHooksConfig
{
+ // ── 라이프사이클 이벤트 ──────────────────────────────────────────────
+ [JsonPropertyName("session_start")]
+ public List SessionStart { get; set; } = new();
+
+ [JsonPropertyName("session_end")]
+ public List SessionEnd { get; set; } = new();
+
+ [JsonPropertyName("agent_stop")]
+ public List AgentStop { get; set; } = new();
+
[JsonPropertyName("user_prompt_submit")]
public List UserPromptSubmit { get; set; } = new();
+ // ── 도구 이벤트 ─────────────────────────────────────────────────────
+ [JsonPropertyName("pre_tool_use")]
+ public List PreToolUse { get; set; } = new();
+
+ [JsonPropertyName("post_tool_use")]
+ public List PostToolUse { get; set; } = new();
+
+ [JsonPropertyName("post_tool_use_failure")]
+ public List PostToolUseFailure { get; set; } = new();
+
+ // ── 컨텍스트 압축 이벤트 ────────────────────────────────────────────
[JsonPropertyName("pre_compact")]
public List PreCompact { get; set; } = new();
[JsonPropertyName("post_compact")]
public List PostCompact { get; set; } = new();
+ // ── 파일·권한 이벤트 ────────────────────────────────────────────────
[JsonPropertyName("file_changed")]
public List FileChanged { get; set; } = new();
- [JsonPropertyName("session_start")]
- public List SessionStart { get; set; } = new();
-
- [JsonPropertyName("session_end")]
- public List SessionEnd { get; set; } = new();
-
[JsonPropertyName("permission_request")]
public List PermissionRequest { get; set; } = new();
}
diff --git a/src/AxCopilot/Services/Agent/AgentLoopService.Execution.cs b/src/AxCopilot/Services/Agent/AgentLoopService.Execution.cs
index ce97667..5c01bf3 100644
--- a/src/AxCopilot/Services/Agent/AgentLoopService.Execution.cs
+++ b/src/AxCopilot/Services/Agent/AgentLoopService.Execution.cs
@@ -369,27 +369,42 @@ public partial class AgentLoopService
/// 도구 호출 처리 후 루프 제어 액션.
private enum ToolCallAction { Continue, Break, Return }
- /// Phase 33-B: 도구 훅(Pre/Post) 실행 헬퍼.
+ /// Phase 33-B / 17-C: 도구 훅(Pre/Post/Failure) 실행 헬퍼.
private async Task RunToolHooksAsync(
Models.LlmSettings llm, LlmService.ContentBlock call,
AgentContext context, string phase, CancellationToken ct,
ToolResult? result = null)
{
- if (!llm.EnableToolHooks || llm.AgentHooks.Count == 0) return;
- try
+ // ── 레거시 AgentHooks (command 스크립트, 하위 호환) ──────────────
+ if (llm.EnableToolHooks && llm.AgentHooks.Count > 0)
{
- var hookResults = await AgentHookRunner.RunAsync(
- llm.AgentHooks, call.ToolName, phase,
- toolInput: call.ToolInput.ToString(),
- toolOutput: result != null ? TruncateOutput(result.Output, 2048) : null,
- success: result?.Success ?? false,
- workFolder: context.WorkFolder,
- timeoutMs: llm.ToolHookTimeoutMs,
- ct: ct);
- foreach (var pr in hookResults.Where(r => !r.Success))
- EmitEvent(AgentEventType.Error, call.ToolName, $"[Hook:{pr.HookName}] {pr.Output}");
+ try
+ {
+ var hookResults = await AgentHookRunner.RunAsync(
+ llm.AgentHooks, call.ToolName, phase,
+ toolInput: call.ToolInput.ToString(),
+ toolOutput: result != null ? TruncateOutput(result.Output, 2048) : null,
+ success: result?.Success ?? false,
+ workFolder: context.WorkFolder,
+ timeoutMs: llm.ToolHookTimeoutMs,
+ ct: ct);
+ foreach (var pr in hookResults.Where(r => !r.Success))
+ EmitEvent(AgentEventType.Error, call.ToolName, $"[Hook:{pr.HookName}] {pr.Output}");
+ }
+ catch (Exception ex) { LogService.Warn($"[AgentLoop] {phase}ToolUse 레거시 훅 실패: {ex.Message}"); }
}
- catch (Exception ex) { LogService.Warn($"[AgentLoop] {phase}ToolUse 훅 실패: {ex.Message}"); }
+
+ // ── Phase 17-C: 확장 훅 (ExtendedHooks — command/http/prompt 모드) ──
+ var extKind = phase == "pre"
+ ? HookEventKind.PreToolUse
+ : (result?.Success == false ? HookEventKind.PostToolUseFailure : HookEventKind.PostToolUse);
+
+ await RunExtendedEventAsync(
+ extKind, null, ct,
+ toolName: call.ToolName,
+ toolInput: call.ToolInput?.ToString(),
+ toolOutput: result != null ? TruncateOutput(result.Output, 2048) : null,
+ toolSuccess: result?.Success ?? true);
}
/// DelegateAgentTool에서 호출하는 서브에이전트 실행기.
diff --git a/src/AxCopilot/Services/Agent/AgentLoopService.ExtendedHooks.cs b/src/AxCopilot/Services/Agent/AgentLoopService.ExtendedHooks.cs
new file mode 100644
index 0000000..918ee67
--- /dev/null
+++ b/src/AxCopilot/Services/Agent/AgentLoopService.ExtendedHooks.cs
@@ -0,0 +1,158 @@
+using AxCopilot.Models;
+
+namespace AxCopilot.Services.Agent;
+
+///
+/// Phase 17-C: AgentLoopService — 확장 훅 시스템 통합.
+/// 에이전트 라이프사이클 이벤트(SessionStart/End, UserPromptSubmit,
+/// PreToolUse/PostToolUse, PreCompact/PostCompact 등)에
+/// ExtendedHookRunner를 연결합니다.
+///
+public partial class AgentLoopService
+{
+ ///
+ /// 지정 이벤트의 확장 훅을 실행합니다.
+ /// - additionalContext가 있으면 시스템 메시지에 주입
+ /// - Block인 경우 true 반환 → 호출자가 작업을 중단해야 함
+ ///
+ internal async Task RunExtendedEventAsync(
+ HookEventKind kind,
+ List? messages,
+ CancellationToken ct,
+ string? toolName = null,
+ string? toolInput = null,
+ string? toolOutput = null,
+ bool toolSuccess = true,
+ string? userMessage = null,
+ string? changedFile = null)
+ {
+ var hooks = GetExtendedHooks(kind);
+ if (hooks.Count == 0) return false;
+
+ var ctx = new HookContext
+ {
+ Event = kind,
+ ToolName = toolName,
+ ToolInput = toolInput,
+ ToolOutput = toolOutput,
+ ToolSuccess = toolSuccess,
+ UserMessage = userMessage,
+ ChangedFilePath = changedFile,
+ SessionId = _sessionId,
+ WorkFolder = _settings.Settings.Llm.WorkFolder ?? "",
+ };
+
+ try
+ {
+ var result = await ExtendedHookRunner.RunEventAsync(hooks, ctx, ct, _llm);
+ ApplyExtendedHookResult(result, messages);
+
+ // HookFired 이벤트 로그 기록
+ if (_eventLog != null && hooks.Count > 0)
+ _ = _eventLog.AppendAsync(AgentEventLogType.HookFired,
+ System.Text.Json.JsonSerializer.Serialize(new
+ {
+ eventKind = kind.ToString(),
+ hookCount = hooks.Count,
+ blocked = result.Block
+ }));
+
+ return result.Block;
+ }
+ catch (Exception ex)
+ {
+ LogService.Warn($"[ExtendedHook] {kind} 훅 실행 오류: {ex.Message}");
+ return false;
+ }
+ }
+
+ /// 훅 결과의 additionalContext를 시스템 메시지에 주입합니다.
+ private static void ApplyExtendedHookResult(ExtendedHookResult result, List? messages)
+ {
+ if (string.IsNullOrEmpty(result.AdditionalContext) || messages == null) return;
+
+ const string marker = "[Hook Context]";
+ var sysMsg = messages.FirstOrDefault(m => m.Role == "system");
+ if (sysMsg != null)
+ {
+ // 기존 [Hook Context] 섹션 교체 (중복 방지)
+ var idx = sysMsg.Content.IndexOf(marker, StringComparison.Ordinal);
+ if (idx >= 0)
+ sysMsg.Content = sysMsg.Content[..idx] + marker + "\n" + result.AdditionalContext;
+ else
+ sysMsg.Content += $"\n\n{marker}\n{result.AdditionalContext}";
+ }
+ else
+ {
+ messages.Insert(0, new ChatMessage
+ {
+ Role = "system",
+ Content = $"{marker}\n{result.AdditionalContext}"
+ });
+ }
+ }
+
+ // ─────────────────────────────────────────────────────────────────────
+ // 설정 → 런타임 엔트리 변환
+ // ─────────────────────────────────────────────────────────────────────
+
+ /// 설정에서 특정 이벤트의 활성 훅 엔트리 목록을 반환합니다.
+ private List GetExtendedHooks(HookEventKind kind)
+ {
+ var cfg = _settings.Settings.Llm.ExtendedHooks;
+
+ IReadOnlyList? configs = kind switch
+ {
+ HookEventKind.SessionStart => cfg.SessionStart,
+ HookEventKind.SessionEnd => cfg.SessionEnd,
+ HookEventKind.AgentStop => cfg.AgentStop,
+ HookEventKind.UserPromptSubmit => cfg.UserPromptSubmit,
+ HookEventKind.PreToolUse => cfg.PreToolUse,
+ HookEventKind.PostToolUse => cfg.PostToolUse,
+ HookEventKind.PostToolUseFailure => cfg.PostToolUseFailure,
+ HookEventKind.PreCompact => cfg.PreCompact,
+ HookEventKind.PostCompact => cfg.PostCompact,
+ HookEventKind.FileChanged => cfg.FileChanged,
+ HookEventKind.PermissionRequest => cfg.PermissionRequest,
+ _ => null
+ };
+
+ if (configs == null || configs.Count == 0) return new List();
+ return ConvertToRuntimeEntries(configs, kind);
+ }
+
+ /// 설정 모델(ExtendedHookEntryConfig) → 런타임 모델(ExtendedHookEntry) 변환.
+ private static List ConvertToRuntimeEntries(
+ IReadOnlyList configs, HookEventKind kind)
+ {
+ return configs
+ .Where(c => c.Enabled)
+ .Select(c => new ExtendedHookEntry
+ {
+ Name = c.Name,
+ Event = kind,
+ Matcher = c.Matcher,
+ Mode = ParseMode(c.Mode),
+ ScriptPath = c.ScriptPath,
+ Url = c.Url,
+ Prompt = c.Prompt,
+ Model = c.Model,
+ Enabled = c.Enabled,
+ Once = c.Once,
+ IsAsync = c.IsAsync,
+ TimeoutSeconds = c.TimeoutSeconds > 0 ? c.TimeoutSeconds : 30,
+ StatusMessage = c.StatusMessage,
+ WatchPaths = c.WatchPaths ?? new List(),
+ })
+ .ToList();
+ }
+
+ private static HookExecutionMode ParseMode(string? mode) =>
+ mode?.ToLowerInvariant() switch
+ {
+ "http" => HookExecutionMode.Http,
+ "prompt" => HookExecutionMode.Prompt,
+ "agent" => HookExecutionMode.Agent,
+ _ => HookExecutionMode.Command
+ };
+}
diff --git a/src/AxCopilot/Services/Agent/AgentLoopService.cs b/src/AxCopilot/Services/Agent/AgentLoopService.cs
index db2a46c..aaf7ad7 100644
--- a/src/AxCopilot/Services/Agent/AgentLoopService.cs
+++ b/src/AxCopilot/Services/Agent/AgentLoopService.cs
@@ -176,6 +176,23 @@ public partial class AgentLoopService
_ = _eventLog?.AppendAsync(AgentEventLogType.UserMessage,
JsonSerializer.Serialize(new { length = userQuery.Length }));
await InitTaskStateAsync(userQuery, _sessionId);
+
+ // Phase 17-C: SessionStart 훅 실행 (fire-and-forget)
+ _ = RunExtendedEventAsync(HookEventKind.SessionStart, messages, CancellationToken.None,
+ userMessage: userQuery);
+
+ // Phase 17-C: UserPromptSubmit 훅 실행 — 차단 시 즉시 종료
+ if (!string.IsNullOrWhiteSpace(userQuery))
+ {
+ var promptBlocked = await RunExtendedEventAsync(
+ HookEventKind.UserPromptSubmit, messages, ct, userMessage: userQuery);
+ if (promptBlocked)
+ {
+ EmitEvent(AgentEventType.Complete, "", "훅 정책에 의해 요청이 차단되었습니다");
+ return "⚠ 요청이 훅 정책에 의해 차단되었습니다.";
+ }
+ }
+
var consecutiveErrors = 0; // Self-Reflection: 연속 오류 카운터
var totalToolCalls = 0; // 복잡도 추정용
@@ -357,6 +374,8 @@ public partial class AgentLoopService
_ = _eventLog?.AppendAsync(AgentEventLogType.CompactionCompleted,
JsonSerializer.Serialize(new { messageCount = messages.Count }));
UpdateTaskStateSummaryAsync("컨텍스트 압축 완료");
+ // Phase 17-C: PostCompact 훅 (fire-and-forget)
+ _ = RunExtendedEventAsync(HookEventKind.PostCompact, messages, CancellationToken.None);
}
// 임계치 기반 적극적 압축 (이전 LLM 호출의 토큰 사용량 기준)
@@ -374,6 +393,8 @@ public partial class AgentLoopService
$"⚠ 컨텍스트 사용량 {usagePct}% — 적극적 컴팩션 실행");
_ = _eventLog?.AppendAsync(AgentEventLogType.CompactionTriggered,
JsonSerializer.Serialize(new { aggressive = true, usagePct }));
+ // Phase 17-C: PreCompact 훅 — 적극적 압축 직전 (압축 발생 확실)
+ await RunExtendedEventAsync(HookEventKind.PreCompact, messages, ct);
var targetTokens = (int)(llm.MaxContextTokens * Defaults.ContextCompressionTargetRatio);
var compacted = await ContextCondenser.CondenseIfNeededAsync(
messages, _llm, targetTokens, ct);
@@ -383,6 +404,8 @@ public partial class AgentLoopService
_ = _eventLog?.AppendAsync(AgentEventLogType.CompactionCompleted,
JsonSerializer.Serialize(new { aggressive = true, messageCount = messages.Count }));
UpdateTaskStateSummaryAsync($"적극적 컴팩션 완료 (이전 컨텍스트 사용량 {usagePct}%)");
+ // Phase 17-C: PostCompact 훅 (fire-and-forget)
+ _ = RunExtendedEventAsync(HookEventKind.PostCompact, messages, CancellationToken.None);
}
}
}
@@ -778,6 +801,10 @@ public partial class AgentLoopService
}
finally
{
+ // Phase 17-C: SessionEnd + AgentStop 확장 훅 실행 (fire-and-forget)
+ _ = RunExtendedEventAsync(HookEventKind.SessionEnd, null, CancellationToken.None, userMessage: userQuery);
+ _ = RunExtendedEventAsync(HookEventKind.AgentStop, null, CancellationToken.None, userMessage: userQuery);
+
// 세션 종료 이벤트 기록
if (_eventLog != null)
_ = _eventLog.AppendAsync(AgentEventLogType.SessionEnd,
diff --git a/src/AxCopilot/Services/Agent/ExtendedHookRunner.cs b/src/AxCopilot/Services/Agent/ExtendedHookRunner.cs
index 743ba9d..4754b21 100644
--- a/src/AxCopilot/Services/Agent/ExtendedHookRunner.cs
+++ b/src/AxCopilot/Services/Agent/ExtendedHookRunner.cs
@@ -3,6 +3,7 @@ using System.IO;
using System.Net.Http;
using System.Text;
using System.Text.Json;
+using AxCopilot.Models;
namespace AxCopilot.Services.Agent;
@@ -22,10 +23,12 @@ public static class ExtendedHookRunner
};
/// 지정 이벤트의 훅을 모두 실행하고 결과를 병합하여 반환.
+ /// Prompt 모드 훅에서 LLM 호출에 사용할 서비스 (null이면 Prompt 모드 스킵).
public static async Task RunEventAsync(
IReadOnlyList hooks,
HookContext context,
- CancellationToken ct = default)
+ CancellationToken ct = default,
+ LlmService? llm = null)
{
if (hooks == null || hooks.Count == 0)
return new ExtendedHookResult();
@@ -53,7 +56,7 @@ public static class ExtendedHookRunner
{
try
{
- var result = await ExecuteSingleAsync(hook, context, CancellationToken.None);
+ var result = await ExecuteSingleAsync(hook, context, CancellationToken.None, llm);
if (result.Block)
{
// exit code 2 = rewake → AdditionalContext를 통해 대화 재활성화
@@ -72,7 +75,7 @@ public static class ExtendedHookRunner
{
_ = Task.Run(async () =>
{
- try { await ExecuteSingleAsync(hook, context, CancellationToken.None); }
+ try { await ExecuteSingleAsync(hook, context, CancellationToken.None, llm); }
catch (Exception) { }
});
}
@@ -95,7 +98,7 @@ public static class ExtendedHookRunner
try
{
- var result = await ExecuteSingleAsync(hook, context, cts.Token);
+ var result = await ExecuteSingleAsync(hook, context, cts.Token, llm);
if (result.Block) { anyBlocked = true; blockReason = result.BlockReason; break; }
if (result.AdditionalContext != null)
@@ -130,18 +133,70 @@ public static class ExtendedHookRunner
private static async Task ExecuteSingleAsync(
ExtendedHookEntry hook,
HookContext context,
- CancellationToken ct)
+ CancellationToken ct,
+ LlmService? llm = null)
{
return hook.Mode switch
{
HookExecutionMode.Command => await RunCommandHookAsync(hook, context, ct),
HookExecutionMode.Http => await RunHttpHookAsync(hook, context, ct),
- HookExecutionMode.Prompt => new ExtendedHookResult(), // LLM 검사는 LlmService 필요 — 외부에서 처리
- HookExecutionMode.Agent => new ExtendedHookResult(), // 미니 에이전트는 AgentLoopService 필요 — 외부 처리
+ // Phase 17-C: Prompt 모드 — LlmService 주입 필요. 없으면 스킵.
+ HookExecutionMode.Prompt => llm != null
+ ? await RunPromptHookAsync(hook, context, llm, ct)
+ : new ExtendedHookResult(),
+ // Agent 모드: AgentLoopService 참조 필요 — 미구현 (향후 Phase 18-A에서 처리)
+ HookExecutionMode.Agent => new ExtendedHookResult(),
_ => new ExtendedHookResult()
};
}
+ ///
+ /// Phase 17-C: Prompt 모드 훅 — LLM으로 도구/이벤트를 평가합니다.
+ /// 프롬프트에서 {{tool_name}}, {{tool_input}}, {{tool_output}}, {{user_message}} 치환.
+ /// 응답에 "block", "deny", "차단" 포함 시 Block=true 반환.
+ ///
+ private static async Task RunPromptHookAsync(
+ ExtendedHookEntry hook,
+ HookContext context,
+ LlmService llm,
+ CancellationToken ct)
+ {
+ if (string.IsNullOrWhiteSpace(hook.Prompt)) return new ExtendedHookResult();
+
+ // 변수 치환
+ var evalPrompt = hook.Prompt
+ .Replace("{{tool_name}}", context.ToolName ?? "", StringComparison.OrdinalIgnoreCase)
+ .Replace("{{tool_input}}", context.ToolInput ?? "", StringComparison.OrdinalIgnoreCase)
+ .Replace("{{tool_output}}", context.ToolOutput ?? "", StringComparison.OrdinalIgnoreCase)
+ .Replace("{{user_message}}", context.UserMessage ?? "", StringComparison.OrdinalIgnoreCase)
+ .Replace("{{event}}", context.Event.ToString(), StringComparison.OrdinalIgnoreCase)
+ .Replace("{{session_id}}", context.SessionId, StringComparison.OrdinalIgnoreCase);
+
+ string response;
+ try
+ {
+ var messages = new List
+ {
+ new() { Role = "user", Content = evalPrompt }
+ };
+ // 훅 전용 소형 모델이 지정된 경우 → 현재는 기본 LLM 사용 (향후 모델 오버라이드 지원 예정)
+ response = await llm.SendAsync(messages, ct);
+ }
+ catch (Exception)
+ {
+ return new ExtendedHookResult();
+ }
+
+ if (string.IsNullOrWhiteSpace(response)) return new ExtendedHookResult();
+
+ // 차단 신호 감지
+ var lower = response.ToLowerInvariant();
+ if (lower.Contains("block") || lower.Contains("deny") || lower.Contains("차단"))
+ return new ExtendedHookResult { Block = true, BlockReason = Truncate(response, 500) };
+
+ return new ExtendedHookResult { AdditionalContext = Truncate(response, 2000) };
+ }
+
private static async Task RunCommandHookAsync(
ExtendedHookEntry hook,
HookContext context,