Expose code-mode parent provenance and terminal status to tool lifecycle hooks

Open 💬 0 comments Opened Jul 12, 2026 by dylanmccavitt

What variant of Codex are you using?

CLI and App Server code mode. Observed on codex-cli 0.144.0; re-checked against the rust-v0.144.1 patch and current main source on 2026-07-11.

What feature would you like to see?

Please expose runtime-owned source/parent provenance and structured terminal status across the existing PreToolUse / PostToolUse lifecycle for tools invoked from code-mode exec JavaScript.

Codex already knows this relationship internally. Nested calls are dispatched with ToolCallSource::CodeMode { cell_id, runtime_tool_call_id }, but the hook payload reduces that to the same Bash-shaped { tool_use_id, tool_name, tool_input, tool_response } used for direct calls. A hook therefore cannot establish that a particular exec_command was actually invoked by a code-mode exec cell. It also cannot obtain the structured terminal exit code from PostToolUse.

A minimal hook contract could carry fields along these lines (exact naming is not important):

{
  "tool_use_id": "opaque-runtime-correlation",
  "tool_name": "Bash",
  "tool_source": {
    "type": "code_mode",
    "parent_tool": "exec",
    "parent_tool_use_id": "opaque-parent-correlation"
  },
  "tool_status": {
    "state": "exited",
    "exit_code": 0
  }
}

tool_source should be present on both the start/pre event and its matching terminal/post event. tool_status belongs only on the terminal event. A command that initially yields a running session should remain open until write_stdin or the runtime's background watcher observes termination; that terminal event should retain the original nested tool ID and source correlation.

Acceptance criteria:

  • Direct tool calls remain distinguishable from code-mode nested calls without inspecting JavaScript source.
  • Every nested exec_command start has exactly one matching terminal disposition, correlated by runtime-owned opaque values.
  • Immediate and yielded commands expose structured terminal state and exit code only after actual process termination.
  • Continuation/poll completion retains the original nested tool ID and code-mode parent correlation.
  • Missing, duplicate, reordered, or conflicting lifecycle events can be detected and failed closed by consumers.
  • The contract does not require persisting JavaScript source, commands, outputs, paths, prompts, or rollout payloads.
  • Hook documentation and focused tests cover direct, nested immediate, nested yielded, nonzero exit, cancellation, and malformed lifecycle cases.

This would allow a local hook/collector to classify an approved verifier command transiently, project only aggregate evidence, and discard the raw hook payload and opaque correlations.

Additional information

A throwaway in-memory state-model spike tested immediate success, nonzero exit, yielded completion, missing completion, completion without start, duplicate, reordered, conflicting, and privacy-bearing events.

The key finding was that a completion-only observation is insufficient: without a runtime-owned start plus correlated terminal event, a consumer cannot distinguish “no verifier ran” from “a verifier ran but completion was lost” without parsing model-authored JavaScript. The two-event contract handled the valid cases and failed closed for every anomaly while retaining only { verifier, version, proofClass, passed } after projection.

Relevant existing issues:

  • #24175 requests structured shell status in PostToolUse, but not code-mode source/parent correlation.
  • #23411 requests PreToolUse for the outer code-mode exec, but intentionally leaves PostToolUse and nested lifecycle provenance out of scope.
  • #16246 fixed/covered terminal PostToolUse after write_stdin, including reuse of the original tool call ID; this request asks that code-mode source correlation and structured status survive that same path.

Current source seams:

  • Nested dispatch retains cell_id and runtime_tool_call_id: codex-rs/core/src/tools/code_mode/mod.rs.
  • Hook dispatch currently forwards only tool ID/name/input/response: codex-rs/core/src/tools/registry.rs.
  • Unified exec hook payload currently presents HookToolName::bash() and response content: codex-rs/core/src/tools/handlers/unified_exec.rs.
  • App Server commandExecution has structured exitCode, but no nested code-mode parent linkage.
  • The diagnostic rollout-trace model has both kinds of data, but it is intentionally raw and is not an appropriate privacy-minimized collector contract.

View original on GitHub ↗