harness-hook
Skill16 starsChanged 7 months agoMITView on GitHub
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.
We split this skill into the rules it gives an agent
4 rules. About 0 are common, 0 are distinctive, and 4 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈0
Distinctive · estimate
This author's own choices. Worth a look.
4
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 115
Matcher: NOT supported — fails in "strict" mode, silently ignored in "best_effort" mode
- Very specific: line 147
Conflict rule: only one notify command is allowed across all enabled hook entities.
- Very specific: line 253
Use "strict" mode because pre_tool_use is supported by both providers.
- Very specific: line 445
If multiple hook entities for the same provider resolve to different target paths, apply fails with HOOK_TARGET_CONFLICT (or …
≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
/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> # scaffold .harness/src/hooks/<hook-id>.json
# edit the file
npx harness apply # project to provider configsCanonical 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:
commandonly (harness canonical). Claude Code natively supportscommand,http,prompt, andagenthandler types, but the harness canonical format only supportscommand. - 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-levelversion: 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:
commandonly - 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 legacynotify = [...]) - 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_completerenders to top-levelnotify = [...] - Handler types: lifecycle hooks accept
command;turn_completeacceptsnotifyandcommand, 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, andpost_compact(prompt_submitandstopignore it); unsupported matcher usage fails in"strict"mode - Command options:
timeout/timeoutSecrender astimeout;statusMessageis supported;cwdandenvare unsupported - Notify normalization rules:
notifyhandler:commandfield used directly; string values are wrapped as["sh", "-lc", "<command>"]; arrays pass through unchangedcommandhandler: first available field amongcommand,bash,linux,osx,powershell,windowsis 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,
applyfails withHOOK_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 output shape:
notify = ["python3", "scripts/on_turn_complete.py"]- Official docs: https://developers.openai.com/codex/hooks
Cursor
- Output file:
.cursor/hooks.json(key:hooks, top-levelversion: 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:
commandonly - Matcher: supported — filters by tool name, subagent type, or command text depending on the event
- Limitations:
cwdandenvfields 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(forstop/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.jsonif 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 code2= block/deny, other codes = fail-open (unlessfailClosed: 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,powershellis required. matchersupport is provider/event-dependent. Codex supports it onsession_start,pre_tool_use,permission_request,post_tool_use,subagent_start,subagent_stop,pre_compact, andpost_compact(prompt_submitandstopignore it); Copilot does not support it.envvalues must all be strings.timeoutSec(ortimeout) must be a positive number.statusMessageis Codex lifecycle-hook only.
notify handler (Codex only)
{
"type": "notify",
"event": "agent-turn-complete",
"command": ["python3", "scripts/on_turn_complete.py"]
}eventdefaults to"agent-turn-complete"when omitted — it is the only supported value.commandmust be a non-empty string or string array.matcheris not supported onnotifyhandlers.
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.jsonhooks.PreToolUse - Copilot:
.github/hooks/harness.generated.jsonhooks.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_completeis skipped. - Copilot: receives
preToolUse;turn_completeis skipped. - Codex: receives
PreToolUseplusnotify.
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> # scaffold .harness/src/hooks/<hook-id>.json
npx harness apply # project all hooks to provider configs
npx harness plan # dry-run: preview operations without writing
npx harness remove hook <hook-id> # remove hook entity and source file
npx harness remove hook <hook-id> --no-delete-source # remove entity but keep source
npx harness validate # validate manifest, ownership, and constraintsOutput 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
- Claude Code hooks: https://code.claude.com/docs/en/hooks
- GitHub Copilot hooks configuration: https://docs.github.com/en/copilot/reference/hooks-configuration
- GitHub Copilot about hooks: https://docs.github.com/en/copilot/concepts/agents/coding-agent/about-hooks
- OpenAI Codex hooks: https://developers.openai.com/codex/hooks
- OpenAI Codex config reference: https://developers.openai.com/codex/config-reference
- Cursor hooks: https://docs.cursor.com/agent/hooks
In this repository20 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- .github/copilot-instructions.mdCopilot instructions
- critical-reviewSkill
- harness-commandSkill
- harness-mcpSkill
- harness-prompt-sectionSkill
- harness-settingsSkill
- harness-skillSkill
- harness-subagentSkill
- npm-workflow-releaseSkill
- critical-reviewSkill
- harness-commandSkill
- harness-hookSkill
- harness-mcpSkill
- harness-prompt-sectionSkill
- harness-settingsSkill
- harness-skillSkill
- harness-subagentSkill
- npm-workflow-releaseSkill
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

