Skip to content
Open
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
95 changes: 76 additions & 19 deletions .harness/src/skills/harness-hook/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,17 @@ Recommendation: start with `"best_effort"` during development, switch to `"stric

| Canonical event | Claude Code | GitHub Copilot | OpenAI Codex | Cursor |
|---|---|---|---|---|
| `session_start` | Yes (`SessionStart`) | Yes (`sessionStart`) | No | Yes (`sessionStart`) |
| `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`) | No | Yes (`beforeSubmitPrompt`) |
| `pre_tool_use` | Yes (`PreToolUse`) | Yes (`preToolUse`) | No | Yes (`preToolUse`) |
| `permission_request` | Yes (`PermissionRequest`) | No | No | No |
| `post_tool_use` | Yes (`PostToolUse`) | Yes (`postToolUse`) | No | Yes (`postToolUse`) |
| `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 | No | Yes (`subagentStart`) |
| `subagent_stop` | Yes (`SubagentStop`) | No | No | Yes (`subagentStop`) |
| `stop` | Yes (`Stop`) | No | No | Yes (`stop`) |
| `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 |
Expand Down Expand Up @@ -135,21 +135,39 @@ Recommendation: start with `"best_effort"` during development, switch to `"stric

### OpenAI Codex CLI

- **Output file:** `.codex/config.toml` (key: `notify = [...]`)
- **Supported canonical events:** `turn_complete` only
- **Handler types:** both `notify` and `command` handlers are accepted — both normalize to a TOML notify command array
- **Matcher:** NOT supported
- **Normalization rules:**
- **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`, `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`, and `post_tool_use`; 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`.
- **Output shape:**
- **Lifecycle output shape:**

```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"
```

- **Notify output shape:**

```toml
notify = ["python3", "scripts/on_turn_complete.py"]
```

- **Official docs:** https://developers.openai.com/codex/config-reference
- **Official docs:** https://developers.openai.com/codex/hooks

### Cursor

Expand Down Expand Up @@ -201,14 +219,16 @@ notify = ["python3", "scripts/on_turn_complete.py"]
"matcher": "Bash",
"cwd": ".",
"env": { "MY_VAR": "value" },
"timeoutSec": 30
"timeoutSec": 30,
"statusMessage": "Checking command"
}
```

- At least one of `command`, `bash`, `linux`, `osx`, `windows`, `powershell` is required.
- `matcher` is Claude-only; ignored/errors for Copilot (strict mode error), not applicable for Codex.
- `matcher` support is provider/event-dependent. Codex supports it on `session_start`, `pre_tool_use`, `permission_request`, and `post_tool_use`; 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)

Expand Down Expand Up @@ -288,7 +308,42 @@ Claude output fragment:
}
```

### 3) Codex turn-complete notification
### 3) Codex pre-tool guard

```json
{
"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`:

