Files
codex/codex-rs/app-server-protocol/schema/json/v2/HookStartedNotification.json
Andrei Eternal 6fef421654 [hooks] userpromptsubmit - hook before user's prompt is executed (#14626)
- this allows blocking the user's prompts from executing, and also
prevents them from entering history
- handles the edge case where you can both prevent the user's prompt AND
add n amount of additionalContexts
- refactors some old code into common.rs where hooks overlap
functionality
- refactors additionalContext being previously added to user messages,
instead we use developer messages for them
- handles queued messages correctly

Sample hook for testing - if you write "[block-user-submit]" this hook
will stop the thread:

example run
```
› sup


• Running UserPromptSubmit hook: reading the observatory notes

UserPromptSubmit hook (completed)
  warning: wizard-tower UserPromptSubmit demo inspected: sup
  hook context: Wizard Tower UserPromptSubmit demo fired. For this reply only, include the exact
phrase 'observatory lanterns lit' exactly once near the end.

• Just riding the cosmic wave and ready to help, my friend. What are we building today? observatory
  lanterns lit


› and [block-user-submit]


• Running UserPromptSubmit hook: reading the observatory notes

UserPromptSubmit hook (stopped)
  warning: wizard-tower UserPromptSubmit demo blocked the prompt on purpose.
  stop: Wizard Tower demo block: remove [block-user-submit] to continue.
```

.codex/config.toml
```
[features]
codex_hooks = true
```

.codex/hooks.json
```
{
  "hooks": {
    "UserPromptSubmit": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "/usr/bin/python3 .codex/hooks/user_prompt_submit_demo.py",
            "timeoutSec": 10,
            "statusMessage": "reading the observatory notes"
          }
        ]
      }
    ]
  }
}
```

.codex/hooks/user_prompt_submit_demo.py
```
#!/usr/bin/env python3

import json
import sys
from pathlib import Path


def prompt_from_payload(payload: dict) -> str:
    prompt = payload.get("prompt")
    if isinstance(prompt, str) and prompt.strip():
        return prompt.strip()

    event = payload.get("event")
    if isinstance(event, dict):
        user_prompt = event.get("user_prompt")
        if isinstance(user_prompt, str):
            return user_prompt.strip()

    return ""


def main() -> int:
    payload = json.load(sys.stdin)
    prompt = prompt_from_payload(payload)
    cwd = Path(payload.get("cwd", ".")).name or "wizard-tower"

    if "[block-user-submit]" in prompt:
        print(
            json.dumps(
                {
                    "systemMessage": (
                        f"{cwd} UserPromptSubmit demo blocked the prompt on purpose."
                    ),
                    "decision": "block",
                    "reason": (
                        "Wizard Tower demo block: remove [block-user-submit] to continue."
                    ),
                }
            )
        )
        return 0

    prompt_preview = prompt or "(empty prompt)"
    if len(prompt_preview) > 80:
        prompt_preview = f"{prompt_preview[:77]}..."

    print(
        json.dumps(
            {
                "systemMessage": (
                    f"{cwd} UserPromptSubmit demo inspected: {prompt_preview}"
                ),
                "hookSpecificOutput": {
                    "hookEventName": "UserPromptSubmit",
                    "additionalContext": (
                        "Wizard Tower UserPromptSubmit demo fired. "
                        "For this reply only, include the exact phrase "
                        "'observatory lanterns lit' exactly once near the end."
                    ),
                },
            }
        )
    )
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
```
2026-03-17 22:09:22 -07:00

162 lines
3.0 KiB
JSON

{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"HookEventName": {
"enum": [
"sessionStart",
"userPromptSubmit",
"stop"
],
"type": "string"
},
"HookExecutionMode": {
"enum": [
"sync",
"async"
],
"type": "string"
},
"HookHandlerType": {
"enum": [
"command",
"prompt",
"agent"
],
"type": "string"
},
"HookOutputEntry": {
"properties": {
"kind": {
"$ref": "#/definitions/HookOutputEntryKind"
},
"text": {
"type": "string"
}
},
"required": [
"kind",
"text"
],
"type": "object"
},
"HookOutputEntryKind": {
"enum": [
"warning",
"stop",
"feedback",
"context",
"error"
],
"type": "string"
},
"HookRunStatus": {
"enum": [
"running",
"completed",
"failed",
"blocked",
"stopped"
],
"type": "string"
},
"HookRunSummary": {
"properties": {
"completedAt": {
"format": "int64",
"type": [
"integer",
"null"
]
},
"displayOrder": {
"format": "int64",
"type": "integer"
},
"durationMs": {
"format": "int64",
"type": [
"integer",
"null"
]
},
"entries": {
"items": {
"$ref": "#/definitions/HookOutputEntry"
},
"type": "array"
},
"eventName": {
"$ref": "#/definitions/HookEventName"
},
"executionMode": {
"$ref": "#/definitions/HookExecutionMode"
},
"handlerType": {
"$ref": "#/definitions/HookHandlerType"
},
"id": {
"type": "string"
},
"scope": {
"$ref": "#/definitions/HookScope"
},
"sourcePath": {
"type": "string"
},
"startedAt": {
"format": "int64",
"type": "integer"
},
"status": {
"$ref": "#/definitions/HookRunStatus"
},
"statusMessage": {
"type": [
"string",
"null"
]
}
},
"required": [
"displayOrder",
"entries",
"eventName",
"executionMode",
"handlerType",
"id",
"scope",
"sourcePath",
"startedAt",
"status"
],
"type": "object"
},
"HookScope": {
"enum": [
"thread",
"turn"
],
"type": "string"
}
},
"properties": {
"run": {
"$ref": "#/definitions/HookRunSummary"
},
"threadId": {
"type": "string"
},
"turnId": {
"type": [
"string",
"null"
]
}
},
"required": [
"run",
"threadId"
],
"title": "HookStartedNotification",
"type": "object"
}