From d14ac57799eb4956898478dcd366fb526fdf9a19 Mon Sep 17 00:00:00 2001 From: flanny7 Date: Wed, 8 Apr 2026 17:24:29 +0900 Subject: [PATCH 1/6] fix: convert .md to .agent.md extension for GitHub Copilot subagents VSCode Copilot Chat requires agent files to have the .agent.md extension to be recognized and loaded. Previously, rulesync generated files as .md which caused them to be silently ignored by Copilot. Fixes dyoshikawa/rulesync#1432 Co-Authored-By: Claude Sonnet 4.6 --- .../subagents/copilot-subagent.test.ts | 50 +++++++++++++++++++ src/features/subagents/copilot-subagent.ts | 14 +++++- 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/src/features/subagents/copilot-subagent.test.ts b/src/features/subagents/copilot-subagent.test.ts index ae4608f52..0fb504b10 100644 --- a/src/features/subagents/copilot-subagent.test.ts +++ b/src/features/subagents/copilot-subagent.test.ts @@ -96,6 +96,56 @@ Plan tasks`; expect(subagent.getFrontmatter().tools).toEqual(["agent/runSubagent"]); }); + + it("converts .md extension to .agent.md in output file path", () => { + const rulesyncSubagent = new RulesyncSubagent({ + baseDir: testDir, + relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, + relativeFilePath: "planner.md", + frontmatter: { + targets: ["copilot"], + name: "planner", + description: "Plan things", + copilot: {}, + }, + body: "Plan tasks", + validate: true, + }); + + const subagent = CopilotSubagent.fromRulesyncSubagent({ + baseDir: testDir, + relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, + rulesyncSubagent, + validate: true, + }) as CopilotSubagent; + + expect(subagent.getRelativeFilePath()).toBe("planner.agent.md"); + }); + + it("preserves .agent.md extension when already present", () => { + const rulesyncSubagent = new RulesyncSubagent({ + baseDir: testDir, + relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, + relativeFilePath: "planner.agent.md", + frontmatter: { + targets: ["copilot"], + name: "planner", + description: "Plan things", + copilot: {}, + }, + body: "Plan tasks", + validate: true, + }); + + const subagent = CopilotSubagent.fromRulesyncSubagent({ + baseDir: testDir, + relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, + rulesyncSubagent, + validate: true, + }) as CopilotSubagent; + + expect(subagent.getRelativeFilePath()).toBe("planner.agent.md"); + }); }); describe("toRulesyncSubagent", () => { diff --git a/src/features/subagents/copilot-subagent.ts b/src/features/subagents/copilot-subagent.ts index 5b4b62a28..f28b4b454 100644 --- a/src/features/subagents/copilot-subagent.ts +++ b/src/features/subagents/copilot-subagent.ts @@ -31,6 +31,18 @@ type CopilotSubagentParams = { body: string; } & AiFileParams; +const AGENT_MD_EXTENSION = ".agent.md"; + +const toAgentMdFilePath = (filePath: string): string => { + if (filePath.endsWith(AGENT_MD_EXTENSION)) { + return filePath; + } + if (filePath.endsWith(".md")) { + return filePath.slice(0, -3) + AGENT_MD_EXTENSION; + } + return filePath + AGENT_MD_EXTENSION; +}; + const normalizeTools = (tools: string | string[] | undefined): string[] => { if (!tools) { return []; @@ -134,7 +146,7 @@ export class CopilotSubagent extends ToolSubagent { frontmatter: copilotFrontmatter, body, relativeDirPath: paths.relativeDirPath, - relativeFilePath: rulesyncSubagent.getRelativeFilePath(), + relativeFilePath: toAgentMdFilePath(rulesyncSubagent.getRelativeFilePath()), fileContent, validate, global, From 70ff8689ceae65c8068894144930b413d4514358 Mon Sep 17 00:00:00 2001 From: flanny7 Date: Wed, 8 Apr 2026 17:30:09 +0900 Subject: [PATCH 2/6] fix: update filePattern and add reverse .agent.md conversion - Change Copilot filePattern from "*.md" to "*.agent.md" so deletion (rulesync generate --delete) correctly finds and removes stale files - Add fromAgentMdFilePath() to convert .agent.md back to .md in toRulesyncSubagent() for correct import paths - Update subagents-processor test to use .agent.md file names Co-Authored-By: Claude Opus 4.6 --- .../subagents/copilot-subagent.test.ts | 19 +++++++++++++++++++ src/features/subagents/copilot-subagent.ts | 9 ++++++++- .../subagents/subagents-processor.test.ts | 2 +- src/features/subagents/subagents-processor.ts | 2 +- 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/features/subagents/copilot-subagent.test.ts b/src/features/subagents/copilot-subagent.test.ts index 0fb504b10..64371cffa 100644 --- a/src/features/subagents/copilot-subagent.test.ts +++ b/src/features/subagents/copilot-subagent.test.ts @@ -174,6 +174,25 @@ Plan tasks`; }); expect(rulesyncSubagent.getBody()).toBe("Plan tasks"); }); + + it("converts .agent.md back to .md for rulesync file path", () => { + const subagent = new CopilotSubagent({ + baseDir: testDir, + relativeDirPath: ".github/agents", + relativeFilePath: "planner.agent.md", + frontmatter: { + name: "planner", + description: "Plan things", + }, + body: "Plan tasks", + fileContent: validContent, + validate: true, + }); + + const rulesyncSubagent = subagent.toRulesyncSubagent(); + + expect(rulesyncSubagent.getRelativeFilePath()).toBe("planner.md"); + }); }); describe("fromFile", () => { diff --git a/src/features/subagents/copilot-subagent.ts b/src/features/subagents/copilot-subagent.ts index f28b4b454..786ee4939 100644 --- a/src/features/subagents/copilot-subagent.ts +++ b/src/features/subagents/copilot-subagent.ts @@ -43,6 +43,13 @@ const toAgentMdFilePath = (filePath: string): string => { return filePath + AGENT_MD_EXTENSION; }; +const fromAgentMdFilePath = (filePath: string): string => { + if (filePath.endsWith(AGENT_MD_EXTENSION)) { + return filePath.slice(0, -AGENT_MD_EXTENSION.length) + ".md"; + } + return filePath; +}; + const normalizeTools = (tools: string | string[] | undefined): string[] => { if (!tools) { return []; @@ -110,7 +117,7 @@ export class CopilotSubagent extends ToolSubagent { frontmatter: rulesyncFrontmatter, body: this.body, relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, - relativeFilePath: this.getRelativeFilePath(), + relativeFilePath: fromAgentMdFilePath(this.getRelativeFilePath()), validate: true, }); } diff --git a/src/features/subagents/subagents-processor.test.ts b/src/features/subagents/subagents-processor.test.ts index 7e4012eb2..67440afb7 100644 --- a/src/features/subagents/subagents-processor.test.ts +++ b/src/features/subagents/subagents-processor.test.ts @@ -646,7 +646,7 @@ description: Copilot agent description --- Copilot agent content`; - await writeFileContent(join(subagentsDir, "copilot-agent.md"), subagentContent); + await writeFileContent(join(subagentsDir, "copilot-agent.agent.md"), subagentContent); const toolFiles = await processor.loadToolFiles(); diff --git a/src/features/subagents/subagents-processor.ts b/src/features/subagents/subagents-processor.ts index 9675b99d2..62f8a0a9a 100644 --- a/src/features/subagents/subagents-processor.ts +++ b/src/features/subagents/subagents-processor.ts @@ -119,7 +119,7 @@ const toolSubagentFactories = new Map Date: Wed, 8 Apr 2026 17:41:23 +0900 Subject: [PATCH 3/6] fix: throw error for non-.md source file paths in toAgentMdFilePath Silently appending .agent.md to a non-.md path (e.g. planner.txt) would produce malformed filenames like planner.txt.agent.md that Copilot Chat would never load. Fail fast with a clear error instead. Co-Authored-By: Claude Sonnet 4.6 --- .../subagents/copilot-subagent.test.ts | 25 +++++++++++++++++++ src/features/subagents/copilot-subagent.ts | 2 +- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/features/subagents/copilot-subagent.test.ts b/src/features/subagents/copilot-subagent.test.ts index 64371cffa..cd8503f79 100644 --- a/src/features/subagents/copilot-subagent.test.ts +++ b/src/features/subagents/copilot-subagent.test.ts @@ -146,6 +146,31 @@ Plan tasks`; expect(subagent.getRelativeFilePath()).toBe("planner.agent.md"); }); + + it("throws when source file path does not end in .md", () => { + const rulesyncSubagent = new RulesyncSubagent({ + baseDir: testDir, + relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, + relativeFilePath: "planner.txt", + frontmatter: { + targets: ["copilot"], + name: "planner", + description: "Plan things", + copilot: {}, + }, + body: "Plan tasks", + validate: true, + }); + + expect(() => + CopilotSubagent.fromRulesyncSubagent({ + baseDir: testDir, + relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH, + rulesyncSubagent, + validate: true, + }), + ).toThrow("Expected .md file path"); + }); }); describe("toRulesyncSubagent", () => { diff --git a/src/features/subagents/copilot-subagent.ts b/src/features/subagents/copilot-subagent.ts index 786ee4939..4f9e03c8e 100644 --- a/src/features/subagents/copilot-subagent.ts +++ b/src/features/subagents/copilot-subagent.ts @@ -40,7 +40,7 @@ const toAgentMdFilePath = (filePath: string): string => { if (filePath.endsWith(".md")) { return filePath.slice(0, -3) + AGENT_MD_EXTENSION; } - return filePath + AGENT_MD_EXTENSION; + throw new Error(`Expected .md file path, got: ${filePath}`); }; const fromAgentMdFilePath = (filePath: string): string => { From 029588d7c85693f6b4ea15ca9ef3c3714a6f9067 Mon Sep 17 00:00:00 2001 From: flanny7 Date: Wed, 8 Apr 2026 18:14:16 +0900 Subject: [PATCH 4/6] docs(hooks): restore and update hooks examples in file-formats.md The hooks Example section was displaced when the .copilot/mcp-config.json section was inserted in commit 73abcff0, splitting the hooks section in two. The override keys description and Example ended up under the copilot section. Changes: - Move override keys paragraph and hooks Example back into .rulesync/hooks.json section - Add Hook definition fields table (command, type, timeout, matcher, prompt, loop_limit, name, description) - Add missing tool event lists: Kilo, Factory Droid, DeepAgents, Codex CLI - Update override keys list to include all 9 supported tools - Update hooks Example to be comprehensive: show all HookDefinitionSchema fields and tool-specific override sections for all supported tools Co-Authored-By: Claude Opus 4.6 --- docs/reference/file-formats.md | 149 ++++++++++++++++++++++++-------- skills/rulesync/file-formats.md | 149 ++++++++++++++++++++++++-------- 2 files changed, 224 insertions(+), 74 deletions(-) diff --git a/docs/reference/file-formats.md b/docs/reference/file-formats.md index 1ec336c2d..10b5a3b7a 100644 --- a/docs/reference/file-formats.md +++ b/docs/reference/file-formats.md @@ -38,21 +38,131 @@ This is Rulesync, a Node.js CLI tool that automatically generates configuration Hooks run scripts at lifecycle events (e.g. session start, before tool use). Events use **canonical camelCase** in this file; Cursor uses them as-is; Claude Code gets PascalCase in `.claude/settings.json`; OpenCode hooks are generated as a JavaScript plugin at `.opencode/plugins/rulesync-hooks.js`; Gemini CLI gets PascalCase (with some specific name mappings) in `.gemini/settings.json`. +**Hook definition fields:** + +| Field | Type | Description | +|-------|------|-------------| +| `command` | string | Shell command to execute (for command-type hooks) | +| `type` | `"command"` \| `"prompt"` | Hook type (default: `"command"`) | +| `timeout` | number | Timeout in milliseconds | +| `matcher` | string | Pattern to match against (e.g. tool names for `preToolUse`/`postToolUse`) | +| `prompt` | string | Prompt text (for prompt-type hooks) | +| `loop_limit` | number \| null | Maximum number of loop iterations (`null` for unlimited) | +| `name` | string | Hook name identifier | +| `description` | string | Human-readable description of the hook | + **Event support:** -- **Cursor:** `sessionStart`, `preToolUse`, `postToolUse`, `stop`, `sessionEnd`, `beforeSubmitPrompt`, `subagentStop`, `preCompact`, `afterFileEdit`, `afterShellExecution`, `postToolUseFailure`, `subagentStart`, `beforeShellExecution`, `beforeMCPExecution`, `afterMCPExecution`, `beforeReadFile`, `afterAgentResponse`, `afterAgentThought`, `beforeTabFileRead`, `afterTabFileEdit` -- **Claude Code:** `sessionStart`, `preToolUse`, `postToolUse`, `stop`, `sessionEnd`, `beforeSubmitPrompt`, `subagentStop`, `preCompact`, `permissionRequest`, `notification`, `setup`, `worktreeCreate`, `worktreeRemove` +- **Cursor:** `sessionStart`, `sessionEnd`, `preToolUse`, `postToolUse`, `beforeSubmitPrompt`, `stop`, `subagentStop`, `preCompact`, `postToolUseFailure`, `subagentStart`, `beforeShellExecution`, `afterShellExecution`, `beforeMCPExecution`, `afterMCPExecution`, `beforeReadFile`, `afterFileEdit`, `afterAgentResponse`, `afterAgentThought`, `beforeTabFileRead`, `afterTabFileEdit` +- **Claude Code:** `sessionStart`, `sessionEnd`, `preToolUse`, `postToolUse`, `beforeSubmitPrompt`, `stop`, `subagentStop`, `preCompact`, `permissionRequest`, `notification`, `setup`, `worktreeCreate`, `worktreeRemove` > **Note:** `worktreeCreate` and `worktreeRemove` are Claude Code-specific events and do not support the `matcher` field. Any matcher defined in the config will be ignored for these events. - **OpenCode:** `sessionStart`, `preToolUse`, `postToolUse`, `stop`, `afterFileEdit`, `afterShellExecution`, `permissionRequest` +- **Kilo:** `sessionStart`, `preToolUse`, `postToolUse`, `stop`, `afterFileEdit`, `afterShellExecution`, `permissionRequest` - **GitHub Copilot:** `sessionStart`, `sessionEnd`, `beforeSubmitPrompt`, `preToolUse`, `postToolUse`, `afterError` - **Gemini CLI:** `sessionStart`, `sessionEnd`, `beforeSubmitPrompt`, `stop`, `beforeAgentResponse`, `afterAgentResponse`, `beforeToolSelection`, `preToolUse`, `postToolUse`, `preCompact`, `notification` +- **Codex CLI:** `sessionStart`, `preToolUse`, `postToolUse`, `beforeSubmitPrompt`, `stop` +- **Factory Droid:** `sessionStart`, `sessionEnd`, `preToolUse`, `postToolUse`, `beforeSubmitPrompt`, `stop`, `subagentStop`, `preCompact`, `permissionRequest`, `notification`, `setup` +- **DeepAgents:** `sessionStart`, `sessionEnd`, `beforeSubmitPrompt`, `permissionRequest`, `postToolUseFailure`, `stop`, `preCompact` > **Note:** Rulesync implements OpenCode hooks as a plugin, so importing from OpenCode to rulesync is not supported. OpenCode only supports command-type hooks (not prompt-type). > **Note:** GitHub Copilot's format uses separate `powershell` and `bash` fields for hooks. Rulesync supports only a single `command` field and resolves this by emitting the command under the `powershell` key on Windows, and under the `bash` key on all other platforms. +Use optional **override keys** so tool-specific events and config live in one file without leaking to others: `cursor.hooks` for Cursor-only events, `claudecode.hooks` for Claude Code-only, `opencode.hooks` for OpenCode-only, `kilo.hooks` for Kilo-only, `copilot.hooks` for GitHub Copilot-only, `geminicli.hooks` for Gemini CLI-only, `codexcli.hooks` for Codex CLI-only, `factorydroid.hooks` for Factory Droid-only, `deepagents.hooks` for DeepAgents-only. Events in shared `hooks` that a tool does not support are skipped for that tool (and a warning is logged at generate time). + +Example: + +```json +{ + "version": 1, + "hooks": { + "sessionStart": [ + { + "type": "command", + "command": ".rulesync/hooks/session-start.sh", + "name": "session-init", + "description": "Initialize session environment" + } + ], + "preToolUse": [ + { + "matcher": "Bash", + "type": "prompt", + "prompt": "Review the command before execution for safety", + "description": "Safety check for shell commands" + } + ], + "postToolUse": [ + { + "matcher": "Write|Edit", + "command": ".rulesync/hooks/format.sh", + "timeout": 10000 + } + ], + "stop": [ + { + "command": ".rulesync/hooks/audit.sh", + "loop_limit": null + } + ] + }, + "cursor": { + "hooks": { + "afterFileEdit": [{ "command": ".cursor/hooks/format.sh" }], + "beforeShellExecution": [{ "command": ".cursor/hooks/pre-shell.sh" }] + } + }, + "claudecode": { + "hooks": { + "notification": [ + { + "matcher": "permission_prompt", + "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/notify.sh" + } + ], + "worktreeCreate": [{ "command": ".claude/hooks/worktree-setup.sh" }] + } + }, + "opencode": { + "hooks": { + "afterShellExecution": [{ "command": ".rulesync/hooks/post-shell.sh" }] + } + }, + "kilo": { + "hooks": { + "afterFileEdit": [{ "command": ".rulesync/hooks/post-edit.sh" }] + } + }, + "copilot": { + "hooks": { + "afterError": [{ "command": ".rulesync/hooks/error-report.sh" }] + } + }, + "geminicli": { + "hooks": { + "beforeAgentResponse": [{ "command": ".rulesync/hooks/pre-response.sh" }] + } + }, + "codexcli": { + "hooks": { + "preToolUse": [{ "matcher": "Bash", "command": ".rulesync/hooks/codex-pre-tool.sh" }] + } + }, + "factorydroid": { + "hooks": { + "setup": [{ "command": ".rulesync/hooks/factory-setup.sh" }] + } + }, + "deepagents": { + "hooks": { + "permissionRequest": [{ "command": ".rulesync/hooks/permission-check.sh" }] + } + } +} +``` + ## `.copilot/mcp-config.json` Example: @@ -85,41 +195,6 @@ This file is used by the GitHub Copilot CLI for MCP server configuration. Rulesy Rulesync preserves explicit `type` values for `http`, `sse`, and `local` servers. For command-based servers that omit a transport type, Rulesync emits the mandatory `"type": "stdio"` field required by the Copilot CLI. -Use optional **override keys** so tool-specific events and config live in one file without leaking to others: `cursor.hooks` for Cursor-only events, `claudecode.hooks` for Claude-only, `opencode.hooks` for OpenCode-only, `copilot.hooks` for GitHub Copilot-only, `geminicli.hooks` for Gemini CLI-only. Events in shared `hooks` that a tool does not support are skipped for that tool (and a warning is logged at generate time). - -Example: - -```json -{ - "version": 1, - "hooks": { - "sessionStart": [{ "type": "command", "command": ".rulesync/hooks/session-start.sh" }], - "postToolUse": [{ "matcher": "Write|Edit", "command": ".rulesync/hooks/format.sh" }], - "stop": [{ "command": ".rulesync/hooks/audit.sh" }] - }, - "cursor": { - "hooks": { - "afterFileEdit": [{ "command": ".cursor/hooks/format.sh" }] - } - }, - "claudecode": { - "hooks": { - "notification": [ - { - "matcher": "permission_prompt", - "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/notify.sh" - } - ] - } - }, - "opencode": { - "hooks": { - "afterShellExecution": [{ "command": ".rulesync/hooks/post-shell.sh" }] - } - } -} -``` - ## `rulesync/commands/*.md` Example: diff --git a/skills/rulesync/file-formats.md b/skills/rulesync/file-formats.md index 1ec336c2d..10b5a3b7a 100644 --- a/skills/rulesync/file-formats.md +++ b/skills/rulesync/file-formats.md @@ -38,21 +38,131 @@ This is Rulesync, a Node.js CLI tool that automatically generates configuration Hooks run scripts at lifecycle events (e.g. session start, before tool use). Events use **canonical camelCase** in this file; Cursor uses them as-is; Claude Code gets PascalCase in `.claude/settings.json`; OpenCode hooks are generated as a JavaScript plugin at `.opencode/plugins/rulesync-hooks.js`; Gemini CLI gets PascalCase (with some specific name mappings) in `.gemini/settings.json`. +**Hook definition fields:** + +| Field | Type | Description | +|-------|------|-------------| +| `command` | string | Shell command to execute (for command-type hooks) | +| `type` | `"command"` \| `"prompt"` | Hook type (default: `"command"`) | +| `timeout` | number | Timeout in milliseconds | +| `matcher` | string | Pattern to match against (e.g. tool names for `preToolUse`/`postToolUse`) | +| `prompt` | string | Prompt text (for prompt-type hooks) | +| `loop_limit` | number \| null | Maximum number of loop iterations (`null` for unlimited) | +| `name` | string | Hook name identifier | +| `description` | string | Human-readable description of the hook | + **Event support:** -- **Cursor:** `sessionStart`, `preToolUse`, `postToolUse`, `stop`, `sessionEnd`, `beforeSubmitPrompt`, `subagentStop`, `preCompact`, `afterFileEdit`, `afterShellExecution`, `postToolUseFailure`, `subagentStart`, `beforeShellExecution`, `beforeMCPExecution`, `afterMCPExecution`, `beforeReadFile`, `afterAgentResponse`, `afterAgentThought`, `beforeTabFileRead`, `afterTabFileEdit` -- **Claude Code:** `sessionStart`, `preToolUse`, `postToolUse`, `stop`, `sessionEnd`, `beforeSubmitPrompt`, `subagentStop`, `preCompact`, `permissionRequest`, `notification`, `setup`, `worktreeCreate`, `worktreeRemove` +- **Cursor:** `sessionStart`, `sessionEnd`, `preToolUse`, `postToolUse`, `beforeSubmitPrompt`, `stop`, `subagentStop`, `preCompact`, `postToolUseFailure`, `subagentStart`, `beforeShellExecution`, `afterShellExecution`, `beforeMCPExecution`, `afterMCPExecution`, `beforeReadFile`, `afterFileEdit`, `afterAgentResponse`, `afterAgentThought`, `beforeTabFileRead`, `afterTabFileEdit` +- **Claude Code:** `sessionStart`, `sessionEnd`, `preToolUse`, `postToolUse`, `beforeSubmitPrompt`, `stop`, `subagentStop`, `preCompact`, `permissionRequest`, `notification`, `setup`, `worktreeCreate`, `worktreeRemove` > **Note:** `worktreeCreate` and `worktreeRemove` are Claude Code-specific events and do not support the `matcher` field. Any matcher defined in the config will be ignored for these events. - **OpenCode:** `sessionStart`, `preToolUse`, `postToolUse`, `stop`, `afterFileEdit`, `afterShellExecution`, `permissionRequest` +- **Kilo:** `sessionStart`, `preToolUse`, `postToolUse`, `stop`, `afterFileEdit`, `afterShellExecution`, `permissionRequest` - **GitHub Copilot:** `sessionStart`, `sessionEnd`, `beforeSubmitPrompt`, `preToolUse`, `postToolUse`, `afterError` - **Gemini CLI:** `sessionStart`, `sessionEnd`, `beforeSubmitPrompt`, `stop`, `beforeAgentResponse`, `afterAgentResponse`, `beforeToolSelection`, `preToolUse`, `postToolUse`, `preCompact`, `notification` +- **Codex CLI:** `sessionStart`, `preToolUse`, `postToolUse`, `beforeSubmitPrompt`, `stop` +- **Factory Droid:** `sessionStart`, `sessionEnd`, `preToolUse`, `postToolUse`, `beforeSubmitPrompt`, `stop`, `subagentStop`, `preCompact`, `permissionRequest`, `notification`, `setup` +- **DeepAgents:** `sessionStart`, `sessionEnd`, `beforeSubmitPrompt`, `permissionRequest`, `postToolUseFailure`, `stop`, `preCompact` > **Note:** Rulesync implements OpenCode hooks as a plugin, so importing from OpenCode to rulesync is not supported. OpenCode only supports command-type hooks (not prompt-type). > **Note:** GitHub Copilot's format uses separate `powershell` and `bash` fields for hooks. Rulesync supports only a single `command` field and resolves this by emitting the command under the `powershell` key on Windows, and under the `bash` key on all other platforms. +Use optional **override keys** so tool-specific events and config live in one file without leaking to others: `cursor.hooks` for Cursor-only events, `claudecode.hooks` for Claude Code-only, `opencode.hooks` for OpenCode-only, `kilo.hooks` for Kilo-only, `copilot.hooks` for GitHub Copilot-only, `geminicli.hooks` for Gemini CLI-only, `codexcli.hooks` for Codex CLI-only, `factorydroid.hooks` for Factory Droid-only, `deepagents.hooks` for DeepAgents-only. Events in shared `hooks` that a tool does not support are skipped for that tool (and a warning is logged at generate time). + +Example: + +```json +{ + "version": 1, + "hooks": { + "sessionStart": [ + { + "type": "command", + "command": ".rulesync/hooks/session-start.sh", + "name": "session-init", + "description": "Initialize session environment" + } + ], + "preToolUse": [ + { + "matcher": "Bash", + "type": "prompt", + "prompt": "Review the command before execution for safety", + "description": "Safety check for shell commands" + } + ], + "postToolUse": [ + { + "matcher": "Write|Edit", + "command": ".rulesync/hooks/format.sh", + "timeout": 10000 + } + ], + "stop": [ + { + "command": ".rulesync/hooks/audit.sh", + "loop_limit": null + } + ] + }, + "cursor": { + "hooks": { + "afterFileEdit": [{ "command": ".cursor/hooks/format.sh" }], + "beforeShellExecution": [{ "command": ".cursor/hooks/pre-shell.sh" }] + } + }, + "claudecode": { + "hooks": { + "notification": [ + { + "matcher": "permission_prompt", + "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/notify.sh" + } + ], + "worktreeCreate": [{ "command": ".claude/hooks/worktree-setup.sh" }] + } + }, + "opencode": { + "hooks": { + "afterShellExecution": [{ "command": ".rulesync/hooks/post-shell.sh" }] + } + }, + "kilo": { + "hooks": { + "afterFileEdit": [{ "command": ".rulesync/hooks/post-edit.sh" }] + } + }, + "copilot": { + "hooks": { + "afterError": [{ "command": ".rulesync/hooks/error-report.sh" }] + } + }, + "geminicli": { + "hooks": { + "beforeAgentResponse": [{ "command": ".rulesync/hooks/pre-response.sh" }] + } + }, + "codexcli": { + "hooks": { + "preToolUse": [{ "matcher": "Bash", "command": ".rulesync/hooks/codex-pre-tool.sh" }] + } + }, + "factorydroid": { + "hooks": { + "setup": [{ "command": ".rulesync/hooks/factory-setup.sh" }] + } + }, + "deepagents": { + "hooks": { + "permissionRequest": [{ "command": ".rulesync/hooks/permission-check.sh" }] + } + } +} +``` + ## `.copilot/mcp-config.json` Example: @@ -85,41 +195,6 @@ This file is used by the GitHub Copilot CLI for MCP server configuration. Rulesy Rulesync preserves explicit `type` values for `http`, `sse`, and `local` servers. For command-based servers that omit a transport type, Rulesync emits the mandatory `"type": "stdio"` field required by the Copilot CLI. -Use optional **override keys** so tool-specific events and config live in one file without leaking to others: `cursor.hooks` for Cursor-only events, `claudecode.hooks` for Claude-only, `opencode.hooks` for OpenCode-only, `copilot.hooks` for GitHub Copilot-only, `geminicli.hooks` for Gemini CLI-only. Events in shared `hooks` that a tool does not support are skipped for that tool (and a warning is logged at generate time). - -Example: - -```json -{ - "version": 1, - "hooks": { - "sessionStart": [{ "type": "command", "command": ".rulesync/hooks/session-start.sh" }], - "postToolUse": [{ "matcher": "Write|Edit", "command": ".rulesync/hooks/format.sh" }], - "stop": [{ "command": ".rulesync/hooks/audit.sh" }] - }, - "cursor": { - "hooks": { - "afterFileEdit": [{ "command": ".cursor/hooks/format.sh" }] - } - }, - "claudecode": { - "hooks": { - "notification": [ - { - "matcher": "permission_prompt", - "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/notify.sh" - } - ] - } - }, - "opencode": { - "hooks": { - "afterShellExecution": [{ "command": ".rulesync/hooks/post-shell.sh" }] - } - } -} -``` - ## `rulesync/commands/*.md` Example: From d961b09b8ba2eed190f866bf83d7aef0fad893ab Mon Sep 17 00:00:00 2001 From: flanny7 Date: Wed, 8 Apr 2026 18:40:39 +0900 Subject: [PATCH 5/6] docs(hooks): simplify example to show only tool-unique keys, add output transformations table Remove tool-specific override sections from Example that only showed regular events. Keep claudecode override only (worktreeCreate is a unique event). Add Tool-specific output transformations table documenting unique output keys/behavior for each tool (Copilot bash/powershell, Gemini matcher grouping, DeepAgents array command format, etc.). Co-Authored-By: Claude Opus 4.6 --- docs/reference/file-formats.md | 61 ++++++++------------------------- skills/rulesync/file-formats.md | 61 ++++++++------------------------- 2 files changed, 28 insertions(+), 94 deletions(-) diff --git a/docs/reference/file-formats.md b/docs/reference/file-formats.md index 10b5a3b7a..a7712371f 100644 --- a/docs/reference/file-formats.md +++ b/docs/reference/file-formats.md @@ -108,61 +108,28 @@ Example: } ] }, - "cursor": { - "hooks": { - "afterFileEdit": [{ "command": ".cursor/hooks/format.sh" }], - "beforeShellExecution": [{ "command": ".cursor/hooks/pre-shell.sh" }] - } - }, "claudecode": { "hooks": { - "notification": [ - { - "matcher": "permission_prompt", - "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/notify.sh" - } - ], "worktreeCreate": [{ "command": ".claude/hooks/worktree-setup.sh" }] } - }, - "opencode": { - "hooks": { - "afterShellExecution": [{ "command": ".rulesync/hooks/post-shell.sh" }] - } - }, - "kilo": { - "hooks": { - "afterFileEdit": [{ "command": ".rulesync/hooks/post-edit.sh" }] - } - }, - "copilot": { - "hooks": { - "afterError": [{ "command": ".rulesync/hooks/error-report.sh" }] - } - }, - "geminicli": { - "hooks": { - "beforeAgentResponse": [{ "command": ".rulesync/hooks/pre-response.sh" }] - } - }, - "codexcli": { - "hooks": { - "preToolUse": [{ "matcher": "Bash", "command": ".rulesync/hooks/codex-pre-tool.sh" }] - } - }, - "factorydroid": { - "hooks": { - "setup": [{ "command": ".rulesync/hooks/factory-setup.sh" }] - } - }, - "deepagents": { - "hooks": { - "permissionRequest": [{ "command": ".rulesync/hooks/permission-check.sh" }] - } } } ``` +> **Note:** The `claudecode.hooks` override is shown above because `worktreeCreate` and `worktreeRemove` are Claude Code-specific events that do not support the `matcher` field. Use tool-specific override keys only for events exclusive to that tool; shared events belong in the top-level `hooks` section. + +**Tool-specific output transformations:** + +| Tool | Unique output keys / behavior | +|------|-------------------------------| +| **GitHub Copilot** | `command` is emitted as `bash` (non-Windows) or `powershell` (Windows); `timeout` is emitted as `timeoutSec` | +| **Gemini CLI** | Hooks are grouped by `matcher` in output; relative commands (starting with `./`) are prefixed with `$GEMINI_PROJECT_DIR/` | +| **Claude Code** | Relative commands are prefixed with `$CLAUDE_PROJECT_DIR/`; `worktreeCreate`/`worktreeRemove` ignore `matcher` | +| **Factory Droid** | Relative commands are prefixed with `$FACTORY_PROJECT_DIR/` | +| **OpenCode / Kilo** | Generated as a JavaScript plugin file (not JSON); only `command`-type hooks are supported | +| **Codex CLI** | Hooks are grouped by `matcher` in output; only `command`-type hooks; generates `.codex/config.toml` with feature flag | +| **DeepAgents** | Output uses flat array structure; `command` is emitted as `["bash", "-c", "..."]`; `matcher` is not supported | + ## `.copilot/mcp-config.json` Example: diff --git a/skills/rulesync/file-formats.md b/skills/rulesync/file-formats.md index 10b5a3b7a..a7712371f 100644 --- a/skills/rulesync/file-formats.md +++ b/skills/rulesync/file-formats.md @@ -108,61 +108,28 @@ Example: } ] }, - "cursor": { - "hooks": { - "afterFileEdit": [{ "command": ".cursor/hooks/format.sh" }], - "beforeShellExecution": [{ "command": ".cursor/hooks/pre-shell.sh" }] - } - }, "claudecode": { "hooks": { - "notification": [ - { - "matcher": "permission_prompt", - "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/notify.sh" - } - ], "worktreeCreate": [{ "command": ".claude/hooks/worktree-setup.sh" }] } - }, - "opencode": { - "hooks": { - "afterShellExecution": [{ "command": ".rulesync/hooks/post-shell.sh" }] - } - }, - "kilo": { - "hooks": { - "afterFileEdit": [{ "command": ".rulesync/hooks/post-edit.sh" }] - } - }, - "copilot": { - "hooks": { - "afterError": [{ "command": ".rulesync/hooks/error-report.sh" }] - } - }, - "geminicli": { - "hooks": { - "beforeAgentResponse": [{ "command": ".rulesync/hooks/pre-response.sh" }] - } - }, - "codexcli": { - "hooks": { - "preToolUse": [{ "matcher": "Bash", "command": ".rulesync/hooks/codex-pre-tool.sh" }] - } - }, - "factorydroid": { - "hooks": { - "setup": [{ "command": ".rulesync/hooks/factory-setup.sh" }] - } - }, - "deepagents": { - "hooks": { - "permissionRequest": [{ "command": ".rulesync/hooks/permission-check.sh" }] - } } } ``` +> **Note:** The `claudecode.hooks` override is shown above because `worktreeCreate` and `worktreeRemove` are Claude Code-specific events that do not support the `matcher` field. Use tool-specific override keys only for events exclusive to that tool; shared events belong in the top-level `hooks` section. + +**Tool-specific output transformations:** + +| Tool | Unique output keys / behavior | +|------|-------------------------------| +| **GitHub Copilot** | `command` is emitted as `bash` (non-Windows) or `powershell` (Windows); `timeout` is emitted as `timeoutSec` | +| **Gemini CLI** | Hooks are grouped by `matcher` in output; relative commands (starting with `./`) are prefixed with `$GEMINI_PROJECT_DIR/` | +| **Claude Code** | Relative commands are prefixed with `$CLAUDE_PROJECT_DIR/`; `worktreeCreate`/`worktreeRemove` ignore `matcher` | +| **Factory Droid** | Relative commands are prefixed with `$FACTORY_PROJECT_DIR/` | +| **OpenCode / Kilo** | Generated as a JavaScript plugin file (not JSON); only `command`-type hooks are supported | +| **Codex CLI** | Hooks are grouped by `matcher` in output; only `command`-type hooks; generates `.codex/config.toml` with feature flag | +| **DeepAgents** | Output uses flat array structure; `command` is emitted as `["bash", "-c", "..."]`; `matcher` is not supported | + ## `.copilot/mcp-config.json` Example: From adeed61aa6f38d148e1f1444ff2f0c2f4a675f31 Mon Sep 17 00:00:00 2001 From: flanny7 Date: Wed, 8 Apr 2026 18:53:17 +0900 Subject: [PATCH 6/6] docs(hooks): add field support matrix by tool with unsupported fields Add a cross-reference table showing which canonical hook definition fields are supported or dropped by each tool. Includes footnotes for partial support (e.g. command-only type, hardcoded timeout, worktree matcher exceptions). Based on both implementation code and official tool docs. Co-Authored-By: Claude Opus 4.6 --- docs/reference/file-formats.md | 20 ++++++++++++++++++++ skills/rulesync/file-formats.md | 20 ++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/docs/reference/file-formats.md b/docs/reference/file-formats.md index a7712371f..2e1c8912c 100644 --- a/docs/reference/file-formats.md +++ b/docs/reference/file-formats.md @@ -118,6 +118,26 @@ Example: > **Note:** The `claudecode.hooks` override is shown above because `worktreeCreate` and `worktreeRemove` are Claude Code-specific events that do not support the `matcher` field. Use tool-specific override keys only for events exclusive to that tool; shared events belong in the top-level `hooks` section. +**Field support by tool:** + +Not all tools support every hook definition field. Fields listed as unsupported are silently dropped during generation. + +| Field | Cursor | Claude Code | Copilot | Gemini CLI | OpenCode | Kilo | Codex CLI | Factory Droid | DeepAgents | +|-------|--------|-------------|---------|------------|----------|------|-----------|---------------|------------| +| `command` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| `type` | ✓ | ✓ | ✗ ^1 | ✓ | ✗ ^1 | ✗ ^1 | ✗ ^1 | ✓ | ✗ ^1 | +| `timeout` | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ | ✓ | ✓ | ✗ ^2 | +| `matcher` | ✓ | ✓ ^3 | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | +| `prompt` | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ ^4 | ✗ | +| `loop_limit` | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | +| `name` | ✗ | ✗ | ✗ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | +| `description` | ✗ | ✗ | ✗ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | + +1. Only `"command"` type is supported; `"prompt"` type hooks are skipped. +2. Hardcoded to 5 seconds; custom timeout is not supported. +3. Except `worktreeCreate`/`worktreeRemove` events, which ignore `matcher`. +4. Rulesync outputs the field, but Factory Droid only supports `"command"` type natively. + **Tool-specific output transformations:** | Tool | Unique output keys / behavior | diff --git a/skills/rulesync/file-formats.md b/skills/rulesync/file-formats.md index a7712371f..2e1c8912c 100644 --- a/skills/rulesync/file-formats.md +++ b/skills/rulesync/file-formats.md @@ -118,6 +118,26 @@ Example: > **Note:** The `claudecode.hooks` override is shown above because `worktreeCreate` and `worktreeRemove` are Claude Code-specific events that do not support the `matcher` field. Use tool-specific override keys only for events exclusive to that tool; shared events belong in the top-level `hooks` section. +**Field support by tool:** + +Not all tools support every hook definition field. Fields listed as unsupported are silently dropped during generation. + +| Field | Cursor | Claude Code | Copilot | Gemini CLI | OpenCode | Kilo | Codex CLI | Factory Droid | DeepAgents | +|-------|--------|-------------|---------|------------|----------|------|-----------|---------------|------------| +| `command` | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | ✓ | +| `type` | ✓ | ✓ | ✗ ^1 | ✓ | ✗ ^1 | ✗ ^1 | ✗ ^1 | ✓ | ✗ ^1 | +| `timeout` | ✓ | ✓ | ✓ | ✓ | ✗ | ✗ | ✓ | ✓ | ✗ ^2 | +| `matcher` | ✓ | ✓ ^3 | ✗ | ✓ | ✓ | ✓ | ✓ | ✓ | ✗ | +| `prompt` | ✓ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ ^4 | ✗ | +| `loop_limit` | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | +| `name` | ✗ | ✗ | ✗ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | +| `description` | ✗ | ✗ | ✗ | ✓ | ✗ | ✗ | ✗ | ✗ | ✗ | + +1. Only `"command"` type is supported; `"prompt"` type hooks are skipped. +2. Hardcoded to 5 seconds; custom timeout is not supported. +3. Except `worktreeCreate`/`worktreeRemove` events, which ignore `matcher`. +4. Rulesync outputs the field, but Factory Droid only supports `"command"` type natively. + **Tool-specific output transformations:** | Tool | Unique output keys / behavior |