fix(copilot-hook): recognize CLI Claude-style tool names + dual-format deny#164
Merged
Merged
Conversation
…t deny
The Copilot CLI emits Claude-style canonical tool names (Read / Write / Edit /
Bash), but canonical_tool_name only mapped the lowercase VS Code vocabulary
(read_file / view / ...), so it returned '' and the hook skipped the policy
check for EVERY CLI native-file/shell tool call — native .env reads sailed
through unblocked. Pass canonical names through.
Also emit the PreToolUse decision in BOTH the top-level
{permissionDecision, permissionDecisionReason} shape (Copilot CLI hooks
reference) and the nested hookSpecificOutput shape (Claude-compatible / VS Code
agent), so the block is honored regardless of which the surface reads.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
pugazhendhi-m
approved these changes
Jun 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The Copilot CLI emits Claude-style canonical tool names (
tool_name: "Read"/Write/Edit/Bash— confirmed from a realagent-audit.logpayload), butcanonical_tool_nameonly mapped the lowercase VS Code vocabulary (read_file,view, …). Socanonical_tool_name("Read")returned'', the hook hitreturn {}, and the policy check was skipped for every CLI native-file (and shell) tool call — a native.envread sailed straight through unblocked.Fix
canonical_tool_name: pass Claude-style names through (raw in ALLOWED_NON_MCP_HOOK_NAMES → raw) before the VS Code lookups. Restores native-file + shell policy enforcement for the CLI.transform_response_for_copilot: emit the decision in both the top-level{permissionDecision, permissionDecisionReason}form (per the Copilot CLI hooks reference) and the nestedhookSpecificOutputform (Claude-compatible / VS Code agent), so the block is honored regardless of which the surface reads.Verification
Replaying the exact CLI
Readof.envthrough the patched hook →permissionDecision: "deny"(top-level + nested). Harmless read → allow..env/.env.prod/.env.localall blocked.This ships to customer machines via self-update — straightforward, additive, no behavior change for the already-working VS Code lowercase names.
🤖 Generated with Claude Code
Greptile Summary
This PR fixes two related gaps in the Copilot hook's CLI support:
canonical_tool_namenow passes Claude-style PascalCase names (Read,Write,Edit,Bash) straight through via an earlyALLOWED_NON_MCP_HOOK_NAMEScheck, restoring policy enforcement for every CLI native-file and shell tool call that was previously being silently skipped.transform_response_for_copilotis updated to emit the decision in both the top-levelpermissionDecision/permissionDecisionReasonshape (Copilot CLI hooks reference) and the nestedhookSpecificOutputshape (VS Code agent), ensuring the block is honored by either surface.canonical_tool_namefix: PascalCase CLI names now resolve to the correct canonical token instead of'', soprocess_pre_tool_useno longer bypasses policy onRead,Write,Edit, andBashfor CLI users.transform_response_for_copilotfix: Both the top-level and nested response shapes are now populated with the same decision and reason, ensuring dual-surface compatibility without introducing any behavioral change for VS Code's existing lowercase-vocabulary path.Confidence Score: 4/5
Safe to merge — the changes are additive and correctly fix a real enforcement gap for CLI users without touching the VS Code path.
Both changes are small, well-scoped, and logically correct. The canonicalization fix restores security enforcement that was previously absent for CLI users. The dual-format response is backward-compatible with VS Code. The main gaps are observability-related: there is no log record of allow/deny decisions in the new CLI enforcement path, and
map_copilot_toolstill misclassifies CLI PascalCase names in the Stop-event analytics path — neither affects the correctness of policy enforcement itself.copilot/hooks/unbound.py — the
map_copilot_toolfunction does not handle CLI-style names and will misclassify them in transcript analytics.Important Files Changed
Flowchart
%%{init: {'theme': 'neutral'}}%% flowchart TD A[PreToolUse event arrives] --> B{canonical_tool_name} B --> C{raw in ALLOWED_NON_MCP_HOOK_NAMES?\nRead / Write / Edit / Bash} C -- YES NEW PATH --> D[return raw as-is\neg. 'Read'] C -- NO --> E{raw in SHELL_TOOLS?} E -- YES --> F[return 'Bash'] E -- NO --> G{raw in READ_TOOLS?} G -- YES --> H[return 'Read'] G -- NO --> I[... other sets ...\nWrite / Edit / mcp*] I --> J[return '' → hook skips] D --> K[process_pre_tool_use continues] F --> K H --> K K --> L[API policy check] L --> M[transform_response_for_copilot] M --> N[Top-level:\npermissionDecision\npermissionDecisionReason\nNEW] M --> O[hookSpecificOutput:\nhookEventName\npermissionDecision\npermissionDecisionReason\nadditionalContext\nEXISTING] N --> P[Copilot CLI reads top-level] O --> Q[VS Code agent reads hookSpecificOutput]%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% flowchart TD A[PreToolUse event arrives] --> B{canonical_tool_name} B --> C{raw in ALLOWED_NON_MCP_HOOK_NAMES?\nRead / Write / Edit / Bash} C -- YES NEW PATH --> D[return raw as-is\neg. 'Read'] C -- NO --> E{raw in SHELL_TOOLS?} E -- YES --> F[return 'Bash'] E -- NO --> G{raw in READ_TOOLS?} G -- YES --> H[return 'Read'] G -- NO --> I[... other sets ...\nWrite / Edit / mcp*] I --> J[return '' → hook skips] D --> K[process_pre_tool_use continues] F --> K H --> K K --> L[API policy check] L --> M[transform_response_for_copilot] M --> N[Top-level:\npermissionDecision\npermissionDecisionReason\nNEW] M --> O[hookSpecificOutput:\nhookEventName\npermissionDecision\npermissionDecisionReason\nadditionalContext\nEXISTING] N --> P[Copilot CLI reads top-level] O --> Q[VS Code agent reads hookSpecificOutput]Comments Outside Diff (2)
copilot/hooks/unbound.py, line 598-721 (link)process_pre_tool_usenow correctly routes CLIRead/Write/Edit/Bashcalls through the policy check, but neither the allow nor deny outcome is ever written to the audit log orerror.logfor those calls. The only log entry that would appear is an error if the API call itself fails. If a.envread were denied in production (the exact scenario described in the PR), there would be no structured log record showing which tool was checked, which file path was involved, or what the decision was — making it impossible to reconstruct the event from logs alone.The logging gap applies equally to the previously-working VS Code lowercase path, so this is a pre-existing pattern, but the new CLI coverage is specifically security-motivated and would benefit from at least an info-level entry capturing
canonical,command, anddecision.copilot/hooks/unbound.py, line 767-807 (link)map_copilot_toolstill misclassifies CLI-style names in transcript analyticsmap_copilot_toolchecksSHELL_TOOLS,READ_TOOLS,WRITE_TOOLS, andEDIT_TOOLS— all of which contain only the VS Code lowercase vocabulary. CLI-emitted names (Read,Write,Edit,Bash) fall through to theelsebranch and are emitted astype: 'afterMCPExecution'instead ofbeforeReadFile/afterFileEdit/afterShellExecution. The fix incanonical_tool_namecloses the PreToolUse policy enforcement gap, but the Stop-event analytics path (build_exchange_from_transcript→map_copilot_tool) will still mis-type any CLI tool entry in the transcript, potentially skewing downstream analytics for CLI users.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Reviews (1): Last reviewed commit: "fix(copilot-hook): recognize CLI Claude-..." | Re-trigger Greptile