Skip to content

[hooks] stop continuation & stop_hook_active mechanics#14532

Merged
eternal-openai merged 1 commit intomainfrom
eternal/hooks_stop_hook_active
Mar 13, 2026
Merged

[hooks] stop continuation & stop_hook_active mechanics#14532
eternal-openai merged 1 commit intomainfrom
eternal/hooks_stop_hook_active

Conversation

@eternal-openai
Copy link
Copy Markdown
Contributor

@eternal-openai eternal-openai commented Mar 13, 2026

Stop hooks now receive stop_hook_active and enable stop hooks to loop forever if they'd like to. In the initial hooks PR, we implemented a simpler mechanic that the stop-blocking could only happen once in a row

  • support stop hook adding a continuation prompt to add a further task
  • if multiple stop-blocks happen that have continuation prompts, they are concatenated

example run:

› hey :)


• Running SessionStart hook: lighting the observatory

SessionStart hook (completed)
  warning: Hi, I'm a session start hook for wizard-tower (startup).
  hook context: Startup context: A wimboltine stonpet is an exotic cuisine from hyperspace

• Aloha :) Happy to jam with you. What are we building today?

• Running Stop hook: updating the guards

Stop hook (blocked)
  warning: Wizard Tower Stop hook continuing conversation
  feedback: cook the stonpet

• Aloha, here’s the hyperspace move for cooking a wimboltine stonpet:

  1. Sear the stonpet in a hot pan with moon-oil until the edges shimmer.
  2. Add star-lime, black salt, and a little fermented nebula paste.
  3. Lower the heat and let it braise for 8 cosmic minutes with a splash of comet broth.
  4. Finish with sky herbs and serve over warm asteroid rice.

  The vibe: crispy outside, tender center, deep interdimensional savor.

• Running Stop hook: updating the guards

Stop hook (completed)
  warning: Wizard Tower Stop hook saw a second pass and stayed calm to avoid a loop.

.codex/config.toml

[features]
codex_hooks = true

.codex/hooks.json

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup|resume",
        "hooks": [
          {
            "type": "command",
            "command": "/usr/bin/python3 .codex/hooks/session_start_demo.py",
            "timeoutSec": 10,
            "statusMessage": "lighting the observatory"
          }
        ]
      }
    ],
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "/usr/bin/python3 .codex/hooks/stop_demo_block.py",
            "timeoutSec": 10,
            "statusMessage": "updating the guards"
          }
        ]
      }
    ]
  }
}

.codex/hooks/session_start_demo.py

#!/usr/bin/env python3

import json
import sys
from pathlib import Path


def main() -> int:
    payload = json.load(sys.stdin)
    cwd = Path(payload.get("cwd", ".")).name or "wizard-tower"
    source = payload.get("source", "startup")
    source_label = "resume" if source == "resume" else "startup"
    source_prefix = (
        "Resume context:"
        if source == "resume"
        else "Startup context:"
    )

    output = {
        "systemMessage": (
            f"Hi, I'm a session start hook for {cwd} ({source_label})."
        ),
        "hookSpecificOutput": {
            "hookEventName": "SessionStart",
            "additionalContext": (
                f"{source_prefix} A wimboltine stonpet is an exotic cuisine from hyperspace"
            ),
        },
    }
    print(json.dumps(output))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())

.codex/hooks/stop_demo_block.py

#!/usr/bin/env python3

import json
import sys


def main() -> int:
    payload = json.load(sys.stdin)
    stop_hook_active = payload.get("stop_hook_active", False)
    last_assistant_message = payload.get("last_assistant_message") or ""
    char_count = len(last_assistant_message.strip())

    if stop_hook_active:
        system_message = (
            "Wizard Tower Stop hook saw a second pass and stayed calm to avoid a loop."
        )
        print(json.dumps({"systemMessage": system_message}))
    else:
        system_message = (
            f"Wizard Tower Stop hook continuing conversation"
        )
        print(json.dumps({"systemMessage": system_message, "decision": "block", "reason": "cook the stonpet"}))

    return 0


if __name__ == "__main__":
    raise SystemExit(main())

@etraut-openai etraut-openai added the oai PRs contributed by OpenAI employees label Mar 13, 2026
@eternal-openai eternal-openai changed the title [hooks] proper stop_hook_active mechanics [hooks] stop continuation & stop_hook_active mechanics Mar 13, 2026
@eternal-openai eternal-openai marked this pull request as ready for review March 13, 2026 18:32
@eternal-openai eternal-openai enabled auto-merge (squash) March 13, 2026 21:21
if stop_hook_active {
if let Some(continuation_prompt) = stop_outcome.continuation_prompt.clone()
{
let developer_message: ResponseItem =
Copy link
Copy Markdown
Collaborator

@pakrym-oai pakrym-oai Mar 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might need to start a turn with a user message instead of the developer message.

The user message will have to be wrapped into a tag so we can disambiguate between automatic and real user message.

}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn stop_hook_can_block_multiple_times_in_same_turn() -> Result<()> {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there no other hook tests? Suprised we just created this file.

},
"reason": {
"default": null,
"description": "Claude requires `reason` when `decision` is `block`; we enforce that semantic rule during output parsing rather than in the JSON schema.",
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we like this description?

} else {
None
};
let aggregate = aggregate_results(results.iter().map(|result| &result.data));
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like StopOutcome and StopHandlerData are the same modulo hook_events and we already pass results into aggregate_results. Should we just build the final StopOutcome there?

@eternal-openai eternal-openai merged commit 9a44a7e into main Mar 13, 2026
32 checks passed
@eternal-openai eternal-openai deleted the eternal/hooks_stop_hook_active branch March 13, 2026 22:51
@github-actions github-actions bot locked and limited conversation to collaborators Mar 13, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

oai PRs contributed by OpenAI employees

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants