| name | harness-hook |
| description | Create and manage lifecycle hook entities in the agent-harness workspace. Covers canonical event authoring, provider projection (Claude Code, GitHub Copilot, OpenAI Codex CLI, Cursor), mode behavior, handler types, the full event/provider support matrix, copy-paste recipes, and common diagnostics. |
/harness-hook — Lifecycle Hook Authoring
A hook entity defines shell commands that run at specific agent lifecycle points. The canonical source lives in .harness/src/hooks/<hook-id>.json. When you run npx harness apply, harness projects that single source into each enabled provider's native config format.
Quick start
npx harness add hook <hook-id>
npx harness apply
Canonical source shape
{
"mode": "strict",
"events": {
"<canonical-event>": [
{
"type": "command",
"command": "node scripts/my-hook.js",
"timeoutSec": 15
}
]
}
}
mode field
| Value | Behavior |
|---|
"strict" (default) | Any unsupported provider/event/handler combination fails with a diagnostic (e.g., HOOK_EVENT_UNSUPPORTED). Use when you want to be sure every enabled provider receives the hook. |
"best_effort" | Unsupported combinations are silently skipped. Use while prototyping multi-provider setups. |
Recommendation: start with "best_effort" during development, switch to "strict" once all providers are intentionally configured.
Canonical event list and provider support
| Canonical event | Claude Code | GitHub Copilot | OpenAI Codex | Cursor |
|---|
session_start | Yes (SessionStart) | Yes (sessionStart) | Yes (SessionStart) | Yes (sessionStart) |
session_end | Yes (SessionEnd) | Yes (sessionEnd) | No | Yes (sessionEnd) |
prompt_submit | Yes (UserPromptSubmit) | Yes (userPromptSubmitted) | Yes (UserPromptSubmit) | Yes (beforeSubmitPrompt) |
pre_tool_use | Yes (PreToolUse) | Yes (preToolUse) | Yes (PreToolUse) | Yes (preToolUse) |
permission_request | Yes (PermissionRequest) | No | Yes (PermissionRequest) | No |
post_tool_use | Yes (PostToolUse) | Yes (postToolUse) | Yes (PostToolUse) | Yes (postToolUse) |
post_tool_failure | Yes (PostToolUseFailure) | No | No | Yes (postToolUseFailure) |
notification | Yes (Notification) | No | No | No |
subagent_start | Yes (SubagentStart) | No | Yes (SubagentStart) | Yes (subagentStart) |
subagent_stop | Yes (SubagentStop) | No | Yes (SubagentStop) | Yes (subagentStop) |
stop | Yes (Stop) | No | Yes (Stop) | Yes (stop) |
stop_failure | Yes (StopFailure) | No | No | No |
teammate_idle | Yes (TeammateIdle) | No | No | No |
task_completed | Yes (TaskCompleted) | No | No | No |
instructions_loaded | Yes (InstructionsLoaded) | No | No | No |
config_change | Yes (ConfigChange) | No | No | No |
worktree_create | Yes (WorktreeCreate) | No | No | No |
worktree_remove | Yes (WorktreeRemove) | No | No | No |
pre_compact | Yes (PreCompact) | No | Yes (PreCompact) | Yes (preCompact) |
post_compact | Yes (PostCompact) | No | Yes (PostCompact) | No |
elicitation | Yes (Elicitation) | No | No | No |
elicitation_result | Yes (ElicitationResult) | No | No | No |
error | No | Yes (errorOccurred) | No | No |
turn_complete | No | No | Yes (notify) | No |
Provider projection details
Claude Code
- Output file:
.claude/settings.json (key: hooks)
- Event names: PascalCase (e.g.,
pre_tool_use → PreToolUse)
- Handler types:
command only (harness canonical). Claude Code natively supports command, http, prompt, and agent handler types, but the harness canonical format only supports command.
- Matcher: supported on most events — filters by tool name, session source, exit reason, etc. Specify
"matcher" on the handler; harness groups handlers with the same matcher under one entry.
- Output shape:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "python3 scripts/check_bash_policy.py" }
]
}
]
}
}
- Blocking events:
PreToolUse, PermissionRequest, UserPromptSubmit, Stop, SubagentStop, TeammateIdle, TaskCompleted, ConfigChange, Elicitation, ElicitationResult, WorktreeCreate — a handler exiting with code 2 blocks/denies the action.
- Official docs: https://code.claude.com/docs/en/hooks
GitHub Copilot
- Output file:
.github/hooks/harness.generated.json (top-level version: 1)
- Event names: camelCase (e.g.,
pre_tool_use → preToolUse, prompt_submit → userPromptSubmitted)
- Supported canonical events:
session_start, session_end, prompt_submit, pre_tool_use, post_tool_use, error
- Handler types:
command only
- Matcher: NOT supported — fails in
"strict" mode, silently ignored in "best_effort" mode
- Output shape:
{
"version": 1,
"hooks": {
"preToolUse": [
{
"type": "command",
"bash": "node scripts/pre_tool.js",
"powershell": "node scripts/pre_tool.js",
"timeoutSec": 15
}
]
}
}
OpenAI Codex CLI
- Output file:
.codex/config.toml (keys: inline [hooks], [features] hooks = true, and legacy notify = [...])
- Supported canonical lifecycle events:
session_start, prompt_submit, pre_tool_use, permission_request, post_tool_use, subagent_start, subagent_stop, pre_compact, post_compact, stop
- Legacy notification event:
turn_complete renders to top-level notify = [...]
- Handler types: lifecycle hooks accept
command; turn_complete accepts notify and command, both normalized to a TOML notify command array
- Matcher: supported for
session_start, pre_tool_use, permission_request, post_tool_use, subagent_start, subagent_stop, pre_compact, and post_compact (prompt_submit and stop ignore it); unsupported matcher usage fails in "strict" mode
- Command options:
timeout/timeoutSec render as timeout; statusMessage is supported; cwd and env are unsupported
- Notify normalization rules:
notify handler: command field used directly; string values are wrapped as ["sh", "-lc", "<command>"]; arrays pass through unchanged
command handler: first available field among command, bash, linux, osx, powershell, windows is selected and wrapped the same way
- Conflict rule: only one notify command is allowed across all enabled hook entities. If two hooks produce different notify commands,
apply fails with HOOK_NOTIFY_CONFLICT.
- Lifecycle output shape:
[features]
hooks = true
[[hooks.PreToolUse]]
matcher = "^Bash$"
[[hooks.PreToolUse.hooks]]
type = "command"
command = "python3 scripts/check_bash.py"
timeout = 30
statusMessage = "Checking Bash command"
notify = ["python3", "scripts/on_turn_complete.py"]
Cursor
- Output file:
.cursor/hooks.json (key: hooks, top-level version: 1)
- Event names: camelCase (e.g.,
pre_tool_use → preToolUse, prompt_submit → beforeSubmitPrompt)
- Supported canonical events:
session_start, session_end, prompt_submit, pre_tool_use, post_tool_use, post_tool_failure, subagent_start, subagent_stop, pre_compact, stop
- Handler types:
command only
- Matcher: supported — filters by tool name, subagent type, or command text depending on the event
- Limitations:
cwd and env fields on command handlers are NOT supported (harness errors if present)
- Hook execution types: Cursor supports both command-based hooks (shell scripts) and prompt-based hooks (LLM-evaluated natural language conditions), but harness only projects command-based hooks.
- Per-script options:
timeout (seconds), loop_limit (for stop/subagentStop), failClosed (block action on hook failure)
- Output shape:
{
"version": 1,
"hooks": {
"preToolUse": [
{
"command": "./scripts/validate-tool.sh",
"matcher": "Shell|Read|Write"
}
]
}
}
- Additional Cursor-native events (no canonical equivalent):
beforeShellExecution, afterShellExecution, beforeMCPExecution, afterMCPExecution, beforeReadFile, afterFileEdit, afterAgentResponse, afterAgentThought, beforeTabFileRead, afterTabFileEdit. These are not projectable from harness canonical events; use them directly in .cursor/hooks.json if needed.
- Configuration locations: Project (
.cursor/hooks.json), user (~/.cursor/hooks.json), enterprise (system-wide), team (cloud-distributed)
- Priority order: Enterprise → Team → Project → User
- Exit code behavior: Exit code
0 = success, exit code 2 = block/deny, other codes = fail-open (unless failClosed: true)
- Official docs: https://docs.cursor.com/agent/hooks
Handler type reference
command handler
{
"type": "command",
"command": "node scripts/hook.js",
"bash": "node scripts/hook.js",
"linux": "node scripts/hook.js",
"osx": "node scripts/hook.js",
"windows": "node scripts\\hook.js",
"powershell": "node scripts\\hook.js",
"matcher": "Bash",
"cwd": ".",
"env": { "MY_VAR": "value" },
"timeoutSec": 30,
"statusMessage": "Checking command"
}
- At least one of
command, bash, linux, osx, windows, powershell is required.
matcher support is provider/event-dependent. Codex supports it on session_start, pre_tool_use, permission_request, post_tool_use, subagent_start, subagent_stop, pre_compact, and post_compact (prompt_submit and stop ignore it); Copilot does not support it.
env values must all be strings.
timeoutSec (or timeout) must be a positive number.
statusMessage is Codex lifecycle-hook only.
notify handler (Codex only)
{
"type": "notify",
"event": "agent-turn-complete",
"command": ["python3", "scripts/on_turn_complete.py"]
}
event defaults to "agent-turn-complete" when omitted — it is the only supported value.
command must be a non-empty string or string array.
matcher is not supported on notify handlers.
Copy-paste recipes
1) Claude + Copilot pre-tool guard
Use "strict" mode because pre_tool_use is supported by both providers.
{
"mode": "strict",
"events": {
"pre_tool_use": [
{
"type": "command",
"bash": "node scripts/hook-pre-tool.js",
"powershell": "node scripts/hook-pre-tool.js",
"timeoutSec": 15
}
]
}
}
Outputs:
- Claude:
.claude/settings.json hooks.PreToolUse
- Copilot:
.github/hooks/harness.generated.json hooks.preToolUse
2) Claude matcher-based tool policy
Applies only when the Bash tool is invoked. Copilot does not support matcher — use a separate hook file or "best_effort" mode if Copilot is also enabled.
{
"mode": "strict",
"events": {
"pre_tool_use": [
{
"type": "command",
"matcher": "Bash",
"command": "python3 scripts/check_bash_policy.py"
}
]
}
}
Claude output fragment:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "python3 scripts/check_bash_policy.py" }
]
}
]
}
}
3) Codex pre-tool guard
{
"mode": "strict",
"events": {
"pre_tool_use": [
{
"type": "command",
"matcher": "^Bash$",
"command": "python3 scripts/check_bash.py",
"timeout": 30,
"statusMessage": "Checking Bash command"
}
]
}
}
Codex output in .codex/config.toml:
[features]
hooks = true
[[hooks.PreToolUse]]
matcher = "^Bash$"
[[hooks.PreToolUse.hooks]]
type = "command"
command = "python3 scripts/check_bash.py"
timeout = 30
statusMessage = "Checking Bash command"
4) Codex turn-complete notification
{
"mode": "strict",
"events": {
"turn_complete": [
{
"type": "notify",
"command": ["python3", "scripts/on_turn_complete.py"]
}
]
}
}
Codex output in .codex/config.toml:
notify = ["python3", "scripts/on_turn_complete.py"]
5) Cross-provider best_effort hook (all three providers)
Covers pre_tool_use for Claude/Copilot/Codex and turn_complete for Codex in one file. Unsupported combinations are skipped rather than failing.
{
"mode": "best_effort",
"events": {
"pre_tool_use": [
{
"type": "command",
"bash": "node scripts/pre_tool.js",
"powershell": "node scripts/pre_tool.js"
}
],
"turn_complete": [
{
"type": "notify",
"command": ["python3", "scripts/on_turn_complete.py"]
}
]
}
}
Behavior:
- Claude: receives
PreToolUse; turn_complete is skipped.
- Copilot: receives
preToolUse; turn_complete is skipped.
- Codex: receives
PreToolUse plus notify.
Common diagnostics
| Code | Cause |
|---|
HOOK_JSON_INVALID | Source file is not valid JSON |
HOOK_MODE_INVALID | mode is not "strict" or "best_effort" |
HOOK_EVENTS_INVALID | events key is missing or not an object |
HOOK_EVENT_UNKNOWN | Event key is not in the canonical event list |
HOOK_HANDLER_TYPE_INVALID | Handler type is not "command" or "notify" |
HOOK_COMMAND_MISSING | command handler has no command field |
HOOK_TIMEOUT_INVALID | timeoutSec/timeout is not a positive number |
HOOK_ENV_INVALID | env is not a string-to-string map |
HOOK_STATUS_MESSAGE_INVALID | statusMessage is present but not a non-empty string |
HOOK_NOTIFY_EVENT_INVALID | notify handler event is not "agent-turn-complete" |
HOOK_NOTIFY_COMMAND_INVALID | notify handler command is empty or wrong type |
HOOK_EVENT_UNSUPPORTED | Event is not supported by the target provider (strict mode) |
HOOK_NOTIFY_CONFLICT | Two Codex hooks produce different notify commands |
HOOK_TARGET_CONFLICT | Multiple hook entities for one provider resolve to different output paths |
CLI commands
npx harness add hook <hook-id>
npx harness apply
npx harness plan
npx harness remove hook <hook-id>
npx harness remove hook <hook-id> --no-delete-source
npx harness validate
Output path overrides
To send a hook entity to a custom path for a specific provider, create a sidecar file:
.harness/src/hooks/<hook-id>.overrides.<provider>.yaml
version: 1
targetPath: ".github/hooks/security.generated.json"
If multiple hook entities for the same provider resolve to different target paths, apply fails with HOOK_TARGET_CONFLICT (or CODEX_CONFIG_TARGET_CONFLICT for Codex).
Official documentation