```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

```json
{
Expand All @@ -310,9 +365,9 @@ Codex output in `.codex/config.toml`:
notify = ["python3", "scripts/on_turn_complete.py"]
```

### 4) Cross-provider best_effort hook (all three providers)
### 5) Cross-provider best_effort hook (all three providers)

Covers `pre_tool_use` for Claude/Copilot and `turn_complete` for Codex in one file. Unsupported combinations are skipped rather than failing.
Covers `pre_tool_use` for Claude/Copilot/Codex and `turn_complete` for Codex in one file. Unsupported combinations are skipped rather than failing.

```json
{
Expand All @@ -338,7 +393,7 @@ Covers `pre_tool_use` for Claude/Copilot and `turn_complete` for Codex in one fi
Behavior:
- Claude: receives `PreToolUse`; `turn_complete` is skipped.
- Copilot: receives `preToolUse`; `turn_complete` is skipped.
- Codex: receives `notify`; `pre_tool_use` is skipped.
- Codex: receives `PreToolUse` plus `notify`.

---

Expand All @@ -354,6 +409,7 @@ Behavior:
| `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) |
Expand Down Expand Up @@ -395,5 +451,6 @@ If multiple hook entities for the same provider resolve to different target path
- 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
5 changes: 3 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,8 @@ Notes:

- Claude: rendered into `.claude/settings.json` as `hooks` configuration.
- Copilot: rendered into `.github/hooks/harness.generated.json` (`version: 1` + `hooks` map).
- Codex: projected into `.codex/config.toml` notify command only (canonical `turn_complete`).
- Codex: projected into `.codex/config.toml` as inline `[hooks]` lifecycle tables for supported events, plus top-level
`notify = [...]` for canonical `turn_complete`.

In strict mode, unsupported provider/event/type projections fail with `HOOK_EVENT_UNSUPPORTED`.

Expand Down Expand Up @@ -164,7 +165,7 @@ High-level flow (`loader.ts` + `planner.ts` + `engine.ts`):
5. Load canonical entities + provider override sidecars (with env var substitution).
6. Render provider artifacts through adapters:
- per-entity renders (`prompt`, `skill`, `subagent`, `hook`)
- optional provider-state render (`codex` composite state for MCP/subagent/hook notify)
- optional provider-state render (`codex` composite state for MCP/subagent/lifecycle hooks/notify)
7. Detect collisions, unmanaged collisions, drift, creates, updates, and stale deletes.
8. Build deterministic `operations`, `nextLock`, and `nextManagedIndex`.
9. `plan` returns diagnostics/operations only.
Expand Down
71 changes: 64 additions & 7 deletions docs/hook-authoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ Supported fields:
- `env` (string map)
- `timeoutSec`
- `timeout`
- `statusMessage` (Codex lifecycle hooks only)

At least one command field is required.

Expand All @@ -93,11 +94,29 @@ Supported fields:
| --- | --- | --- |
| Claude | most lifecycle events (mapped to Claude names) | `command` |
| Copilot | `session_start`, `session_end`, `prompt_submit`, `pre_tool_use`, `post_tool_use`, `stop`, `subagent_stop`, `error` | `command` |
| Codex | `turn_complete` | `notify` and `command` (both normalized to `notify`) |
| Codex | `session_start`, `prompt_submit`, `pre_tool_use`, `permission_request`, `post_tool_use`, `stop`, plus legacy `turn_complete` notification | `command` for lifecycle hooks; `notify` and `command` for `turn_complete` |

## Codex command normalization
## Codex lifecycle hooks and notifications

When Codex projects a `turn_complete` handler, both `notify` and `command` handler types are converted into a TOML `notify` command array:
Codex lifecycle hooks render inline in `.codex/config.toml` under `[hooks]` and enable the canonical feature flag:

```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"
```

Codex matcher support is limited to `session_start`, `pre_tool_use`, `permission_request`, and `post_tool_use`. `cwd` and `env` are not supported for Codex lifecycle command hooks.

For legacy Codex notifications, `turn_complete` handlers still render to top-level `notify = [...]`. Both `notify` and `command` handler types are converted into a TOML notify command array:

- `notify` handlers: the `command` field is used directly (arrays pass through; strings are wrapped as `["sh", "-lc", "<command>"]`).
- `command` handlers: the first available command field (`command`, `bash`, `linux`, `osx`, `powershell`, `windows`) is selected and wrapped the same way.
Expand Down Expand Up @@ -222,7 +241,44 @@ Expected Copilot output fragment:
}
```

### 4) Codex turn-complete notification
### 4) Codex pre-tool guard

Canonical source:

```json
{
"mode": "strict",
"events": {
"pre_tool_use": [
{
"type": "command",
"matcher": "^Bash$",
"command": "python3 scripts/check_bash.py",
"timeout": 30,
"statusMessage": "Checking Bash command"
}
]
}
}
```

Expected Codex output fragment in `.codex/config.toml`:

```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"
```

### 5) Codex turn-complete notification

Canonical source:

Expand All @@ -246,7 +302,7 @@ Expected Codex output fragment in `.codex/config.toml`:
notify = ["python3", "scripts/on_turn_complete.py"]
```

### 5) One file for all providers (`best_effort`)
### 6) One file for all providers (`best_effort`)

Canonical source:

Expand All @@ -273,8 +329,8 @@ Canonical source:

Behavior:

- Claude/Copilot use `pre_tool_use`.
- Codex uses `turn_complete`.
- Claude/Copilot/Codex use `pre_tool_use`.
- Codex also uses `turn_complete`.
- Unsupported parts are skipped instead of failing.

## Target path overrides
Expand Down Expand Up @@ -305,6 +361,7 @@ Rules:
- `HOOK_COMMAND_MISSING`
- `HOOK_TIMEOUT_INVALID`
- `HOOK_ENV_INVALID`
- `HOOK_STATUS_MESSAGE_INVALID`
- `HOOK_NOTIFY_EVENT_INVALID`
- `HOOK_NOTIFY_COMMAND_INVALID`
- `HOOK_EVENT_UNSUPPORTED`
Expand Down
4 changes: 3 additions & 1 deletion docs/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,9 @@ Authoring examples: [Hook Authoring Guide](./hook-authoring.md)

- Codex projects canonical `session_start`, `prompt_submit`, `pre_tool_use`, `permission_request`, `post_tool_use`, and `stop` into inline `[hooks]` tables in `.codex/config.toml`.
- Codex also keeps canonical `turn_complete` as legacy `notify = [...]` output for current compatibility.
- When inline hooks are present, `harness` emits `[features] codex_hooks = true`.
- When inline hooks are present, `harness` emits `[features] hooks = true`; `codex_hooks` is treated as a deprecated upstream alias during import.
- Matcher is supported for projected Codex `session_start`, `pre_tool_use`, `permission_request`, and `post_tool_use` events.
- `statusMessage` is supported for Codex lifecycle command hooks.
- `cwd` and `env` command fields are unsupported for Codex inline hook projection and fail in strict mode.
- Multiple distinct notify commands across enabled hooks fail with `HOOK_NOTIFY_CONFLICT`.

Expand Down Expand Up @@ -230,6 +231,7 @@ For hook entities the default sidecar path is:
## References

- [OpenAI Codex Config Reference](https://developers.openai.com/codex/config-reference)
- [OpenAI Codex Hooks](https://developers.openai.com/codex/hooks)
- [OpenAI AGENTS.md Guide](https://developers.openai.com/codex/agents)
- [Claude Code Settings](https://docs.claude.com/en/docs/claude-code/settings)
- [Claude Code Hooks](https://code.claude.com/docs/en/hooks)
Expand Down
3 changes: 2 additions & 1 deletion docs/toolkit.hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ Parses and validates canonical hook source documents loaded from `.harness/src/h
- `command`, `windows`, `linux`, `osx`, `bash`, or `powershell`
- `timeoutSec` / `timeout` must be positive numbers when present.
- `env` must be an object of string values.
- `statusMessage`, when present, must be a non-empty string.

### `notify` handler validation

Expand All @@ -41,4 +42,4 @@ Parses and validates canonical hook source documents loaded from `.harness/src/h

## Diagnostic families

Emits `HOOK_*` diagnostics for invalid mode/event/handler/timeout/env/notify shape.
Emits `HOOK_*` diagnostics for invalid mode/event/handler/timeout/env/status-message/notify shape.
2 changes: 1 addition & 1 deletion docs/toolkit.provider.codex.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Builds the Codex provider adapter.
- `mcp_servers` (merged MCP servers)
- `agents.<id>` entries (enabled subagents, with `developer_instructions`, `description`, and supported
provider-specific options)
- inline `[hooks]` config plus `[features] codex_hooks = true` when Codex hook events are projected
- inline `[hooks]` config plus `[features] hooks = true` when Codex lifecycle hook events are projected
- `notify = [...]` (projected from hook `turn_complete`)
- Returns no artifact when merged payload is empty.
- Output string is normalized to one trailing newline.
Expand Down
20 changes: 20 additions & 0 deletions docs/toolkit.provider.hook-capabilities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# `packages/toolkit/src/provider-adapters/hook-capabilities.ts`

## Purpose

Centralizes provider hook event capabilities so renderers and legacy import paths use the same support matrix.

## Core exports

- `HOOK_PROVIDER_CAPABILITIES`
- `getHookEventCapability(provider, eventName)`
- `nativeToCanonicalHookEvent(provider, nativeEvent)`

## Captured capabilities

- Native provider event name for each supported canonical event.
- Whether `matcher` is supported for that provider/event pair.
- Command-handler field support such as timeout key, `cwd`, `env`, and `statusMessage`.

This module is data-oriented. Provider renderers still own provider-native output shapes, so Claude/Codex grouped hook
config and Copilot/Cursor flat hook config stay separate.
12 changes: 10 additions & 2 deletions docs/toolkit.provider.hooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@ Shared hook projection utilities for provider adapters.
- `renderClaudeHookSettings(hooks)`
- `renderCopilotHookConfig(hooks)`
- `renderCursorHookConfig(hooks)`
- `renderCodexHookConfigObject(hooks)`
- `resolveCodexNotifyCommand(hooks)`

Provider event names and matcher support are shared through `hook-capabilities.ts` so forward rendering and `u-haul`
reverse imports use the same mapping.

## Provider projections

### Claude
Expand Down Expand Up @@ -42,8 +46,12 @@ Shared hook projection utilities for provider adapters.

### Codex

- Projects canonical `turn_complete` events in all modes; strict mode controls whether unsupported events throw vs. are skipped.
- Normalizes either canonical `notify` or `command` handlers into notify command arrays.
- Maps canonical `session_start`, `prompt_submit`, `pre_tool_use`, `permission_request`, `post_tool_use`, and `stop`
into Codex inline `[hooks]` tables.
- Emits `[features] hooks = true` when lifecycle hooks are projected.
- Supports `statusMessage` on Codex lifecycle command handlers.
- Projects canonical `turn_complete` as the legacy top-level `notify = [...]` command.
- Normalizes either canonical `notify` or `command` `turn_complete` handlers into notify command arrays.
- Returns one merged notify command, failing on incompatible multiple values (`HOOK_NOTIFY_CONFLICT`).

## Strict vs best-effort
Expand Down
Loading
Loading