Skip to content

fix(copilot-hook): recognize CLI Claude-style tool names + dual-format deny#164

Merged
MohamedAklamaash merged 1 commit into
mainfrom
aklamaash/copilot-cli-native-tool-names
Jun 18, 2026
Merged

fix(copilot-hook): recognize CLI Claude-style tool names + dual-format deny#164
MohamedAklamaash merged 1 commit into
mainfrom
aklamaash/copilot-cli-native-tool-names

Conversation

@MohamedAklamaash

@MohamedAklamaash MohamedAklamaash commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Problem

The Copilot CLI emits Claude-style canonical tool names (tool_name: "Read" / Write / Edit / Bash — confirmed from a real agent-audit.log payload), but canonical_tool_name only mapped the lowercase VS Code vocabulary (read_file, view, …). So canonical_tool_name("Read") returned '', the hook hit return {}, and the policy check was skipped for every CLI native-file (and shell) tool call — a native .env read sailed straight through unblocked.

Fix

  1. 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.
  2. transform_response_for_copilot: emit the decision in both the top-level {permissionDecision, permissionDecisionReason} form (per the Copilot CLI hooks reference) and the nested hookSpecificOutput form (Claude-compatible / VS Code agent), so the block is honored regardless of which the surface reads.

Verification

Replaying the exact CLI Read of .env through the patched hook → permissionDecision: "deny" (top-level + nested). Harmless read → allow. .env/.env.prod/.env.local all 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_name now passes Claude-style PascalCase names (Read, Write, Edit, Bash) straight through via an early ALLOWED_NON_MCP_HOOK_NAMES check, restoring policy enforcement for every CLI native-file and shell tool call that was previously being silently skipped. transform_response_for_copilot is updated to emit the decision in both the top-level permissionDecision/permissionDecisionReason shape (Copilot CLI hooks reference) and the nested hookSpecificOutput shape (VS Code agent), ensuring the block is honored by either surface.

  • canonical_tool_name fix: PascalCase CLI names now resolve to the correct canonical token instead of '', so process_pre_tool_use no longer bypasses policy on Read, Write, Edit, and Bash for CLI users.
  • transform_response_for_copilot fix: 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_tool still 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_tool function does not handle CLI-style names and will misclassify them in transcript analytics.

Important Files Changed

Filename Overview
copilot/hooks/unbound.py Two targeted fixes: (1) canonical_tool_name now short-circuits on ALLOWED_NON_MCP_HOOK_NAMES so CLI PascalCase names (Read/Write/Edit/Bash) are no longer silently dropped, restoring policy enforcement; (2) transform_response_for_copilot now emits permissionDecision/permissionDecisionReason at the top level in addition to hookSpecificOutput for dual-surface compatibility. Logic is correct and additive; no behavioral change for VS Code lowercase names.

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]
Loading
%%{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]
Loading

Comments Outside Diff (2)

  1. copilot/hooks/unbound.py, line 598-721 (link)

    P2 No decision logging on the new CLI enforcement path

    process_pre_tool_use now correctly routes CLI Read/Write/Edit/Bash calls through the policy check, but neither the allow nor deny outcome is ever written to the audit log or error.log for those calls. The only log entry that would appear is an error if the API call itself fails. If a .env read 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, and decision.

  2. copilot/hooks/unbound.py, line 767-807 (link)

    P2 map_copilot_tool still misclassifies CLI-style names in transcript analytics

    map_copilot_tool checks SHELL_TOOLS, READ_TOOLS, WRITE_TOOLS, and EDIT_TOOLS — all of which contain only the VS Code lowercase vocabulary. CLI-emitted names (Read, Write, Edit, Bash) fall through to the else branch and are emitted as type: 'afterMCPExecution' instead of beforeReadFile / afterFileEdit / afterShellExecution. The fix in canonical_tool_name closes the PreToolUse policy enforcement gap, but the Stop-event analytics path (build_exchange_from_transcriptmap_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

…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>
@MohamedAklamaash MohamedAklamaash requested a review from a team June 18, 2026 11:47
@MohamedAklamaash MohamedAklamaash merged commit 3deff47 into main Jun 18, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants