fix: resolve hook permissions to proper categories for approval

When the pre-tool-use hook returns 'ask', the Copilot CLI creates
PermissionRequestHook instead of categorized PermissionRequestRead/
Write/Shell. This caused 'Permission: hook' labels and broke category-
based auto-approval ('Always approve read' wouldn't cover grep/glob).

Add ResolveHookToolCategory mapping in CopilotApprovalCoordinator to
map known tool names (view/grep/glob→read, edit/create→write, etc.)
to their permission categories. Wire into GetFallbackToolName,
BuildPermissionApprovalEvent, and CreateApprovalPolicyOutput so:
- Approval banner shows 'Permission: read' instead of 'Permission: hook'
- 'Always approve' stores the category key, covering all tools in it
- Hook short-circuits when category is already auto-approved
Unknown tools (MCP, custom) keep existing 'hook' behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
David Kaya
2026-03-30 09:41:51 +02:00
co-authored by Copilot
parent 39fee48c0b
commit 042cec6065
4 changed files with 251 additions and 1 deletions
@@ -21,6 +21,24 @@ internal sealed class CopilotApprovalCoordinator
private const string HookPermissionKind = "hook";
private const string ToolCallingActivityType = "tool-calling";
private static readonly Dictionary<string, string> HookToolCategories = new(StringComparer.OrdinalIgnoreCase)
{
["view"] = ReadPermissionKind,
["glob"] = ReadPermissionKind,
["grep"] = ReadPermissionKind,
["lsp"] = ReadPermissionKind,
["edit"] = WritePermissionKind,
["create"] = WritePermissionKind,
["powershell"] = ShellPermissionKind,
["read_powershell"] = ShellPermissionKind,
["write_powershell"] = ShellPermissionKind,
["stop_powershell"] = ShellPermissionKind,
["list_powershell"] = ShellPermissionKind,
["web_fetch"] = UrlPermissionKind,
["web_search"] = UrlPermissionKind,
["store_memory"] = MemoryPermissionKind,
};
private readonly ConcurrentDictionary<string, PendingApprovalRequest> _pendingApprovals = new(StringComparer.Ordinal);
private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, byte>> _requestApprovedTools = new(StringComparer.Ordinal);
@@ -140,6 +158,16 @@ internal sealed class CopilotApprovalCoordinator
string permissionKind = string.IsNullOrWhiteSpace(request.Kind)
? "tool access"
: request.Kind.Trim();
if (request is PermissionRequestHook hook)
{
string? resolvedCategory = ResolveHookToolCategory(hook.ToolName);
if (resolvedCategory is not null)
{
permissionKind = resolvedCategory;
}
}
string agentName = string.IsNullOrWhiteSpace(agent.Name) ? agent.Id : agent.Name;
string? sessionId = NormalizeOptionalString(invocation.SessionId);
string? normalizedToolName = NormalizeOptionalString(toolName);
@@ -476,10 +504,22 @@ internal sealed class CopilotApprovalCoordinator
PermissionRequestWrite => WritePermissionKind,
PermissionRequestRead => ReadPermissionKind,
PermissionRequestMemory => StoreMemoryToolName,
PermissionRequestHook hook => ResolveHookToolCategory(hook.ToolName),
_ => null,
};
}
internal static string? ResolveHookToolCategory(string? toolName)
{
string? normalized = NormalizeOptionalString(toolName);
if (normalized is null)
{
return null;
}
return HookToolCategories.TryGetValue(normalized, out string? category) ? category : null;
}
private static bool MatchesAutoApprovedTool(
IReadOnlyList<string> autoApprovedToolNames,
string? toolName,
@@ -248,11 +248,13 @@ internal static class CopilotSessionHooks
};
}
string? autoApprovedToolName = CopilotApprovalCoordinator.ResolveHookToolCategory(toolName) ?? toolName;
bool requiresApproval = CopilotApprovalCoordinator.RequiresToolCallApproval(
command.Pattern.ApprovalPolicy,
agentDefinition.Id,
toolName,
toolName);
autoApprovedToolName);
return new PreToolUseHookOutput
{