CM-71014 copilot agent dialect - #520
Merged
Merged
Conversation
…tool vocabulary One hooks file is executed by more than one runtime: VS Code's chat runtime and the Copilot agent runtime (Copilot CLI, and VS Code agent sessions). The agent runtime keys its payload dialect off the CASE of the configured event key. Cycode registered camelCase, so it answered with its own dialect (sessionId, no hook_event_name), which matches_payload rejects - scans were skipped fail-open with nothing surfaced beyond a -v debug line. Confirmed against a customer capture where VS Code itself uses the agent runtime, so this was never CLI-only: their VS Code chat prompts were going unscanned. Register PascalCase event keys. Both runtimes then deliver the Claude-style dialect already parsed here, verified live on Copilot CLI 1.0.75 and VS Code 1.133, and against the customer's capture. The tool vocabulary still differs per runtime, so accept both rather than switch: VS Code reads files as read_file/filePath and names MCP tools mcp_<server>_<tool>; the agent runtime uses Read/path and <server>-<tool>. Agent MCP servers are declared in ~/.copilot/mcp-config.json, so that is read alongside VS Code's mcp.json, and the server split is longest-match since server names may themselves contain the separator. The agent reuses its read tool for directory listings with an identical payload shape, so the path is stat-ed before it counts as a file read. Also drop the timestamp-absent condition from ClaudeCode.matches_payload. Matching on a field being absent is what broke Copilot scanning in the first place, and the documented transcript_path is a sufficient positive test on its own. Stale camelCase installs stay rejected; they are corrected by reinstalling hooks. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nt flag Report mode was never asynchronous. The rendered command backgrounded the scan with `<&0 &`, but the backgrounded child inherits the hook's stdout and the runner reads that pipe until EOF, so the response waited for the scan anyway. Measured: the runner is released after 0s with the stdout redirect and after the full scan duration without it. `<&0` is still required - a bare `cmd &` has its stdin reattached to /dev/null, so the scan reads an empty payload and allows without scanning anything, which looks non-blocking only because it does nothing. Render `<&0 >/dev/null 2>&1 &` so the payload flows and the runner is released. Also stop passing --event and remove the flag. It was added when Copilot CLI payloads carried no event name, to avoid a reinstall when CLI support landed; registering the events in PascalCase means every runtime now self-describes via hook_event_name, so it never had a consumer beyond one debug field. NOTE: hooks installed before this change still pass --event, and the CLI now exits 2 on the unknown option. Copilot treats a non-zero PreToolUse exit as a denial of every tool call, so the MDM scripts must drop --event in the same rollout that ships this binary. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Claude Code Review
This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.
Tip: disable this comment in your organization's Code Review settings.
omer-roth
reviewed
Aug 16, 2026
omer-roth
reviewed
Aug 16, 2026
The stat guard swallowed OSError silently, so a file read that was skipped because its path could not be reached looked identical to one skipped for being a directory. Log it at debug with the path. Reachable on the older interpreters in the support matrix: Path.is_file() raises PermissionError on 3.9 and 3.11 (verified), while on 3.13+ it delegates to os.path.isfile and swallows the error itself. requires-python is >=3.9 and the Docker image and release builds run 3.9, so the branch is live for those installs and inert for the bundled executable. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
omer-roth
approved these changes
Aug 17, 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.
One hooks file is executed by more than one runtime: VS Code's chat runtime and
the Copilot agent runtime (Copilot CLI, and VS Code agent sessions). The agent
runtime keys its payload dialect off the CASE of the configured event key. Cycode
registered camelCase, so it answered with its own dialect (sessionId, no
hook_event_name), which matches_payload rejects - scans were skipped fail-open
with nothing surfaced beyond a -v debug line.
Register PascalCase event keys. Both runtimes then deliver the Claude-style
dialect already parsed here, verified live on Copilot CLI 1.0.75 and VS Code
1.133.
The tool vocabulary still differs per runtime, so accept both rather than switch:
VS Code reads files as read_file/filePath and names MCP tools mcp__;
the agent runtime uses Read/path and -. Agent MCP servers are
declared in ~/.copilot/mcp-config.json, so that is read alongside VS Code's
mcp.json, and the server split is longest-match since server names may themselves
contain the separator. The agent reuses its read tool for directory listings with
an identical payload shape, so the path is stat-ed before it counts as a file read.
Also drop the timestamp-absent condition from ClaudeCode.matches_payload. Matching
on a field being absent is what broke Copilot scanning in the first place, and the
documented transcript_path is a sufficient positive test on its own.
Stale camelCase installs stay rejected; they are corrected by reinstalling hooks.