Skip to content
Merged
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
8 changes: 4 additions & 4 deletions docs/reference/file-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ Multiple files can set `root: true` for the same target in project and global mo

Hermes Agent accepts native snake-case events under `hermesagent.hooks`: `pre_tool_call`, `post_tool_call`, `transform_terminal_output`, `transform_tool_result`, `transform_llm_output`, `pre_llm_call`, `post_llm_call`, `pre_verify`, `pre_api_request`, `post_api_request`, `api_request_error`, `on_session_start`, `on_session_end`, `on_session_finalize`, `on_session_reset`, `subagent_start`, `subagent_stop`, `pre_gateway_dispatch`, `pre_approval_request`, `post_approval_response`, `kanban_task_claimed`, `kanban_task_completed`, and `kanban_task_blocked`. Rulesync maps shared canonical events first, applies canonical keys from `hermesagent.hooks` next, then applies exact native keys last. An exact native key therefore wins when both forms resolve to the same Hermes event. Native-only events remain under `hermesagent.hooks` on import instead of leaking into other targets. Rulesync owns the event keys inside the `hooks:` mapping of `config.yaml`, but not the mapping itself: Hermes v0.20.0 nests the [outbound webhook registry](https://hermes-agent.nousresearch.com/docs/user-guide/features/hooks) under the same key as `hooks.outbound`, so any key there that is not a Hermes hook event is carried over from the existing file untouched. Rulesync neither authors nor imports `outbound`, since it is a list of webhook targets rather than a hook event; it only makes sure a regenerate leaves it alone. An event key Rulesync did write, including one under an undocumented event name supplied through `hermesagent.hooks`, is still retracted when it disappears from the source.

Hooks run scripts at lifecycle events (e.g. session start, before tool use). Events use **canonical camelCase** in this file, and Rulesync translates them per tool: Cursor uses them as-is; Claude Code, Factory Droid, Codex CLI, Qwen Code, and Goose get PascalCase (with a few tool-specific name mappings) in their settings files; OpenCode and Kilo hooks are emitted as JavaScript plugins (`.opencode/plugins/rulesync-hooks.js`, `.kilo/plugins/rulesync-hooks.js`) — both share one event surface apart from `notification` (see below), in which `preToolUse`/`postToolUse` become named `tool.execute.before`/`tool.execute.after` hooks, `preCompact` becomes the named `experimental.session.compacting` hook and `beforeSubmitPrompt` the named `chat.message` hook (both receive `(input, output)` and expose nothing to match on, so a `matcher` on either is dropped), `beforeShellExecution`/`afterShellExecution` also land in those named `tool.execute.*` hooks with an implicit `input.tool === "bash"` gate — OpenCode has no shell-execution lifecycle event (`command.executed`, which earlier Rulesync versions mapped `afterShellExecution` to, is a _slash-command_ event, so the hook never fired on shell commands; regenerate to fix), and matchers on the shell events are dropped with a warning since the named hooks expose no command text, and the rest are `event.type` dispatches — `sessionStart` → `session.created`, `stop` → `session.idle`, `afterFileEdit` → `file.edited`, `permissionRequest` → `permission.asked`, `permissionDenied` → `permission.replied` (which fires for every reply, so the generated handler is gated on `event.properties.reply === "reject"`), `notification` → `tui.toast.show` (**OpenCode only** — Kilo's plugin docs document no TUI events, so `notification` is not part of its surface; note too that OpenCode's toast channel is broader than the canonical event, since every `info`/`success`/`warning`/`error` toast fires the hook rather than only the ones asking for your attention, and most toasts originate in the TUI client, so a headless `opencode run` rarely fires it at all), `postCompact` → `session.compacted`, `afterError` → `session.error`, `fileChanged` → `file.watcher.updated`; Amp hooks are emitted as a TypeScript plugin (`.amp/plugins/rulesync-hooks.ts`, or `~/.config/amp/plugins/rulesync-hooks.ts` in global mode) using `session.start`, `tool.call`, `tool.result`, `agent.start`, and `agent.end`; Pi Coding Agent hooks are emitted as a Rulesync-owned TypeScript extension (`.pi/extensions/rulesync-hooks.ts`, or `~/.pi/agent/extensions/rulesync-hooks.ts` in global mode) that subscribes to Pi's snake_case extension events (`sessionStart` → `session_start`, `stop` → `agent_end`, `preToolUse` → `tool_call` with the matcher tested as a regex against the tool name, `preCompact` → `session_before_compact`, `postCompact` → `session_compact`, `postModelInvocation` → `message_end` gated on assistant messages so it runs once per finalized model response) and observes events only — command hooks run but cannot block or mutate Pi events; Copilot and Copilot CLI map event names to their own camelCase (e.g. `beforeSubmitPrompt` → `userPromptSubmitted`, `stop` → `agentStop`, `afterError` → `errorOccurred`) and use `powershell`/`bash` command fields — Copilot CLI additionally covers a wider event set and supports `prompt` and `http` hook types beyond `command`; deepagents-cli gets the Hooks v2 PascalCase `HookEvent` names (e.g. `SessionStart`, `PostToolUseFailure`) in a `{ "hooks": { "<Event>": [{ "matcher": …, "hooks": [{ "type": "command", … }] }] } }` document — this requires deepagents-code 0.1.52+, the release where Hooks v2 became generally available (the legacy flat list is removed upstream on 2026-09-01; Rulesync still imports the legacy format but no longer writes it); `kiro-cli` and `kiro-ide` emit hooks into the standalone `.kiro/hooks/rulesync.json` with PascalCase triggers, while the deprecated `kiro` alias still writes them into `.kiro/agents/default.json` using the older event names (`agentSpawn`, `userPromptSubmit`, `preToolUse`, `postToolUse`, `stop`); Qwen Code emits PascalCase events into the `hooks` key of `.qwen/settings.json` (its supported event set differs from Gemini CLI's).
Hooks run scripts at lifecycle events (e.g. session start, before tool use). Events use **canonical camelCase** in this file, and Rulesync translates them per tool: Cursor uses them as-is; Claude Code, Factory Droid, Codex CLI, Qwen Code, and Goose get PascalCase (with a few tool-specific name mappings) in their settings files; OpenCode and Kilo hooks are emitted as JavaScript plugins (`.opencode/plugins/rulesync-hooks.js`, `.kilo/plugins/rulesync-hooks.js`) — both share one event surface apart from `notification` (see below), in which `preToolUse`/`postToolUse` become named `tool.execute.before`/`tool.execute.after` hooks, `preCompact` becomes the named `experimental.session.compacting` hook and `beforeSubmitPrompt` the named `chat.message` hook (both receive `(input, output)` and expose nothing to match on, so a `matcher` on either is dropped), `beforeShellExecution`/`afterShellExecution` also land in those named `tool.execute.*` hooks with an implicit `input.tool === "bash"` gate — OpenCode has no shell-execution lifecycle event (`command.executed`, which earlier Rulesync versions mapped `afterShellExecution` to, is a _slash-command_ event, so the hook never fired on shell commands; regenerate to fix), and matchers on the shell events are dropped with a warning since the named hooks expose no command text, and the rest are `event.type` dispatches — `sessionStart` → `session.created`, `stop` → `session.idle`, `afterFileEdit` → `file.edited`, `permissionRequest` → `permission.asked`, `permissionDenied` → `permission.replied` (which fires for every reply, so the generated handler is gated on `event.properties.reply === "reject"`), `notification` → `tui.toast.show` (**OpenCode only** — Kilo's plugin docs document no TUI events, so `notification` is not part of its surface; note too that OpenCode's toast channel is broader than the canonical event, since every `info`/`success`/`warning`/`error` toast fires the hook rather than only the ones asking for your attention, and most toasts originate in the TUI client, so a headless `opencode run` rarely fires it at all), `postCompact` → `session.compacted`, `afterError` → `session.error`, `fileChanged` → `file.watcher.updated`; Amp hooks are emitted as a TypeScript plugin (`.amp/plugins/rulesync-hooks.ts`, or `~/.config/amp/plugins/rulesync-hooks.ts` in global mode) using `session.start`, `tool.call`, `tool.result`, `agent.start`, and `agent.end`; Pi Coding Agent hooks are emitted as a Rulesync-owned TypeScript extension (`.pi/extensions/rulesync-hooks.ts`, or `~/.pi/agent/extensions/rulesync-hooks.ts` in global mode) that subscribes to Pi's snake_case extension events (`sessionStart` → `session_start`, `stop` → `agent_end`, `preToolUse` → `tool_call` with the matcher tested as a regex against the tool name, `preCompact` → `session_before_compact`, `postCompact` → `session_compact`, `postModelInvocation` → `message_end` gated on assistant messages so it runs once per finalized model response) and observes events only — command hooks run but cannot block or mutate Pi events; Copilot and Copilot CLI map event names to their own camelCase (e.g. `beforeSubmitPrompt` → `userPromptSubmitted`, `stop` → `agentStop`, `afterError` → `errorOccurred`) and write the command into the `bash`/`powershell` field named by the canonical `shell` selector, or into the portable `command` field when none is set — Copilot CLI additionally covers a wider event set and supports `prompt` and `http` hook types beyond `command`; deepagents-cli gets the Hooks v2 PascalCase `HookEvent` names (e.g. `SessionStart`, `PostToolUseFailure`) in a `{ "hooks": { "<Event>": [{ "matcher": …, "hooks": [{ "type": "command", … }] }] } }` document — this requires deepagents-code 0.1.52+, the release where Hooks v2 became generally available (the legacy flat list is removed upstream on 2026-09-01; Rulesync still imports the legacy format but no longer writes it); `kiro-cli` and `kiro-ide` emit hooks into the standalone `.kiro/hooks/rulesync.json` with PascalCase triggers, while the deprecated `kiro` alias still writes them into `.kiro/agents/default.json` using the older event names (`agentSpawn`, `userPromptSubmit`, `preToolUse`, `postToolUse`, `stop`); Qwen Code emits PascalCase events into the `hooks` key of `.qwen/settings.json` (its supported event set differs from Gemini CLI's).

