Skip to content

GitHub Copilot: one hook for Copilot CLI and VS Code agent mode (0.26.0) - #44

Merged
davidcrowe merged 2 commits into
mainfrom
feat/copilot-harness
Sep 22, 2026
Merged

davidcrowe merged 2 commits into
mainfrom
feat/copilot-harness

Conversation

@davidcrowe

Copy link
Copy Markdown
Collaborator

GitHub Copilot: one hook for Copilot CLI and VS Code agent mode

Copilot CLI and VS Code's agent mode both read ~/.copilot/hooks/*.json in Claude Code's hook format. The installer (agenticcontrolplane.com, companion PR) writes one file there; this PR makes govern.mjs absorb the two places the readers disagree.

What changes under ACP_HARNESS=copilot

  • VS Code sends its own tool ids (run_in_terminal, create_file, replace_string_in_file, …) with camelCase inputs (filePath). Copilot CLI's PascalCase payload already uses Claude's names. VS Code's dialect is canonicalized once, before any decision, so the hardline floor, dotted Bash.* policy and the gateway classifier see one vocabulary. Exact-match only.
  • Copilot CLI documents the bare {permissionDecision} output; VS Code documents the hookSpecificOutput wrapper. Every PreToolUse verdict is written in both shapes.
  • VS Code calls are attributed copilot-vscode; CLI calls stay copilot.
  • On a box where Claude Code was wired without the plugin, VS Code also runs the ~/.claude/settings.json hook. When the Copilot registration exists, that hook stands down for VS Code's calls: one governed call, one row.
  • No updatedInput vendor-token injection under copilot. Copilot CLI ignores it (preToolUse hookSpecificOutput.updatedInput is ignored in Copilot CLI github/copilot-cli#2013) and VS Code drops any updatedInput that fails the tool's schema.

What does not change for other harnesses

The only code that runs for a non-copilot registration is the VS Code-dialect tell, and the tell set deliberately excludes names Cursor also uses (grep_search, file_search, read_file). String tool_input parsing is scoped to ACP_HARNESS=copilot.

Tests

test/copilot-dialect.test.mjs spawns the real hook against a stub gateway: both output shapes for deny and ask, VS Code canonicalization and attribution, camelCase key twins, string tool_input, stand-down with and without the registration, local-mode hardline deny, no token injection. Full suite 252/252.

Verification limits, stated plainly

Contract-verified against GitHub's hooks reference, the Copilot CLI hooks guide and VS Code's hooks reference (2026-09-21). Not exercised against a live Copilot session: Copilot CLI 1.0.87 picks up the gh login and this account has no Copilot access. VS Code agent hooks are Preview. GitHub's tracker records a CLI version where a preToolUse deny was not honoured (github/copilot-cli#3874).

Merge order

This first. install.sh fetches bin/govern.mjs from main at install time, so the site PR's Copilot registration governs nothing until this lands.

Copilot CLI and VS Code both read ~/.copilot/hooks/*.json in Claude Code's
hook format, but they disagree in two places the hook has to absorb:

- VS Code sends its own tool ids (run_in_terminal, create_file,
  replace_string_in_file ...) with camelCase inputs, while Copilot CLI's
  PascalCase payload already uses Claude's names. Canonicalize VS Code's
  dialect once, before any decision, so the hardline floor, dotted Bash.*
  policy and the gateway classifier see one vocabulary. Exact-match only,
  and the dialect tell excludes names Cursor also uses.
- Copilot CLI documents the bare {permissionDecision} output; VS Code
  documents the hookSpecificOutput wrapper. Under ACP_HARNESS=copilot every
  PreToolUse verdict is written in both shapes.

Attribute VS Code calls as copilot-vscode. On a box where Claude Code was
wired without the plugin, VS Code also runs the ~/.claude/settings.json
hook; when the Copilot registration exists that hook stands down for VS
Code's calls, so one governed call produces one row. No vendor-token
updatedInput under copilot: the CLI ignores it and VS Code schema-drops it.

Version 0.23.0: the hook file changed, and attestation is trust-on-first-use
per (client, version), so shipping this under 0.22.0 would flag every fresh
install as an edited hook.

Nine tests spawn the real hook against a stub gateway: both output shapes
for deny and ask, VS Code canonicalization and attribution, camelCase key
twins, string tool_input, stand-down with and without the registration,
local-mode hardline deny, and no token injection.
@davidcrowe davidcrowe changed the title GitHub Copilot: one hook for Copilot CLI and VS Code agent mode GitHub Copilot: one hook for Copilot CLI and VS Code agent mode (0.23.0) Sep 22, 2026
@davidcrowe

Copy link
Copy Markdown
Collaborator Author

Bumped to 0.23.0 (plugin.json, marketplace.json, govern.mjs). Attestation is trust-on-first-use per (client, version), so a changed hook under 0.22.0 would have flagged every fresh install as hash_mismatch. After merge, set the registry's latest to 0.23.0 (gatewaystack-connect scripts/_tmp_set_hook_registry_0914.mjs 0.23.0 0.16.0), and consider adding copilot and copilot-vscode to its client list.

main moved to 0.25.0 while this branch was cut at 0.22.0. Only the three
version lines conflicted. Attestation is trust-on-first-use per version,
so this ships as 0.26.0, above the live 0.25.0. Full suite 278/278 on the
merged tree.
@davidcrowe davidcrowe changed the title GitHub Copilot: one hook for Copilot CLI and VS Code agent mode (0.23.0) GitHub Copilot: one hook for Copilot CLI and VS Code agent mode (0.26.0) Sep 22, 2026
@davidcrowe

Copy link
Copy Markdown
Collaborator Author

Correction: main was already at 0.25.0, so this now ships as 0.26.0 (merged origin/main in; only the version lines conflicted; 278/278). The registry was set to latest=0.23.0 in the meantime, which is below the live 0.25.0 — after merging, re-run: node scripts/_tmp_set_hook_registry_0914.mjs 0.26.0 0.16.0

@davidcrowe
davidcrowe merged commit d7ce4fd into main Sep 22, 2026
2 checks passed
@davidcrowe
davidcrowe deleted the feat/copilot-harness branch September 22, 2026 22:05
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.

1 participant