Skip to content

Commit cc13010

Browse files
authored
Merge pull request #2085 from dyoshikawa/resolve-issue-2079-copilotcli-skill-hint-hook
feat(copilotcli): support skill argument-hint frontmatter and preMcpToolCall hook event
2 parents 2e90267 + 744121a commit cc13010

8 files changed

Lines changed: 89 additions & 6 deletions

File tree

docs/reference/file-formats.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ Events present in the shared `hooks` block but unsupported by a given tool are s
143143
| `afterFileEdit` |||||||||||||||||
144144
| `beforeShellExecution` |||||||||||||||||
145145
| `afterShellExecution` |||||||||||||||||
146-
| `beforeMCPExecution` |||||| |||||||||||
146+
| `beforeMCPExecution` |||||| |||||||||||
147147
| `afterMCPExecution` |||||||||||||||||
148148
| `beforeReadFile` |||||||||||||||||
149149
| `beforeAgentResponse` |||||||||||||||||
@@ -183,7 +183,7 @@ Events present in the shared `hooks` block but unsupported by a given tool are s
183183
> **Note:** Hook file paths per tool:
184184
>
185185
> - **Copilot (cloud agent)**`<project>/.github/hooks/copilot-hooks.json`.
186-
> - **Copilot CLI** — project: `<project>/.github/hooks/copilotcli-hooks.json`; global: `~/.copilot/hooks/copilot-hooks.json`. The Copilot CLI docs let you choose any filename inside `.github/hooks/`, so Rulesync uses the CLI-specific name to avoid colliding with the cloud-agent file when both targets are enabled. The global path is a Rulesync convention; the official Copilot CLI documentation does not currently enumerate a global hooks location, so this placement may change if the spec later mandates an alternate layout. Copilot CLI uses a **wider event surface** than the shared cloud-agent set (`sessionStart`, `sessionEnd`, `userPromptSubmitted`, `preToolUse`, `postToolUse`, `postToolUseFailure`, `agentStop` ← `stop`, `subagentStart`, `subagentStop`, `errorOccurred` ← `afterError`, `preCompact`, `permissionRequest`, `notification`) and supports three hook types: **`command`** (`bash`/`powershell` with optional `timeoutSec`, plus pass-through `cwd`/`env`), **`prompt`** (a `prompt` string — Copilot CLI only honors prompt hooks on `sessionStart`, so prompt hooks on other events are dropped), and **`http`** (`url`/`headers`/`allowedEnvVars` with optional `timeoutSec`). On `preToolUse` / `postToolUse`, an entry's optional `matcher` field (a regex compiled as `^(?:PATTERN)$`, tested against the tool name) is emitted and round-tripped; on any other event a matcher is dropped (with a warning) because the CLI does not honor it there ([changelog v1.0.36, 2026-04-24](https://github.com/github/copilot-cli) and [v1.0.63, 2026-06-15](https://github.com/github/copilot-cli)).
186+
> - **Copilot CLI** — project: `<project>/.github/hooks/copilotcli-hooks.json`; global: `~/.copilot/hooks/copilot-hooks.json`. The Copilot CLI docs let you choose any filename inside `.github/hooks/`, so Rulesync uses the CLI-specific name to avoid colliding with the cloud-agent file when both targets are enabled. The global path is a Rulesync convention; the official Copilot CLI documentation does not currently enumerate a global hooks location, so this placement may change if the spec later mandates an alternate layout. Copilot CLI uses a **wider event surface** than the shared cloud-agent set (`sessionStart`, `sessionEnd`, `userPromptSubmitted`, `preToolUse`, `postToolUse`, `postToolUseFailure`, `agentStop` ← `stop`, `subagentStart`, `subagentStop`, `errorOccurred` ← `afterError`, `preCompact`, `permissionRequest`, `notification`, `preMcpToolCall` ← `beforeMCPExecution`) and supports three hook types: **`command`** (`bash`/`powershell` with optional `timeoutSec`, plus pass-through `cwd`/`env`), **`prompt`** (a `prompt` string — Copilot CLI only honors prompt hooks on `sessionStart`, so prompt hooks on other events are dropped), and **`http`** (`url`/`headers`/`allowedEnvVars` with optional `timeoutSec`). On `preToolUse` / `postToolUse`, an entry's optional `matcher` field (a regex compiled as `^(?:PATTERN)$`, tested against the tool name) is emitted and round-tripped; on any other event a matcher is dropped (with a warning) because the CLI does not honor it there ([changelog v1.0.36, 2026-04-24](https://github.com/github/copilot-cli) and [v1.0.63, 2026-06-15](https://github.com/github/copilot-cli)).
187187
> - **Antigravity IDE / Antigravity CLI** — project: `<project>/.agents/hooks.json`; global: `~/.gemini/config/hooks.json`. Both targets share the same dedicated `hooks.json` (a Claude-Code-style matcher map nested under a generated `rulesync` hook name), so enabling both writes the same file.
188188
> - **Devin Desktop (formerly Windsurf)** — project: `<project>/.windsurf/hooks.json`; global: `~/.codeium/windsurf/hooks.json`. The Cascade Hooks file location is unchanged by the Devin Desktop rebrand.
189189
> - **AugmentCode** — project: `<project>/.augment/settings.json`; global: `~/.augment/settings.json`. Hooks are merged under the top-level `hooks` key of the shared settings file (which also holds `toolPermissions`).
@@ -506,6 +506,7 @@ copilot: # for GitHub Copilot-specific parameters (optional)
506506
copilotcli: # for GitHub Copilot CLI-specific parameters (optional; project .github/skills/, global ~/.copilot/skills/)
507507
license: MIT # (optional)
508508
allowed-tools: "shell" # (optional) tools pre-approved without per-use confirmation
509+
argument-hint: "[message]" # (optional) hint shown for the skill's expected arguments
509510
rovodev: # for Rovo Dev CLI-specific parameters (optional; Agent Skills standard)
510511
allowed-tools: "grep bash" # (optional) space-separated string (a YAML list is also accepted)
511512
license: MIT # (optional)

