agentleFS
madebywildmadebywild/agent-harness/.github/skills/harness-hook/SKILL.md

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
  1. Very specific:

    Matcher: NOT supported — fails in "strict" mode, silently ignored in "best_effort" mode

    line 115
  2. Very specific:

    Conflict rule: only one notify command is allowed across all enabled hook entities.

    line 147
  3. Very specific:

    Use "strict" mode because pre_tool_use is supported by both providers.

    line 253
  4. Very specific:

    If multiple hook entities for the same provider resolve to different target paths, apply fails with HOOK_TARGET_CONFLICT (or …

    line 445

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

/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 configs

Canonical source shape

{
  "mode": "strict",
  "events": {
    "<canonical-event>": [
      {
        "type": "command",
        "command": "node scripts/my-hook.js",
        "timeoutSec": 15
      }
    ]
  }
}

mode field

ValueBehavior
"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 eventClaude CodeGitHub CopilotOpenAI CodexCursor
session_startYes (SessionStart)Yes (sessionStart)Yes (SessionStart)Yes (sessionStart)
session_endYes (SessionEnd)Yes (sessionEnd)NoYes (sessionEnd)
prompt_submitYes (UserPromptSubmit)Yes (userPromptSubmitted)Yes (UserPromptSubmit)Yes (beforeSubmitPrompt)
pre_tool_useYes (PreToolUse)Yes (preToolUse)Yes (PreToolUse)Yes (preToolUse)
permission_requestYes (PermissionRequest)NoYes (PermissionRequest)No
post_tool_useYes (PostToolUse)Yes (postToolUse)Yes (PostToolUse)Yes (postToolUse)
post_tool_failureYes (PostToolUseFailure)NoNoYes (postToolUseFailure)
notificationYes (Notification)NoNoNo
subagent_startYes (SubagentStart)NoYes (SubagentStart)Yes (subagentStart)
subagent_stopYes (SubagentStop)NoYes (SubagentStop)Yes (subagentStop)
stopYes (Stop)NoYes (Stop)Yes (stop)
stop_failureYes (StopFailure)NoNoNo
teammate_idleYes (TeammateIdle)NoNoNo
task_completedYes (TaskCompleted)NoNoNo
instructions_loadedYes (InstructionsLoaded)NoNoNo
config_changeYes (ConfigChange)NoNoNo
worktree_createYes (WorktreeCreate)NoNoNo
worktree_removeYes (WorktreeRemove)NoNoNo
pre_compactYes (PreCompact)NoYes (PreCompact)Yes (preCompact)
post_compactYes (PostCompact)NoYes (PostCompact)No
elicitationYes (Elicitation)NoNoNo
elicitation_resultYes (ElicitationResult)NoNoNo
errorNoYes (errorOccurred)NoNo
turn_completeNoNoYes (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 output shape:
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

CodeCause
HOOK_JSON_INVALIDSource file is not valid JSON
HOOK_MODE_INVALIDmode is not "strict" or "best_effort"
HOOK_EVENTS_INVALIDevents key is missing or not an object
HOOK_EVENT_UNKNOWNEvent key is not in the canonical event list
HOOK_HANDLER_TYPE_INVALIDHandler type is not "command" or "notify"
HOOK_COMMAND_MISSINGcommand handler has no command field
HOOK_TIMEOUT_INVALIDtimeoutSec/timeout is not a positive number
HOOK_ENV_INVALIDenv is not a string-to-string map
HOOK_STATUS_MESSAGE_INVALIDstatusMessage is present but not a non-empty string
HOOK_NOTIFY_EVENT_INVALIDnotify handler event is not "agent-turn-complete"
HOOK_NOTIFY_COMMAND_INVALIDnotify handler command is empty or wrong type
HOOK_EVENT_UNSUPPORTEDEvent is not supported by the target provider (strict mode)
HOOK_NOTIFY_CONFLICTTwo Codex hooks produce different notify commands
HOOK_TARGET_CONFLICTMultiple 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 constraints

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

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

Reviews can't be read right now.

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.