Example:

Expand Down Expand Up @@ -174,7 +174,7 @@ Example:
- `commandRegex` (optional): Regex applied to the shell command string, narrowing an `Execute` matcher group further (e.g. `"^git "`). Forwarded to Factory Droid, which skips invalid regex values. Like `matcher`, it belongs to the whole matcher group, so every hook sharing that matcher receives it.
- `async` (optional): Boolean. When `true`, the hook command runs in the background without blocking. Forwarded to Qwen Code (`.qwen/settings.json`) and JetBrains Junie (`~/.junie/config.json`, same field name).
- `env` (optional, `command` hooks): a map of extra environment variables merged into the hook process's environment. Forwarded to Qwen Code (`.qwen/settings.json`), Copilot CLI, and Grok CLI (`.grok/hooks/rulesync.json`, upstream `HookConfig.env`, merged into the spawned command's `extra_env`). Documented on command hooks only, so it is neither emitted on a hook of another type nor imported from one (a value found there is dropped with a warning). For Grok CLI, an entry whose key is empty or contains `=`, or whose key or value contains a newline, carriage return or NUL, is refused in both directions — the tool rebuilds each entry into a `KEY=VALUE` string, so such a key would name a different variable than it appears to.
- `shell` (optional): Either `"bash"` or `"powershell"` — the only two interpreter values any tool accepts. Forwarded to Qwen Code and Claude Code command hooks. Like `args`, `async` and `asyncRewake`, it is documented on command hooks only, so it is neither emitted on a hook of another type nor imported from one (a value found there is dropped with a warning).
- `shell` (optional): Either `"bash"` or `"powershell"` — the only two interpreter values any tool accepts. Forwarded to Qwen Code, Claude Code, Copilot and Copilot CLI command hooks; for the two Copilot targets it names the `bash`/`powershell` field the command is written into, and leaving it unset selects their portable `command` field. Like `args`, `async` and `asyncRewake`, it is documented on command hooks only, so it is neither emitted on a hook of another type nor imported from one (a value found there is dropped with a warning).
- `url` / `headers` / `allowedEnvVars` (optional, `http` hooks): the POST target URL, request headers (values support `$VAR` interpolation), and the env-var allowlist for that interpolation. Forwarded to Claude Code and Qwen Code http hooks.
- `server` / `tool` / `input` (optional, `mcp_tool` hooks): the configured MCP server name, the tool to call on it, and the (arbitrary JSON) arguments, whose string values support `${path}` substitution from the hook input. Forwarded to Claude Code mcp_tool hooks.
- `model` (optional, `prompt` / `agent` hooks): the model used for evaluation (defaults to a fast model). Forwarded to Claude Code prompt/agent hooks and to Qwen Code prompt hooks.
Expand Down Expand Up @@ -259,11 +259,11 @@ Events present in the shared `hooks` block but unsupported by a given tool are s

