feat(vscode-copilot): agent hooks for Copilot Chat in VS Code - #23
feat(vscode-copilot): agent hooks for Copilot Chat in VS Code#23svadrutk wants to merge 10 commits into
Conversation
… row, renderer Ninth provider for GitHub Copilot Chat in VS Code. The wire dialect is Claude Code's, so decode delegates via decodeClaudeAs and encode wraps encodeClaude with two placement fixups VS Code's parser requires: decision/reason nested in hookSpecificOutput for Stop/SubagentStop, and additionalContext on SubagentStart. Capability row is grounded in the extension source (chatHookService.ts, toolCallingLoop.ts, hookResultProcessor.ts) rather than the docs, which contradict themselves on placement. install.Render writes agenthooks-vscode.json at ~/.copilot/hooks/ and .github/hooks/ with PascalCase keys, no matcher (VS Code ignores them, so --filter is the real enforcement), no version, and both timeout spellings.
VS Code and the Copilot CLI glob the same two hook directories, so every generated file is loaded by both. PascalCase keys make the input identical but the CLI answers flat while VS Code answers nested, so the CLI's own COPILOT_* env demotes ProviderVSCodeCopilot to ProviderCopilot before decode — each session then gets an already-correct capability row and encoder with no branch downstream. The mirror case is the same helper: decodeCopilot now falls through to the Claude codec on a snake_case payload, so a camelCase CLI config discovered by VS Code no longer lands on KindOther with the tool fields lost. e2e (real copilot 1.0.81) confirms the CLI registers all five drivable PascalCase names, UserPromptSubmit included, and honors a flat deny.
Ten registry entries: #38-41 backfill the Copilot CLI dialect behaviors DESIGN.md has described since the provider landed but never registered, #42-47 cover VS Code Copilot Chat, each Mitigation naming the code that handles it. README no longer claims Copilot IDE surfaces fire nothing — it now describes the two dialects, which install target to pick, and scopes the cross-runtime claim to what the e2e actually measured.
VS Code's tool names reached CanonicalToolFor unmapped, so every one of them classified as ToolOther. A deny-shell policy silently did not cover run_in_terminal — confirmed live: a capture session ran 'echo hello' straight through a deny that should have blocked it. Enumerated from the extension's wire-name table rather than its package.json, which registers different names (copilot_readFile vs the read_file that actually reaches a hook). Seven execution tools now classify as shell, the four subagent tools as task, plus the edit, search and fetch spellings. canonicalNames is shared and provider-independent, so this is fixed once for every caller rather than per-provider. The read-only terminal tools and run_vscode_command stay ToolOther, pinned by test so a later sweep cannot silently widen an allow-shell policy.
Three payloads recorded from a real VS Code Copilot Chat agent turn (1.135.0 / Copilot Chat 0.63.0), scrubbed but structurally intact. All 25 recorded events came back stamped vscode-copilot, so VS Code's own hookExecutor spawned the hook rather than the bundled Copilot CLI. The capture confirmed the field set matches claudeIn's tags exactly, and that transcript_path, source, model and tool_use_id all ship — unlike the CLI, VS Code needs no ID synthesis. One benign divergence recorded: SessionStart.source is "new" where Claude Code says "startup"; nothing switches on the value. codec_vscode_test.go now decodes the recorded corpus as ground truth and keeps the Claude corpus as an explicit drift assertion, which also covers the five events the three fixtures do not. TestVSCodeDetection gains the half that could not be proven before the capture: a real VS Code payload shape-detects as claude-code, so flag-only detection is a constraint rather than a preference. Quirk #48 records the trap that produced the deny hole: the names the extension registers are not the names a hook receives. Not closed: the stop-continuation check never ran, because the recorder has no OnStop handler and cannot emit Continue().
|
Running ultrareview automatically — This PR adds a whole new provider dialect (vscode-copilot) across 25 files, with subtle response-schema placement differences and silent-failure modes — a bug could silently break hooks for both VS Code and the Copilot CLI, warranting deep multi-pass review.. I'll post findings when complete. |
There was a problem hiding this comment.
Ultrareview completed in 13m 50s
All reported issues were addressed across 25 files
Tip: instead of fixing issues one by one fix them all with cubic
Re-trigger cubic
There was a problem hiding this comment.
All reported issues were addressed across 14 files (changes from recent commits).
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
| // VS Code ships no provider env marker and no field Claude Code doesn't | ||
| // also send, so this one is reachable by the generated --provider flag | ||
| // only: neither detectFromEnv nor detectFromShape can produce it. | ||
| ProviderVSCodeCopilot: true, |
There was a problem hiding this comment.
It's good to call this out, but wonder if there's anything we can do about this.
If I give VSCode or Copilot a hook and lie to agenthooks via --provider claude using the current version, what happens?
If behaviour is completely equivalent, I'd think about not adding explicit support and just documenting that --provider claude works for these two harnesses.
If there's any difference in behaviour, I'd smarten the detection logic to reliably detect it or remove detection support from Claude, as we can no longer guarantee it works faithfully.
There was a problem hiding this comment.
Claude behavior isn't fully equivalent, there are some key differences that affect the provider functionality
- Claude expects decision and reason at the top level. VS Code reads them inside
hookSpecificOutput. Using the Claude codec makesContinueWith(...)silently do nothing in VS Code. - VS Code accepts
additionalContextforSubagentStart. Claude’s codec does not encode context for that event. - Events would report
ProviderClaudeCode, so provider-specific policy, capability checks, MCP resolution, documentation, and diagnostics would all follow the wrong runtime.
...and a couple others.
It's hard to smarten the detection logic cause VSCode passes essentially the same shape as Claude, I think silent misclassification is worse than requiring this configuration
implement-agenthooks-support-for-vscode-copilot | HumanLayer Task
Why the change
Copilot Chat in VS Code fires agent hooks that this library could not serve, because it speaks the Claude-shaped dialect rather than the Copilot CLI's camelCase one, and the README told users the IDE surfaces fire nothing.
Special things to note
e2e/vscode_test.goand no fake hook-executor harness. Verification is unit plus install plus a cross-runtime e2e against the realcopilotbinary, backed by one manual capture session whose recorded payloads are committed as fixtures. Two things stay unproven and are marked as such: the stop-continuation path (the recorder has noOnStophandler), andSubagentStart/SubagentStop/PreCompact, which never fired in either runtime.event.go's canonical tool map is not VS Code-only. It is shared across every provider, and VS Code's tool names were missing from it, sorun_in_terminalclassified asToolOtherand a deny-shell policy silently did not block it — caught live when a capture session ranecho hellostraight through a deny. The trap is that the extension registerscopilot_readFilein itspackage.jsonbut sendsread_fileon the wire, so auditing from the obvious source maps the wrong strings.vscode-copilotandcopilottogether double-fires in the CLI.Change outline
The wire shape is Claude Code's, so both codecs delegate and only the response placement differs. Decode relabels; encode wraps.
Placement is per-event and not symmetric. Getting it wrong fails silently rather than loudly, which is why each row is pinned by a test.
The runtime is resolved before decode, so nothing downstream needs a second branch.
The mirror case shares the helper, and fixes a bug that exists on
maintoday: a camelCase CLI config discovered by VS Code decoded toKindOtherwith the tool fields lost.Files, by responsibility:
The renderer's omissions each prevent a silent failure, so they are worth reading as intent rather than as gaps: no
matcher(VS Code parses matchers and then ignores them, so--filteron the argv is the only real enforcement), noversion, nobash/powershell(VS Code's split iswindows/linux/osx), and both timeout spellings because the reference table saystimeoutwhile a usage example saystimeoutSec.{"hooks": {"PreToolUse": [{ "type": "command", "command": "/usr/local/bin/myhooks agenthooks run --provider=vscode-copilot --timeout=30s --filter=names=Bash", "timeout": 30, "timeoutSec": 30 }]}}Target.DirScopeUserhooks/agenthooks-vscode.json~/.copilotScopeProject.github/hooks/agenthooks-vscode.jsonScopePluginSummary by cubic
Adds Copilot Chat in VS Code as a ninth provider. It speaks Claude Code's wire dialect, so decoding delegates to the Claude codec and encoding wraps it with event-specific placement fixups. The installed config is shared with the Copilot CLI — both glob the same hook directories — so CLI sessions demote the provider via
COPILOT_*env, and the mirror case lets a Claude-shaped CLI payload fall through to the Claude codec instead ofKindOther.New Features
agenthooks-vscode.jsonat~/.copilot/hooks/(user) or.github/hooks/(project) with PascalCase event keys, nomatcher, and both timeout spellings.Stopnesting and the timeout key.--provider=vscode-copilotdegrades toclaude-code..vscode/mcp.jsonand.mcp.json, with the extension's truncatingmcp_prefix sanitization and JSONC parsing.Bug Fixes
run_in_terminaland six other execution tools, previously allToolOther.decodeCopilotfalls through to the Claude codec on snake_case input, so tool args survive and the flat response schema is kept.toolArgsacross Copilot CLI versions, where it is a JSON string through 1.0.80 and a plain object from 1.0.81.Written for commit 64a288b. Summary will update on new commits.
Manual VS Code verification
Verified on VS Code 1.135 / Copilot Chat 0.63 on macOS using a project-scoped
.github/hooks/agenthooks-vscode.jsonin a real repository. Each result below was checked against the Copilot Hooks log; tool/continuation results were also checked in the session transcript or on disk. The normal project hook configuration was restored after the temporary decision hooks.agenthooks-vscode.json, invoked the generated command with--provider=vscode-copilot, and used the repository ascwd.SessionStart,UserPromptSubmit,PreToolUse,PostToolUse, andStop, all completing successfully.read_fileandrun_in_terminaleach produced matching pre/post tool-use IDs. Terminal outputVSCODE_AGENTHOOKS_OKarrived intact inPostToolUse.tool_response.OnToolPrepolicy classifiedrun_in_terminalasToolShelland returned nestedpermissionDecision: "deny". VS Code blockedtouch /tmp/VSCODE_AGENTHOOKS_SHOULD_NOT_EXIST; the file remained absent.Stopcontinuationstop_hook_active: false; nesteddecision: "block"/reasoncaused one more model iteration containingVSCODE_STOP_CONTINUATION_WORKED. The second Stop hadstop_hook_active: trueand finished without a loop.hookSpecificOutput.additionalContextsuppliedVSCODE_SESSION_CONTEXT_WORKED; the model returned that marker when asked.decision: "block"/reasononUserPromptSubmitprevented the model from processing the marker prompt.permissionDecision: "ask"displayed approval UI; approving allowed the harmless terminal command to run.VSCODE_UPDATE_INPUT_ORIGINALterminal command was rewritten throughupdatedInput; onlyVSCODE_UPDATED_INPUT_WORKEDexecuted.hookSpecificOutput.additionalContextand a diagnostic top-level block/reason were parsed and logged, but neither marker appeared in the immediate next response on 1.135. Version-pinned source confirms support but showsappendHookContext(...)is started withoutawait; see quirk #49,docs/research/vscode-copilot-posttooluse-context.md, microsoft/vscode#314118, and fix PR microsoft/vscode#331785. The PR therefore retains the documented nested shape andCapAddContextfor fixed VS Code builds.Scope notes
SubagentStart,SubagentStop, andPreCompactwere not observed because this VS Code session did not expose a deterministic way to trigger them.go test ./...passes atc303db3.