Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions com.github.copilot/hooks/hooks.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 16 additions & 5 deletions hooks/hook-hosts.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -38,15 +38,26 @@ export const HOSTS = {
},
// Copilot CLI. PascalCase names put it in its VS Code-compatible mode
// (snake_case SessionStart input; SubagentStart still sends camelCase
// agentName in 1.0.88); it drops prompt-hook output, so there's no prompt hook.
// VS Code reads this file too but doesn't expand ${PLUGIN_ROOT} for Agent
// Plugins hooks or pass their output to the model (pluginParsers.ts,
// copilotPluginConverters.ts on microsoft/vscode main, 2026-09).
// agentName in 1.0.88); it drops prompt-hook output, so it has no prompt
// reminder hook. The other events it lists run only telemetry.
// VS Code reads this file too but doesn't set or expand ${PLUGIN_ROOT} for
// Agent Plugins hooks or pass their output to the model (pluginParsers.ts,
// copilotPluginConverters.ts on microsoft/vscode main, 2026-09), so each
// command checks that its script exists and otherwise exits 0 without output.
copilot: {
manifest: "com.github.copilot/hooks/hooks.json",
format: "flat",
rootVariable: "PLUGIN_ROOT",
events: { SessionStart: "SessionStart", SubagentStart: "SubagentStart" },
runOnlyIfScriptExists: true,
telemetry: "copilot-cli",
events: {
SessionStart: "SessionStart",
SubagentStart: "SubagentStart",
UserPromptSubmit: "UserPromptSubmit",
PostToolUse: "PostToolUse",
PostToolUseFailure: "PostToolUseFailure",
SubagentStop: "SubagentStop",
},
contextOutput: (eventName, text) => ({
additionalContext: text,
hookSpecificOutput: { hookEventName: eventName, additionalContext: text },
Expand Down
103 changes: 103 additions & 0 deletions hooks/telemetry-adapters/copilot-cli.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
// @ts-check

import {
serviceForToolkit,
serviceForToolName,
} from "../telemetry-classify.mjs";
import { GATEWAY_TOOLS } from "../telemetry-contract.mjs";
import {
arcadeToolProperties,
otherServerProperties,
} from "../telemetry-events.mjs";

/** @typedef {import("../telemetry-adapter.mjs").TelemetryAdapter} TelemetryAdapter */
/** @typedef {import("../telemetry-adapter.mjs").HookInput} HookInput */

export const COPILOT_ARCADE_SERVER = "arcade";

const pickString = (/** @type {unknown} */ value) =>
typeof value === "string" && value !== "" ? value : undefined;

/**
* @param {Record<string, any>} raw
* @returns {HookInput}
*/
const normalize = (raw) => {
/** @type {HookInput} */
const input = {};
const hook_event_name = pickString(raw.hook_event_name);
if (hook_event_name) input.hook_event_name = hook_event_name;
const session_id = pickString(raw.session_id);
if (session_id) input.session_id = session_id;
const prompt_id = pickString(raw.prompt_id);
if (prompt_id) input.prompt_id = prompt_id;
const source = pickString(raw.source);
if (source) input.source = source;
if (raw.prompt !== undefined) input.prompt = raw.prompt;
const tool_name = pickString(raw.tool_name);
if (tool_name) input.tool_name = tool_name;
if (raw.tool_input !== undefined && raw.tool_input !== null && typeof raw.tool_input === "object") {
input.tool_input = raw.tool_input;
}
if (
raw.tool_result !== null &&
typeof raw.tool_result === "object" &&
typeof raw.tool_result.text_result_for_llm === "string"
) {
input.tool_response = raw.tool_result.text_result_for_llm;
}
if (raw.error !== undefined) input.error = raw.error;
if (raw.is_interrupt !== undefined) input.is_interrupt = raw.is_interrupt;
const agent_type = pickString(raw.agent_type);
if (agent_type) input.agent_type = agent_type;
const agent_id = pickString(raw.agent_id);
if (agent_id) input.agent_id = agent_id;
if (raw.last_assistant_message !== undefined) input.last_assistant_message = raw.last_assistant_message;
return input;
};

/**
* @param {unknown} toolName
* @param {Record<string, any> | undefined} toolInput
*/
const toolProperties = (toolName, toolInput) => {
if (typeof toolName !== "string") return null;
// Arcade tool names never contain "-", so the last one ends the server name.
const splitAt = toolName.lastIndexOf("-");
// Built-in tools (Bash, Agent) have no server prefix.
if (splitAt <= 0 || splitAt === toolName.length - 1) return null;
const server = toolName.slice(0, splitAt);
const tool = toolName.slice(splitAt + 1);
if (server === COPILOT_ARCADE_SERVER) {
return arcadeToolProperties("arcade", tool, toolInput);
}
if (/** @type {readonly string[]} */ (GATEWAY_TOOLS).includes(tool)) {
return arcadeToolProperties("other_arcade", tool, toolInput);
}
const serverParts = server.split(/[^A-Za-z0-9]+/);
const service =
serviceForToolName(tool) ?? serverParts.map(serviceForToolkit).find(Boolean);
return otherServerProperties(service);
};

/** @type {TelemetryAdapter} */
const copilotCliAdapter = {
host: "copilot-cli",
dataVariable: "COPILOT_PLUGIN_DATA",
// Copilot documents "true" and doesn't say which other values it
// accepts, so any value that isn't clearly off counts.
optOutSwitches: [{ name: "COPILOT_OFFLINE", anyValue: false }],
requiresTurn: false,
promptReminder: false,
subagentSession: true,
hookRows: [
{ event: "UserPromptSubmit" },
{ event: "PostToolUse", matcher: ".+-.+" },
{ event: "PostToolUseFailure", matcher: ".+-.+" },
{ event: "SubagentStop" },
],
normalize,
toolProperties,
};

export default copilotCliAdapter;
17 changes: 17 additions & 0 deletions test/fixtures/telemetry/copilot-cli/app-prompt.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"input": {
"hook_event_name": "UserPromptSubmit",
"session_id": "dd3beb80-4471-4513-99a4-a57d3d7c08df",
"timestamp": "2026-09-24T21:27:02.743Z",
"cwd": "/Users/someone/private-repo",
"prompt": "What is on my calendar tomorrow?"
},
"expected": {
"event": "Plugin prompt submitted",
"extra": {
"could_use_arcade": true,
"service_hints": ["calendar"],
"reminder_sent": false
}
}
}
26 changes: 26 additions & 0 deletions test/fixtures/telemetry/copilot-cli/confirmation-prompt.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"prelude": [
{
"hook_event_name": "UserPromptSubmit",
"session_id": "dd3beb80-4471-4513-99a4-a57d3d7c08df",
"timestamp": "2026-09-24T21:27:02.743Z",
"cwd": "/Users/someone/private-repo",
"prompt": "What is on my calendar tomorrow?"
}
],
"input": {
"hook_event_name": "UserPromptSubmit",
"session_id": "dd3beb80-4471-4513-99a4-a57d3d7c08df",
"timestamp": "2026-09-24T21:27:02.743Z",
"cwd": "/Users/someone/private-repo",
"prompt": "ok"
},
"expected": {
"event": "Plugin prompt submitted",
"extra": {
"could_use_arcade": false,
"service_hints": [],
"reminder_sent": false
}
}
}
55 changes: 55 additions & 0 deletions test/fixtures/telemetry/copilot-cli/hooks.enabled.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
{
"version": 1,
"hooks": {
"SessionStart": [
{
"type": "command",
"command": "if [ -f \"${PLUGIN_ROOT}/hooks/session-start.mjs\" ]; then node \"${PLUGIN_ROOT}/hooks/session-start.mjs\" --host copilot; fi",
"powershell": "if ($env:PLUGIN_ROOT -and (Test-Path -LiteralPath ($env:PLUGIN_ROOT + '/hooks/session-start.mjs'))) { node ($env:PLUGIN_ROOT + '/hooks/session-start.mjs') --host copilot }",
"timeout": 5
}
],
"SubagentStart": [
{
"type": "command",
"command": "if [ -f \"${PLUGIN_ROOT}/hooks/subagent-start.mjs\" ]; then node \"${PLUGIN_ROOT}/hooks/subagent-start.mjs\" --host copilot; fi",
"powershell": "if ($env:PLUGIN_ROOT -and (Test-Path -LiteralPath ($env:PLUGIN_ROOT + '/hooks/subagent-start.mjs'))) { node ($env:PLUGIN_ROOT + '/hooks/subagent-start.mjs') --host copilot }",
"timeout": 5
}
],
"UserPromptSubmit": [
{
"type": "command",
"command": "if [ -f \"${PLUGIN_ROOT}/hooks/telemetry.mjs\" ]; then node \"${PLUGIN_ROOT}/hooks/telemetry.mjs\" --host copilot; fi",
"powershell": "if ($env:PLUGIN_ROOT -and (Test-Path -LiteralPath ($env:PLUGIN_ROOT + '/hooks/telemetry.mjs'))) { node ($env:PLUGIN_ROOT + '/hooks/telemetry.mjs') --host copilot }",
"timeout": 5
}
],
"PostToolUse": [
{
"type": "command",
"command": "if [ -f \"${PLUGIN_ROOT}/hooks/telemetry.mjs\" ]; then node \"${PLUGIN_ROOT}/hooks/telemetry.mjs\" --host copilot; fi",
"powershell": "if ($env:PLUGIN_ROOT -and (Test-Path -LiteralPath ($env:PLUGIN_ROOT + '/hooks/telemetry.mjs'))) { node ($env:PLUGIN_ROOT + '/hooks/telemetry.mjs') --host copilot }",
"timeout": 5,
"matcher": ".+-.+"
}
],
"PostToolUseFailure": [
{
"type": "command",
"command": "if [ -f \"${PLUGIN_ROOT}/hooks/telemetry.mjs\" ]; then node \"${PLUGIN_ROOT}/hooks/telemetry.mjs\" --host copilot; fi",
"powershell": "if ($env:PLUGIN_ROOT -and (Test-Path -LiteralPath ($env:PLUGIN_ROOT + '/hooks/telemetry.mjs'))) { node ($env:PLUGIN_ROOT + '/hooks/telemetry.mjs') --host copilot }",
"timeout": 5,
"matcher": ".+-.+"
}
],
"SubagentStop": [
{
"type": "command",
"command": "if [ -f \"${PLUGIN_ROOT}/hooks/telemetry.mjs\" ]; then node \"${PLUGIN_ROOT}/hooks/telemetry.mjs\" --host copilot; fi",
"powershell": "if ($env:PLUGIN_ROOT -and (Test-Path -LiteralPath ($env:PLUGIN_ROOT + '/hooks/telemetry.mjs'))) { node ($env:PLUGIN_ROOT + '/hooks/telemetry.mjs') --host copilot }",
"timeout": 5
}
]
}
}
24 changes: 24 additions & 0 deletions test/fixtures/telemetry/copilot-cli/post-tool-agent.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"prelude": [
{
"hook_event_name": "UserPromptSubmit",
"session_id": "dd3beb80-4471-4513-99a4-a57d3d7c08df",
"timestamp": "2026-09-24T21:27:02.743Z",
"cwd": "/Users/someone/private-repo",
"prompt": "What is on my calendar tomorrow?"
}
],
"input": {
"hook_event_name": "PostToolUse",
"session_id": "dd3beb80-4471-4513-99a4-a57d3d7c08df",
"timestamp": "2026-09-24T21:27:02.743Z",
"cwd": "/Users/someone/private-repo",
"tool_name": "Agent",
"tool_input": { "agent_type": "arcade:arcade-operator" },
"tool_result": {
"result_type": "success",
"text_result_for_llm": "done"
}
},
"expected": null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"prelude": [
{
"hook_event_name": "UserPromptSubmit",
"session_id": "dd3beb80-4471-4513-99a4-a57d3d7c08df",
"timestamp": "2026-09-24T21:27:02.743Z",
"cwd": "/Users/someone/private-repo",
"prompt": "What is on my calendar tomorrow?"
}
],
"input": {
"hook_event_name": "PostToolUse",
"session_id": "dd3beb80-4471-4513-99a4-a57d3d7c08df",
"timestamp": "2026-09-24T21:27:02.743Z",
"cwd": "/Users/someone/private-repo",
"tool_name": "arcade-Arcade_ListApps",
"tool_input": {},
"tool_result": {
"result_type": "success",
"text_result_for_llm": "[\"Gmail\",\"Slack\",\"Calendar\"]"
}
},
"expected": {
"event": "Plugin tool called",
"extra": {
"server": "arcade",
"tool": "Arcade_ListApps"
}
}
}
24 changes: 24 additions & 0 deletions test/fixtures/telemetry/copilot-cli/post-tool-bash.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"prelude": [
{
"hook_event_name": "UserPromptSubmit",
"session_id": "dd3beb80-4471-4513-99a4-a57d3d7c08df",
"timestamp": "2026-09-24T21:27:02.743Z",
"cwd": "/Users/someone/private-repo",
"prompt": "What is on my calendar tomorrow?"
}
],
"input": {
"hook_event_name": "PostToolUse",
"session_id": "dd3beb80-4471-4513-99a4-a57d3d7c08df",
"timestamp": "2026-09-24T21:27:02.743Z",
"cwd": "/Users/someone/private-repo",
"tool_name": "Bash",
"tool_input": { "command": "echo done" },
"tool_result": {
"result_type": "success",
"text_result_for_llm": "done"
}
},
"expected": null
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"prelude": [
{
"hook_event_name": "UserPromptSubmit",
"session_id": "dd3beb80-4471-4513-99a4-a57d3d7c08df",
"timestamp": "2026-09-24T21:27:02.743Z",
"cwd": "/Users/someone/private-repo",
"prompt": "What is on my calendar tomorrow?"
}
],
"input": {
"hook_event_name": "PostToolUseFailure",
"session_id": "dd3beb80-4471-4513-99a4-a57d3d7c08df",
"timestamp": "2026-09-24T21:27:02.743Z",
"cwd": "/Users/someone/private-repo",
"tool_name": "arcade-Slack_SendMessage",
"tool_input": {},
"error": "MCP server 'arcade': Slack requires authorization"
},
"expected": {
"event": "Plugin tool failed",
"extra": {
"server": "arcade",
"tool": "app_tool",
"service": "chat",
"failure_kind": "auth_required"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"prelude": [
{
"hook_event_name": "UserPromptSubmit",
"session_id": "dd3beb80-4471-4513-99a4-a57d3d7c08df",
"timestamp": "2026-09-24T21:27:02.743Z",
"cwd": "/Users/someone/private-repo",
"prompt": "What is on my calendar tomorrow?"
}
],
"input": {
"hook_event_name": "PostToolUseFailure",
"session_id": "dd3beb80-4471-4513-99a4-a57d3d7c08df",
"timestamp": "2026-09-24T21:27:02.743Z",
"cwd": "/Users/someone/private-repo",
"tool_name": "arcade-error_tool",
"tool_input": {},
"error": "MCP server 'arcade': Something went wrong in the upstream service"
},
"expected": {
"event": "Plugin tool failed",
"extra": {
"server": "arcade",
"tool": "other",
"failure_kind": "tool_error"
}
}
}
Loading
Loading