skills/rulesync/file-formats.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ Events present in the shared `hooks` block but unsupported by a given tool are s
143143
| `afterFileEdit` |||||||||||||||||
144144
| `beforeShellExecution` |||||||||||||||||
145145
| `afterShellExecution` |||||||||||||||||
146-
| `beforeMCPExecution` |||||| |||||||||||
146+
| `beforeMCPExecution` |||||| |||||||||||
147147
| `afterMCPExecution` |||||||||||||||||
148148
| `beforeReadFile` |||||||||||||||||
149149
| `beforeAgentResponse` |||||||||||||||||
@@ -183,7 +183,7 @@ Events present in the shared `hooks` block but unsupported by a given tool are s
183183
> **Note:** Hook file paths per tool:
184184
>
185185
> - **Copilot (cloud agent)**`<project>/.github/hooks/copilot-hooks.json`.
186-
> - **Copilot CLI** — project: `<project>/.github/hooks/copilotcli-hooks.json`; global: `~/.copilot/hooks/copilot-hooks.json`. The Copilot CLI docs let you choose any filename inside `.github/hooks/`, so Rulesync uses the CLI-specific name to avoid colliding with the cloud-agent file when both targets are enabled. The global path is a Rulesync convention; the official Copilot CLI documentation does not currently enumerate a global hooks location, so this placement may change if the spec later mandates an alternate layout. Copilot CLI uses a **wider event surface** than the shared cloud-agent set (`sessionStart`, `sessionEnd`, `userPromptSubmitted`, `preToolUse`, `postToolUse`, `postToolUseFailure`, `agentStop` ← `stop`, `subagentStart`, `subagentStop`, `errorOccurred` ← `afterError`, `preCompact`, `permissionRequest`, `notification`) and supports three hook types: **`command`** (`bash`/`powershell` with optional `timeoutSec`, plus pass-through `cwd`/`env`), **`prompt`** (a `prompt` string — Copilot CLI only honors prompt hooks on `sessionStart`, so prompt hooks on other events are dropped), and **`http`** (`url`/`headers`/`allowedEnvVars` with optional `timeoutSec`). On `preToolUse` / `postToolUse`, an entry's optional `matcher` field (a regex compiled as `^(?:PATTERN)$`, tested against the tool name) is emitted and round-tripped; on any other event a matcher is dropped (with a warning) because the CLI does not honor it there ([changelog v1.0.36, 2026-04-24](https://github.com/github/copilot-cli) and [v1.0.63, 2026-06-15](https://github.com/github/copilot-cli)).
186+
> - **Copilot CLI** — project: `<project>/.github/hooks/copilotcli-hooks.json`; global: `~/.copilot/hooks/copilot-hooks.json`. The Copilot CLI docs let you choose any filename inside `.github/hooks/`, so Rulesync uses the CLI-specific name to avoid colliding with the cloud-agent file when both targets are enabled. The global path is a Rulesync convention; the official Copilot CLI documentation does not currently enumerate a global hooks location, so this placement may change if the spec later mandates an alternate layout. Copilot CLI uses a **wider event surface** than the shared cloud-agent set (`sessionStart`, `sessionEnd`, `userPromptSubmitted`, `preToolUse`, `postToolUse`, `postToolUseFailure`, `agentStop` ← `stop`, `subagentStart`, `subagentStop`, `errorOccurred` ← `afterError`, `preCompact`, `permissionRequest`, `notification`, `preMcpToolCall` ← `beforeMCPExecution`) and supports three hook types: **`command`** (`bash`/`powershell` with optional `timeoutSec`, plus pass-through `cwd`/`env`), **`prompt`** (a `prompt` string — Copilot CLI only honors prompt hooks on `sessionStart`, so prompt hooks on other events are dropped), and **`http`** (`url`/`headers`/`allowedEnvVars` with optional `timeoutSec`). On `preToolUse` / `postToolUse`, an entry's optional `matcher` field (a regex compiled as `^(?:PATTERN)$`, tested against the tool name) is emitted and round-tripped; on any other event a matcher is dropped (with a warning) because the CLI does not honor it there ([changelog v1.0.36, 2026-04-24](https://github.com/github/copilot-cli) and [v1.0.63, 2026-06-15](https://github.com/github/copilot-cli)).
187187
> - **Antigravity IDE / Antigravity CLI** — project: `<project>/.agents/hooks.json`; global: `~/.gemini/config/hooks.json`. Both targets share the same dedicated `hooks.json` (a Claude-Code-style matcher map nested under a generated `rulesync` hook name), so enabling both writes the same file.
188188
> - **Devin Desktop (formerly Windsurf)** — project: `<project>/.windsurf/hooks.json`; global: `~/.codeium/windsurf/hooks.json`. The Cascade Hooks file location is unchanged by the Devin Desktop rebrand.
189189
> - **AugmentCode** — project: `<project>/.augment/settings.json`; global: `~/.augment/settings.json`. Hooks are merged under the top-level `hooks` key of the shared settings file (which also holds `toolPermissions`).
@@ -506,6 +506,7 @@ copilot: # for GitHub Copilot-specific parameters (optional)
506506
copilotcli: # for GitHub Copilot CLI-specific parameters (optional; project .github/skills/, global ~/.copilot/skills/)
507507
license: MIT # (optional)
508508
allowed-tools: "shell" # (optional) tools pre-approved without per-use confirmation
509+
argument-hint: "[message]" # (optional) hint shown for the skill's expected arguments
509510
rovodev: # for Rovo Dev CLI-specific parameters (optional; Agent Skills standard)
510511
allowed-tools: "grep bash" # (optional) space-separated string (a YAML list is also accepted)
511512
license: MIT # (optional)