> **Amp command syntax:** Amp executes plugin commands with [Bun Shell](https://bun.com/docs/runtime/shell), whose syntax differs slightly from POSIX shells. Use `$VAR` for environment expansion (`${VAR}` remains literal) and `$(command)` for command substitution (backticks remain literal). Rulesync passes the authored command through unchanged so quoting and escaped operators retain their Bun Shell meaning.

> **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.
> **Note:** GitHub Copilot's format uses separate `powershell` and `bash` fields for hooks, plus a portable `command` field that upstream copies into both when neither is present. Rulesync picks between them with the canonical `shell` selector, and writes the portable `command` field when a hook does not set one. Earlier versions chose the field from the platform Rulesync happened to run on; regenerate to get a machine-independent file.

> **Note:** Hook file paths per tool:
>
> - **Copilot (cloud agent / VS Code)** — project: `<project>/.github/hooks/copilot-hooks.json`; global: `~/.copilot/hooks/copilot-ide-hooks.json`. VS Code and the coding agent both document `~/.copilot/hooks` as the user scope and load every `*.json` in that folder; the Copilot CLI's global file already occupies `copilot-hooks.json` there, so the VS Code target uses a distinct filename and the two never overwrite each other. Note the flip side of "every `*.json` is loaded": generating **both** `copilot` and `copilotcli` in global mode leaves two files in that one folder, and a reader of the folder runs the hooks from both — so a command present in your canonical config fires twice per event. Generate only one of the two globally unless you want that.
> - **Copilot (cloud agent / VS Code)** — project: `<project>/.github/hooks/copilot-hooks.json`; global: `~/.copilot/hooks/copilot-ide-hooks.json`. Command hooks carry `bash`/`powershell` with optional `timeoutSec`, plus the canonical `env` map. Which command field is written is chosen by the canonical `shell` selector; without it the portable `command` field is written, which upstream copies to both. It is deliberately **not** chosen from the platform Rulesync runs on: the cloud agent runs hooks in a **Linux sandbox** where only `bash` and `command` are honored, so a `powershell` entry generated on a Windows machine would simply never run. It also keeps the output identical everywhere, which matters because the cloud agent reads this file from the repository. For the same reason, an imported entry carrying both fields resolves to `bash` (with a warning) on every platform. VS Code and the coding agent both document `~/.copilot/hooks` as the user scope and load every `*.json` in that folder; the Copilot CLI's global file already occupies `copilot-hooks.json` there, so the VS Code target uses a distinct filename and the two never overwrite each other. Note the flip side of "every `*.json` is loaded": generating **both** `copilot` and `copilotcli` in global mode leaves two files in that one folder, and a reader of the folder runs the hooks from both — so a command present in your canonical config fires twice per event. Generate only one of the two globally unless you want that.
> - **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`, `userPromptTransformed` ← `userPromptExpansion`, `preMcpToolCall` ← `beforeMCPExecution`) and supports three hook types: **`command`** (`bash`/`powershell` with optional `timeoutSec`, plus pass-through `cwd`/`env`; on import the portable `command` field is read as the cross-platform fallback when neither shell field is present, and `timeout` is honored as an alias for `timeoutSec` when `timeoutSec` is absent. On generate the canonical `shell` selector chooses `bash` or `powershell`; without it the portable `command` field is written, so the generated file does not depend on the machine Rulesync ran on), **`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`). An entry's optional `matcher` field is emitted and round-tripped on the six events the hooks reference documents as matcher-aware — `preToolUse` and `postToolUse` (regex on the tool name), `permissionRequest` (tool name), `notification` (notification type), `preCompact` (the trigger, `manual` or `auto`) and `subagentStart` (agent name); on any other event a matcher is dropped with a warning because the CLI does not honor it there. See the [hooks reference](https://docs.github.com/en/copilot/reference/hooks-reference).
> - **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.
> - **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.
Expand Down
Loading
Loading