| title | Contributing Hooks | ||||
|---|---|---|---|---|---|
| description | How to implement, register, and validate hook artifacts in hve-core | ||||
| sidebar_position | 7 | ||||
| author | Microsoft | ||||
| ms.date | 2026-06-18 | ||||
| ms.topic | how-to | ||||
| keywords |
|
||||
| estimated_reading_time | 6 |
Hooks let you run lightweight automation during Copilot lifecycle events without modifying agents, prompts, or skills. In hve-core, hooks are packaged as collection artifacts and can be distributed with other AI customization files.
Use a hook when you need event-driven behavior such as:
- collecting local diagnostics or telemetry
- enforcing lightweight local policy checks
- triggering sidecar automation before or after tool calls
Hooks are collection-scoped, like every other distributable artifact type. Use this structure for hook contributions:
| Path | Purpose |
|---|---|
.github/hooks/<collection>/<name>.json |
Hook manifest that maps lifecycle events to executable commands |
.github/hooks/<collection>/<name>/ |
Hook implementation scripts and support files |
scripts/linting/schemas/hook-manifest.schema.json |
JSON Schema (draft-07) that defines the manifest contract |
collections/*.collection.yml |
Collection registration with kind: hook |
collections/*.collection.md |
Human-readable hook entry in the collection documentation table |
Manifests live one collection level down (.github/hooks/<collection>/) so the installer can activate each collection's hooks independently by adding only that collection's folder to chat.hookFilesLocations. A flat .github/hooks/<name>.json is treated as a repo-specific artifact and is excluded from distribution.
The telemetry hook is the current reference implementation:
.github/hooks/shared/telemetry.json.github/hooks/shared/telemetry/
- Add a manifest at
.github/hooks/<collection>/<name>.json. - Add executable scripts under
.github/hooks/<collection>/<name>/. - Register the hook in one or more
collections/*.collection.ymlfiles. - Document the hook in the matching
collections/*.collection.mdfiles. - Add or update docs under
docs/for setup and usage.
Minimal manifest pattern:
{
"version": 1,
"hooks": {
"preToolUse": [
{
"type": "command",
"bash": ".github/hooks/shared/my-hook/my-hook.sh",
"powershell": ".github/hooks/shared/my-hook/Invoke-MyHook.ps1",
"timeoutSec": 10
}
]
}
}For reliability and portability, hook scripts should follow these rules:
- Read event payload JSON from stdin.
- Return quickly on the disabled path.
- Write
{"continue":true}to stdout on normal completion. - Avoid interactive prompts.
- Keep runtime short and respect
timeoutSecvalues in the manifest. - Support both bash and PowerShell paths when practical.
Telemetry follows this model with a no-op gate and structured JSONL append behavior.
Manifests are validated against scripts/linting/schemas/hook-manifest.schema.json, the authoritative contract. The schema enforces the allowed top-level keys (version, description, hooks), the eight CLI-lowercase event names (sessionStart, userPromptSubmit, preToolUse, postToolUse, preCompact, subagentStart, subagentStop, stop), and the permitted command properties.
Run npm run lint:hooks to validate every collection-scoped manifest. On failure, the validator prints each error and the schema path so you can reconcile the manifest against the contract.
Hook payloads can contain sensitive data. PreToolUse inputs include full file
contents being written and shell command strings, and UserPromptSubmit
includes the full prompt, any of which may carry secrets. Follow these rules
when a hook persists payloads to disk:
- Store only the minimum needed for the hook's purpose. Prefer derived signals (keys, lengths, counts, truncated previews) over verbatim values.
- Gate any verbatim payload capture behind its own explicit opt-in, separate from the hook's main enable gate, and default it off.
- Write to local, gitignored locations and never to committed paths.
- Document exactly what is captured, where it is written, and how to remove it.
The telemetry hook applies this pattern: its processed sessions-*.jsonl stream
stores only tool-input key names and a truncated prompt preview, while the
verbatim raw-input.jsonl dump is a separate opt-in (HVE_TELEMETRY_RAW=1,
off by default). See Local Telemetry.
Write a single CLI-format block per event: lowercase event keys with bash and powershell command properties. VS Code automatically converts the lowercase CLI event names to its PascalCase form and maps bash to osx/linux and powershell to windows, so one block covers both surfaces. Do not also declare a PascalCase copy of the same event; VS Code would register and fire both, duplicating every invocation.
Choose CLI event names that convert to valid VS Code events:
sessionStart->SessionStartpreToolUse->PreToolUseuserPromptSubmit->UserPromptSubmit(notuserPromptSubmitted)stop->Stop(VS Code has nosessionEndoragentStopevent)
Add a collection item with kind: hook:
items:
- path: .github/hooks/<collection>/my-hook.json
kind: hookThen update the corresponding collection markdown (collections/*.collection.md) in the Hooks section so users can discover what the hook does.
Before opening a PR:
- Run
npm run lint:hooks - Run
npm run plugin:validate - Run
npm run plugin:generate - Run
npm run lint:md
When your hook includes scripts, also run the relevant script linters and tests for those languages.
🤖 Crafted with precision by ✨Copilot following brilliant human instruction, then carefully refined by our team of discerning human reviewers.