src/features/hooks/copilotcli-hooks.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ describe("CopilotcliHooks", () => {
156156
preCompact: [{ command: "echo compact" }],
157157
permissionRequest: [{ command: "echo perm" }],
158158
notification: [{ command: "echo notify" }],
159+
beforeMCPExecution: [{ command: "echo mcp" }],
159160
},
160161
};
161162
const rulesyncHooks = new RulesyncHooks({
@@ -177,6 +178,47 @@ describe("CopilotcliHooks", () => {
177178
expect(parsed.hooks.preCompact).toBeDefined();
178179
expect(parsed.hooks.permissionRequest).toBeDefined();
179180
expect(parsed.hooks.notification).toBeDefined();
181+
// `beforeMCPExecution` maps to Copilot CLI's `preMcpToolCall` (v1.0.51).
182+
expect(parsed.hooks.preMcpToolCall).toBeDefined();
183+
});
184+
185+
it("round-trips the preMcpToolCall hook event", async () => {
186+
const copilotConfig = {
187+
version: 1,
188+
hooks: {
189+
preMcpToolCall: [{ type: "command", bash: "echo mcp" }],
190+
},
191+
};
192+
const imported = new CopilotcliHooks({
193+
outputRoot: testDir,
194+
relativeDirPath: ".github/hooks",
195+
relativeFilePath: "copilotcli-hooks.json",
196+
fileContent: JSON.stringify(copilotConfig),
197+
validate: false,
198+
});
199+
200+
// Import maps `preMcpToolCall` back to canonical `beforeMCPExecution`.
201+
const canonical = JSON.parse(imported.toRulesyncHooks().getFileContent());
202+
expect(canonical.hooks.beforeMCPExecution).toBeDefined();
203+
expect(canonical.hooks.beforeMCPExecution[0]).toMatchObject({
204+
type: "command",
205+
command: "echo mcp",
206+
});
207+
208+
// Re-export emits the Copilot CLI event name again.
209+
const rulesyncHooks = new RulesyncHooks({
210+
outputRoot: testDir,
211+
relativeDirPath: RULESYNC_RELATIVE_DIR_PATH,
212+
relativeFilePath: "hooks.json",
213+
fileContent: JSON.stringify(canonical),
214+
validate: false,
215+
});
216+
const reExported = await CopilotcliHooks.fromRulesyncHooks({
217+
outputRoot: testDir,
218+
rulesyncHooks,
219+
});
220+
const parsed = JSON.parse(reExported.getFileContent());
221+
expect(parsed.hooks.preMcpToolCall).toBeDefined();
180222
});
181223

182224
it("emits prompt and http hook types and preserves cwd/env", async () => {

src/features/hooks/copilotcli-hooks.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ import {
3838
* (`sessionStart`, `sessionEnd`, `userPromptSubmitted`, `preToolUse`,
3939
* `postToolUse`, `postToolUseFailure`, `agentStop`, `subagentStart`,
4040
* `subagentStop`, `errorOccurred`, `preCompact`, `permissionRequest`,
41-
* `notification`). Each entry supports three hook types:
41+
* `notification`, `preMcpToolCall`). Each entry supports three hook types:
4242
*
4343
* - `command` — the `bash` / `powershell` command-field shape with optional
4444
* `timeoutSec`, plus optional `cwd` / `env`.

src/features/skills/copilotcli-skill.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,26 @@ Skill content goes here.`,
230230
const roundTripped = CopilotcliSkill.fromRulesyncSkill({ rulesyncSkill });
231231
expect(roundTripped.getFrontmatter()["allowed-tools"]).toBe("shell");
232232
});
233+
234+
it("should round-trip the argument-hint skill frontmatter", () => {
235+
const skill = new CopilotcliSkill({
236+
dirName: "hint-skill",
237+
frontmatter: {
238+
name: "hint-skill",
239+
description: "Takes an argument",
240+
"argument-hint": "[message]",
241+
},
242+
body: "body",
243+
});
244+
245+
const rulesyncSkill = skill.toRulesyncSkill();
246+
expect(rulesyncSkill.getFrontmatter().copilotcli).toEqual({
247+
"argument-hint": "[message]",
248+
});
249+
250+
const roundTripped = CopilotcliSkill.fromRulesyncSkill({ rulesyncSkill });
251+
expect(roundTripped.getFrontmatter()["argument-hint"]).toBe("[message]");
252+
});
233253
});
234254

235255
describe("isTargetedByRulesyncSkill", () => {

src/features/skills/copilotcli-skill.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ export const CopilotcliSkillFrontmatterSchema = z.looseObject({
2626
// Pre-approved tools the agent may run without per-use confirmation.
2727
// https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/add-skills
2828
"allowed-tools": z.optional(z.union([z.string(), z.array(z.string())])),
29+
// Hint shown for the skill's expected arguments. Added in Copilot CLI v1.0.62
30+
// (2026-06-13). https://github.com/github/copilot-cli/blob/main/changelog.md
31+
"argument-hint": z.optional(z.string()),
2932
});
3033

3134
export type CopilotcliSkillFrontmatter = z.infer<typeof CopilotcliSkillFrontmatterSchema>;
@@ -129,6 +132,9 @@ export class CopilotcliSkill extends ToolSkill {
129132
...(frontmatter["allowed-tools"] !== undefined && {
130133
"allowed-tools": frontmatter["allowed-tools"],
131134
}),
135+
...(frontmatter["argument-hint"] !== undefined && {
136+
"argument-hint": frontmatter["argument-hint"],
137+
}),
132138
};
133139
const rulesyncFrontmatter: RulesyncSkillFrontmatterInput = {
134140
name: frontmatter.name,
@@ -167,6 +173,9 @@ export class CopilotcliSkill extends ToolSkill {
167173
...(rulesyncFrontmatter.copilotcli?.["allowed-tools"] !== undefined && {
168174
"allowed-tools": rulesyncFrontmatter.copilotcli["allowed-tools"],
169175
}),
176+
...(rulesyncFrontmatter.copilotcli?.["argument-hint"] !== undefined && {
177+
"argument-hint": rulesyncFrontmatter.copilotcli["argument-hint"],
178+
}),
170179
};
171180

172181
return new CopilotcliSkill({

src/features/skills/rulesync-skill.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ const RulesyncSkillFrontmatterSchemaInternal = z.looseObject({
114114
z.looseObject({
115115
license: z.optional(z.string()),
116116
"allowed-tools": z.optional(z.union([z.string(), z.array(z.string())])),
117+
"argument-hint": z.optional(z.string()),
117118
}),
118119
),
119120
pi: z.optional(
@@ -265,6 +266,7 @@ export type RulesyncSkillFrontmatterInput = {
265266
copilotcli?: {
266267
license?: string;
267268
"allowed-tools"?: string | string[];
269+
"argument-hint"?: string;
268270
};
269271
pi?: {
270272
"allowed-tools"?: string[];

0 commit comments

Comments
 (0)