43 Commits

  • test: branch on target OS instead of runner flavor (#29712)
    ## Why
    
    Core tests should branch on the executor's operating system, not on
    runner details such as Docker or Wine. This keeps platform behavior
    stable as new test backends are added and reserves Wine-specific skips
    for actual runner debt.
    
    ## What
    
    - Add `TestTargetOs` and target/host-aware skip helpers while keeping
    `TestEnvironment` internal.
    - Replace topology enum access with remote predicates and a narrow
    Docker accessor.
    - Migrate OS-semantic Wine skips, preserve runner-specific gaps, and
    document the skip taxonomy.
    
    ## Validation
    
    - `just test -p core_test_support`
    - `just test -p codex-core
    remote_test_env_can_connect_and_use_filesystem`
    - `bazel test //codex-rs/core:core-all-wine-exec-test
    --test_output=errors` reached test execution; unrelated existing
    view-image, path, and timing failures remain.
    - `just test -p codex-core` and `just test` reached broad test
    execution; this checkout has unrelated helper, sandbox, and timing
    failures.
  • chore(core) rm AskForApproval::OnFailure (#28418)
    ## Summary
    Deletes the OnFailure variant of the `AskForApproval` enum. This option
    has been deprecated since #11631.
    
    ## Testing
    - [x] Tests pass
  • [codex] Use expect in integration tests (#28441)
    The workspace denies `clippy::expect_used` in production. Although
    `clippy.toml` allows `expect` in tests, Bazel Clippy compiles
    integration-test helper code in a way that does not receive that
    exemption, which encouraged verbose `unwrap_or_else(... panic!(...))`
    and equivalent `match`/`let else` forms.
    
    This allows `clippy::expect_used` once at each integration-test crate
    root (including aggregated suites and test-support libraries), then
    replaces manual panic-based Result and Option unwraps with
    `expect`/`expect_err`. Standalone `tests/*.rs` files remain their own
    crate roots. Intentional assertion and unexpected-variant panics remain
    unchanged, and the production `expect_used = "deny"` lint remains in
    place.
    
    The cleanup is mechanical and net-negative in line count.
  • Respect blocking PostToolUse hooks in code mode (#28365)
    ## Summary
    
    Make blocking hook behavior reliable for tools invoked from code mode.
    
    Previously, a `PostToolUse` hook could block a completed tool result,
    but code mode would still return the original typed result to
    JavaScript. The hook appeared blocked in hook telemetry while the
    running script continued with the result.
    
    This change:
    
    - rejects the nested JavaScript tool promise when `PostToolUse` blocks
    - normalizes `decision: "block"` and exit code 2 to the same blocking
    behavior
    - surfaces the hook feedback as the rejected promise's error
    - adds end-to-end coverage for the relevant PreToolUse and PostToolUse
    interactions
    
    ## Hook semantics in code mode
    
    | Hook behavior | Code-mode result |
    |---|---|
    | PreToolUse block | Reject the promise before the tool executes |
    | PreToolUse `updatedInput` | Execute the rewritten invocation and
    return its result |
    | PostToolUse `decision: "block"` | Execute the tool, then reject the
    promise with the hook reason |
    | PostToolUse exit code 2 | Same behavior as `decision: "block"` |
    | PostToolUse `continue: false` | Preserve the existing feedback-only
    behavior; do not reject the promise |
    
    ## Test coverage
    
    Added or strengthened end-to-end coverage proving that:
    
    - a PreToolUse block rejects the JavaScript promise before execution
    - a PreToolUse input rewrite executes only the rewritten command
    - JavaScript receives the rewritten command's result
    - PostToolUse `decision: "block"` rejects after the command executes
    - PostToolUse exit code 2 has the same behavior
    - the hook observes the original completed tool response
    - the blocked original result does not reach JavaScript
    - existing direct-mode replacement behavior remains intact
    - `continue: false` without a reason produces deterministic fallback
    feedback
  • core: enable remote compaction v2 by default (#27573)
    ## Why
    
    Remote compaction v2 is ready to become the default for providers that
    already support remote compaction. Leaving it behind an
    under-development opt-in keeps eligible sessions on the legacy
    remote-compaction path.
    
    This does not broaden provider eligibility: OpenAI and Azure move to v2,
    while Bedrock and OSS providers retain their existing local-compaction
    behavior.
    
    ## What changed
    
    - Mark `remote_compaction_v2` stable and enable it by default.
    - Make tests that intentionally cover legacy remote compaction
    explicitly disable v2.
    - Update parity coverage so v2 exercises the production default and only
    legacy mode opts out.
    
    ## Verification
    
    - `just test -p codex-core
    auto_compact_runs_after_resume_when_token_usage_is_over_limit
    auto_compact_counts_encrypted_reasoning_before_last_user
    auto_compact_runs_when_reasoning_header_clears_between_turns
    responses_lite_compact_request_uses_lite_transport_contract`
  • Pair thread environment settings (#26687)
    ## Why
    
    Thread cwd and environment selections are a single logical setting in
    core: updating one without the other can silently desynchronize the
    next-turn execution context. This change makes that relationship
    explicit in the internal thread settings flow while preserving the
    existing app-server public API shape.
    
    ## What changed
    
    - Moved the cwd/environment pair through internal
    `ThreadSettingsOverrides.environment_settings` instead of a top-level
    internal `cwd` field.
    - Kept `thread/settings/update` public params unchanged, with app-server
    translating top-level `cwd` into the paired internal settings shape.
    - Moved `Op::UserInput` environment overrides into thread settings so
    user turns and settings updates use the same core path.
    - Updated core, app-server, MCP, memories, sample, and test callsites to
    construct the paired settings shape.
    
    ## Verification
    
    - `git diff --check`
    - Local test run starting after PR creation.
  • Switch runtime to cloud config bundle (#24622)
    ## Summary
    
    - Adapts the moved `codex-cloud-config` crate from the legacy cloud
    requirements endpoint to the new config bundle endpoint.
    - Switches runtime consumers from `CloudRequirementsLoader` to
    `CloudConfigBundleLoader` so one shared bundle supplies cloud-delivered
    config and requirements.
    - Removes the legacy cloud requirements domain loader path.
    
    ## Details
    
    This intentionally keeps `codex-cloud-config` monolithic for review
    lineage: the previous PR establishes the crate move, and this PR shows
    the behavior change against that moved implementation. A follow-up PR
    splits the module back into focused files.
    
    The new bundle path preserves the important cloud requirements loader
    semantics where intended: account-scoped signed cache, 30 minute TTL, 5
    minute refresh cadence, retry/backoff, auth recovery, and fail-closed
    startup loading. The cached payload changes from a single requirements
    TOML string to the backend-delivered bundle, and validation rejects
    malformed config or requirements fragments before cache write/use.
  • Add experimental turn additional context (#24154)
    ## Summary
    
    Adds experimental `additionalContext` support to `turn/start` and
    `turn/steer` so clients can provide ephemeral external context, such as
    browser or automation state, without turning that plumbing into a
    visible user prompt or triggering user-prompt lifecycle behavior.
    
    ## API Shape
    
    The parameter shape is:
    
    ```ts
    additionalContext?: Record<string, {
      value: string
      kind: "untrusted" | "application"
    }> | null
    ```
    
    Example:
    
    ```json
    {
      "additionalContext": {
        "browser_info": {
          "value": "Active tab is CI failures.",
          "kind": "untrusted"
        },
        "automation_info": {
          "value": "CI rerun is in progress.",
          "kind": "application"
        }
      }
    }
    ```
    
    The keys are opaque and caller-defined.
    
    ## Context Injection
    
    When provided, accepted entries are inserted into model context as
    hidden contextual message items, not as visible thread user-message
    items.
    
    `kind: "untrusted"` entries are inserted with role `user`:
    
    ```text
    <external_${key}>${value}</external_${key}>
    ```
    
    `kind: "application"` entries are inserted with role `developer`:
    
    ```text
    <${key}>${value}</${key}>
    ```
    
    Values are not escaped. Each value is truncated to 1k approximate tokens
    before wrapping.
    
    For `turn/start`, accepted additional context is inserted before normal
    user input. For `turn/steer`, additional context is merged only when the
    steer includes non-empty user input; context-only steers still reject as
    empty input.
    
    ## Dedupe Strategy
    
    `AdditionalContextStore` lives on session state and stores the latest
    complete additional-context map.
    
    Each `turn/start` or non-empty `turn/steer` treats its
    `additionalContext` as the current complete set of values. Entries are
    injected only when the key is new or the exact entry for that key
    changed, including `value` or `kind`. After merging, the store is
    replaced with the provided map, so omitted keys are removed from the
    retained set and can be injected again later if reintroduced.
    
    Omitting `additionalContext`, passing `null`, or passing an empty object
    resets the store to empty and injects nothing.
    
    ## What Changed
    
    - Threads experimental v2 `additionalContext` through app-server into
    core turn start and steer handling.
    - Adds separate contextual fragment types for untrusted user-role
    context and application developer-role context.
    - Uses pending response input items so additional context can be
    combined with normal user input without treating it as prompt text.
    - Adds integration coverage for start/steer flow, role routing,
    dedupe/reset behavior, deletion/re-add behavior, hook-blocked input
    behavior, empty context-only steer rejection, external-fragment marker
    matching, and truncation.
  • Default function tools into tool hooks (#23757)
    # Why
    
    `PreToolUse`, `PostToolUse`, and `updatedInput` coverage for local
    function tools currently depends on each handler remembering to wire up
    the hook contract itself. That makes coverage easy to miss as new
    function tools are added, even though most of them share the same basic
    shape: a model-facing function call with JSON arguments.
    
    # What
    
    This makes `CoreToolRuntime` provide the default hook contract for
    ordinary local function tools:
    
    - build generic `PreToolUse` and `PostToolUse` payloads from the
    function tool name and arguments
    - apply `updatedInput` rewrites back into function-tool arguments
    through the same default path
    - let tool outputs override the post-hook input or response when they
    have a more stable hook-facing contract
    
    The exceptions stay explicit:
    
    - hosted tools remain outside the generic local function path
    - code-mode `wait` and `write_stdin` opt out for now
    - `PostToolUse` feedback replaces only the model-visible response, so
    code mode keeps its typed tool result
    
    With the generic path in place, the MCP and extension-tool adapters no
    longer need their own duplicate pre/post hook plumbing. The new coverage
    exercises the registry default plus end-to-end local function behavior
    for pre-hook blocking, `updatedInput` rewriting, and post-hook context.
  • Remove plugin hooks feature flag (#22552)
    # Why
    
    This is a follow-up stacked on top of the `plugin_hooks` default-on
    change. Once we are comfortable making plugin hooks part of the normal
    plugin behavior, the separate feature flag stops buying us much and
    leaves extra branching/cache state behind.
    
    # What
    
    - remove the `PluginHooks` feature and generated config-schema entries
    - make plugin hook loading/listing follow plugin enablement directly
    - drop plugin-manager cache/state that only existed to distinguish
    hook-flag toggles
    - remove tests and fixtures that modeled `plugin_hooks = true/false`
  • Support compact SessionStart hooks (#21272)
    # Why
    
    Compaction replaces the live conversation history, so hooks that use
    `SessionStart` to re-inject durable model context need a way to run
    again after that rewrite.
    
    Related - #19905 adds dedicated compact lifecycle hooks
    
    # What
    
    - add `compact` as a supported `SessionStart` source and matcher value
    - change pending `SessionStart` state from a single slot to a small FIFO
    queue so `resume` / `startup` / `clear` can be preserved alongside a
    later `compact`
    - drain all queued `SessionStart` sources before the next model request,
    preserving their original order
    
    # Testing
    
    The new integration coverage verifies both the basic `compact` matcher
    path and the stacked `resume` -> `compact` case where both hooks
    contribute `additionalContext` to the next model turn.
  • [1 of 7] Add thread settings to UserInput (#23080)
    **Stack position:** [1 of 7]
    
    ## Summary
    
    The first three PRs in this stack are a cleanup pass before the actual
    thread settings API work.
    
    Today, core has several overlapping "user input" ops: `UserInput`,
    `UserInputWithTurnContext`, and `UserTurn`. They differ mostly in how
    much next-turn state they carry, which makes the later queued thread
    settings update harder to reason about and review.
    
    This PR starts that cleanup by adding the shared
    `ThreadSettingsOverrides` payload and allowing `Op::UserInput` to carry
    it. Existing variants remain in place here, so this layer is mostly a
    behavior-preserving API shape change plus mechanical constructor
    updates.
    
    ## End State After PR3
    
    By the end of PR3, `Op::UserInput` is the only "user input" core op. It
    can carry optional thread settings overrides for callers that need to
    update stored defaults with a turn, while callers without updates use
    empty settings. `Op::UserInputWithTurnContext` and `Op::UserTurn` are
    deleted.
    
    ## End State After PR5
    
    By the end of PR5, core will have only two ops for this area:
    
    - `Op::UserInput` for user-input-bearing submissions.
    - `Op::ThreadSettings` for settings-only updates.
    
    ## Stack
    
    1. [1 of 7] [Add thread settings to
    UserInput](https://github.com/openai/codex/pull/23080) (this PR)
    2. [2 of 7] [Remove
    UserInputWithTurnContext](https://github.com/openai/codex/pull/23081)
    3. [3 of 7] [Remove
    UserTurn](https://github.com/openai/codex/pull/23075)
    4. [4 of 7] [Placeholder for OverrideTurnContext
    cleanup](https://github.com/openai/codex/pull/23087)
    5. [5 of 7] [Replace OverrideTurnContext with
    ThreadSettings](https://github.com/openai/codex/pull/22508)
    6. [6 of 7] [Add app-server thread settings
    API](https://github.com/openai/codex/pull/22509)
    7. [7 of 7] [Sync TUI thread
    settings](https://github.com/openai/codex/pull/22510)
  • [codex] Remove legacy shell output formatting paths (#22706)
    ## Why
    
    The client and tool pipeline still carried compatibility code for legacy
    structured shell output. Current shell and apply_patch responses are
    already plain text for model consumption, so keeping a
    JSON-serialization path plus shell-item rewrite logic makes the request
    formatter and tests preserve a format we do not need anymore.
    
    ## What Changed
    
    - Removed the client-side shell output rewrite from
    `core/src/client_common.rs`.
    - Removed the structured exec-output formatter and the shell `freeform`
    switch so tool emitters use one model-facing formatter.
    - Collapsed apply_patch/shell serialization tests around the remaining
    plain-text output expectations and removed duplicate one-variant
    parameterized cases.
    - Kept the `ApplyPatchModelOutput::ShellCommandViaHeredoc` compatibility
    input shape, but no longer treats it as a separate output-format mode.
    
    ## Validation
    
    - `cargo test -p codex-core client_common`
    - `cargo test -p codex-core shell_serialization`
    - `cargo test -p codex-core apply_patch_cli`
    - `just fix -p codex-core`
    
    ## Documentation
    
    No external Codex documentation update is needed.
  • chore(features) rm Feature::ApplyPatchFreeform (#22711)
    ## Summary
    Removes the feature since this is effectively on by default in all cases
    where we should use it, or can be configured via models.json.
    
    ## Testing
    - [x] unit tests pass
  • Spill oversized PreToolUse additionalContext (#22529)
    # Why
    
    `PreToolUse.additionalContext` became model-visible after #20692, but
    the hook-output spilling path from #21069 never picked up that newer
    lane. As a result, oversized `PreToolUse` context could bypass the
    truncation/spill treatment that already applies to the other hook
    outputs Codex forwards to the model.
    
    # What
    
    - Run `PreToolUseOutcome.additional_contexts` through
    `maybe_spill_texts(...)`
    - Add an integration test proving a large `PreToolUse.additionalContext`
    is replaced with a truncated preview plus spill-file pointer, while the
    full text is preserved on disk.
  • [codex] Remove unused legacy shell tools (#22246)
    ## Why
    
    Recent session history showed no active use of the raw `shell`,
    `local_shell`, or `container.exec` execution surfaces. Keeping those
    handlers/specs wired into core leaves duplicate shell execution paths
    alongside the supported `shell_command` and unified exec tools.
    
    ## What changed
    
    - Removed the raw `shell` handler/spec and its `ShellToolCallParams`
    protocol helper.
    - Removed the legacy `local_shell` and `container.exec` handler/spec
    plumbing while preserving persisted-history compatibility for old
    response items.
    - Normalized model/config `default` and `local` shell selections to
    `shell_command`.
    - Pruned tests that exercised removed raw-shell/local-shell/apply-patch
    variants and kept coverage on `shell_command`, unified exec, and
    freeform `apply_patch`.
    
    ## Verification
    
    - `git diff --check`
    - `cargo test -p codex-protocol`
    - `cargo test -p codex-tools`
    - `cargo test -p codex-core tools::handlers::shell`
    - `cargo test -p codex-core tools::spec`
    - `cargo test -p codex-core tools::router`
    - `cargo test -p codex-core
    active_call_preserves_triggering_command_context`
    - `cargo test -p codex-core guardian_tests`
    - `cargo test -p codex-core --test all shell_serialization`
    - `cargo test -p codex-core --test all apply_patch_cli`
    - `cargo test -p codex-core --test all shell_command_`
    - `cargo test -p codex-core --test all local_shell`
    - `cargo test -p codex-core --test all otel::`
    - `cargo test -p codex-core --test all hooks::`
    - `just fix -p codex-core`
    - `just fix -p codex-tools`
  • Support PreToolUse updatedInput rewrites (#20527)
    ## Why
    
    `PreToolUse` already exposes `updatedInput` in its hook output schema,
    but Codex currently rejects it instead of applying the rewrite. That
    leaves hook authors unable to make the documented pre-execution
    adjustment to a tool call before it runs.
    
    ## What
    
    - Accept `updatedInput` from `PreToolUse` hooks when paired with
    `permissionDecision: "allow"`.
    - Apply the rewritten input before dispatch so the tool executes the
    updated payload, not the original one.
    - Preserve the stable hook-facing compatibility shapes that
    participating tool handlers expose:
    - Bash-like tools (`shell`, `container.exec`, `local_shell`,
    `shell_command`, `exec_command`) use `{ "command": ... }`.
    - `apply_patch` exposes its patch body through the same command-shaped
    hook contract.
      - MCP tools expose their JSON argument object directly.
    - Keep each participating tool handler responsible for translating
    hook-facing `updatedInput` back into its concrete invocation shape.
    
    ## Verification
    
    Direct Bash-like rewrite coverage:
    
    - `pre_tool_use_rewrites_shell_before_execution`
    - `pre_tool_use_rewrites_container_exec_before_execution`
    - `pre_tool_use_rewrites_local_shell_before_execution`
    - `pre_tool_use_rewrites_shell_command_before_execution`
    - `pre_tool_use_rewrites_exec_command_before_execution`
    
    These cases assert that each supported Bash-like surface runs only the
    rewritten command while the hook still observes the original `{
    "command": ... }` input.
    
    `pre_tool_use_rewrites_apply_patch_before_execution`
    
    - Model emits one patch.
    - Hook swaps in a different patch.
    - Asserts only the rewritten file is created, and the hook saw the
    original patch.
    
    `pre_tool_use_rewrites_code_mode_nested_exec_command_before_execution`
    
    - Model runs one nested shell command from code mode.
    - Hook rewrites it.
    - Asserts only the rewritten command runs, and the hook saw the original
    nested input.
    
    `pre_tool_use_rewrites_mcp_tool_before_execution`
    
    - Model calls the RMCP echo tool.
    - Hook rewrites the MCP arguments.
    - Asserts the MCP server receives and returns the rewritten message, not
    the original one.
  • [codex] Delete function-style apply_patch (#21651)
    ## Why
    
    `apply_patch` is now a freeform/custom tool. Keeping the old
    JSON/function-style registration and parsing path left another way for
    models and tests to invoke `apply_patch`, which made the tool surface
    harder to reason about.
    
    ## What changed
    
    - Removed the `ApplyPatchToolType::Function` variant, JSON `apply_patch`
    spec, and handler support for function payloads.
    - Kept `apply_patch_tool_type = freeform` as the supported model
    metadata path, including Bedrock catalog metadata.
    - Migrated `apply_patch` tests and SSE fixtures to custom/freeform tool
    calls.
    
    ## Verification
    
    - `cargo test -p codex-tools -p codex-protocol -p codex-model-provider`
    - `cargo test -p codex-core tools::handlers::apply_patch --lib`
    - `cargo test -p codex-core --test all
    apply_patch_tool_executes_and_emits_patch_events`
    - `cargo test -p codex-core --test all
    apply_patch_reports_parse_diagnostics`
    - `cargo test -p codex-exec test_apply_patch_tool`
    - `just fix -p codex-core`
    - `just fix -p codex-tools -p codex-protocol -p codex-model-provider -p
    codex-exec`
  • hook trust metadata and enforcement (#20321)
    # Why
    
    We want shared hook trust that both the app and the TUI can build on,
    but the metadata is only useful if runtime behavior agrees with it. This
    PR adds a single backend trust model for hooks so unmanaged hooks cannot
    run until the current definition has been reviewed, while managed hooks
    remain runnable and non-configurable.
    
    # What
    
    - persist `trusted_hash` alongside hook state in `config.toml`
    - expose `currentHash` and derived `trustStatus` through `hooks/list`
    - derive trust from normalized hook definitions so equivalent hooks from
    `config.toml` and `hooks.json` share the same trust identity
    - gate unmanaged hooks on trust before they enter the runnable handler
    set
    
    # Reviewer Notes
    
    - key file to review is `codex-rs/hooks/src/engine/discovery.rs`
    - the only **core** change is schema related
  • Support PreToolUse additionalContext (#20692)
    # Why
    
    `PreToolUse` already exposes `hookSpecificOutput.additionalContext` in
    the generated hook schema, but the runtime still rejected it as
    unsupported. That leaves `PreToolUse` out of step with the other
    context-injecting hooks and prevents hook authors from attaching
    model-visible guidance to a pending tool call before it runs.
    
    # What
    
    - Parse `PreToolUse.additionalContext` and carry it through the hook
    event pipeline.
    - Record `PreToolUse` context at the hook boundary so successful context
    is preserved for both allowed and blocked calls without widening the
    tool registry surface.
    - Preserve existing deny behavior when context is combined with either
    `permissionDecision: "deny"` or the legacy `decision: "block"` shape.
  • Spill large hook outputs from context (#21069)
    ## Why
    
    Large hook outputs can enter model-visible context through hook-specific
    paths such as `additionalContext` and `Stop` continuation prompts.
    Without a dedicated cap, one hook can inject a large blob directly into
    conversation history instead of leaving a bounded preview for the model
    and preserving the full text elsewhere.
    
    ## What
    
    - spill hook text once it exceeds a fixed `2_500`-token budget,
    preserving the full output on disk and leaving a head/tail preview plus
    saved path in context
    - add shared hook-output spilling under
    `CODEX_HOME/hook_outputs/<thread_id>/<uuid>.txt`
    - apply the cap to both `additionalContext`, `feedback_message`, and
    `Stop` continuation fragments
  • Alias codex_hooks feature as hooks (#20522)
    # Why
    
    The hooks feature flag should use the concise canonical name `hooks`,
    while existing configs that still use `codex_hooks` continue to work
    during the rename.
    
    # What
    
    - change the canonical `Feature::CodexHooks` key from `codex_hooks` to
    `hooks`
    - register `codex_hooks` through the existing legacy-alias path
    - update the config schema and canonical config fixtures to prefer
    `hooks`
    - add regression coverage that both `hooks` and `codex_hooks` resolve to
    `Feature::CodexHooks`
    
    # Verification
    
    - `cargo test -p codex-features`
    - `cargo test -p codex-core config::schema_tests`
    - `cargo test -p codex-core
    pre_tool_use_blocks_shell_when_defined_in_config_toml`
    - `cargo test -p codex-app-server
    hooks_list_uses_each_cwds_effective_feature_enablement`
  • core tests: migrate hook turns to profiles (#20041)
    ## Summary
    - Removes `SandboxPolicy` from the hooks test suite.
    - Submits hook-related turns with explicit `PermissionProfile` values
    for disabled, read-only, and workspace-write cases.
    - Preserves the managed-network hook test by configuring and submitting
    a workspace-write profile with enabled network, allowing the existing
    requirements-backed proxy path to remain covered.
    
    ## Verification
    - `cargo check -p codex-core --tests`
    - `just fmt`
  • Discover hooks bundled with plugins (#19705)
    ## Why
    
    Plugins can bundle lifecycle hooks, but Codex previously only discovered
    hooks from user, project, and managed config layers. This adds the
    plugin discovery and runtime plumbing needed for plugin-bundled hooks
    while keeping execution behind the `plugin_hooks` feature flag.
    
    ## What
    
    - Discovers plugin hook sources from each plugin's default
    `hooks/hooks.json`.
    - Supports `plugin.json` manifest `hooks` entries as either relative
    paths or inline hook objects.
    - Plumbs discovered plugin hook sources through plugin loading into the
    hook runtime when `plugin_hooks` is enabled.
    - Marks plugin-originated hook runs as `HookSource::Plugin`.
    - Injects `PLUGIN_ROOT` and `CLAUDE_PLUGIN_ROOT` into plugin hook
    command environments.
    - Updates generated schemas and hook source metadata for the plugin hook
    source.
    
    ## Stack
    
    1. This PR - openai/codex#19705
    2. openai/codex#19778
    3. openai/codex#19840
    4. openai/codex#19882
    
    ## Reviewer Notes
    
    - Core logic is in `codex-rs/core-plugins/src/loader.rs` and
    `codex-rs/hooks/src/engine/discovery.rs`
    - Moved existing / adding new tests to
    `codex-rs/core-plugins/src/loader_tests.rs` hence the large diff there
    - Otherwise mostly plumbing and minor schema updates
    
    ### Core Changes
    
    The `codex-rs/core` changes are limited to wiring plugin hook support
    into existing core flows:
    
    - `core/src/session/session.rs` conditionally pulls effective plugin
    hook sources and plugin hook load warnings from `PluginsManager` when
    `plugin_hooks` is enabled, then passes them into `HooksConfig`.
    - `core/src/hook_runtime.rs` adds the `plugin` metric tag for
    `HookSource::Plugin`.
    - `core/config.schema.json` picks up the new `plugin_hooks` feature
    flag, and `core/src/plugins/manager_tests.rs` updates fixtures for the
    added plugin hook fields.
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • permissions: add built-in default profiles (#19900)
    ## Why
    
    The migration away from `SandboxPolicy` needs new configs to start from
    permissions profiles instead of deriving profiles from legacy sandbox
    modes. Existing users can have empty `config.toml` files, and we should
    not rewrite user-owned config files that may live in shared
    repositories.
    
    This PR introduces built-in profile names so an empty config can resolve
    to a canonical `PermissionProfile`, while explicit named `[permissions]`
    profiles still behave predictably.
    
    ## What changed
    
    - Adds built-in `default_permissions` profile names:
      - `:read-only` maps to `PermissionProfile::read_only()`.
    - `:workspace` maps to the workspace-write profile, including
    project-root metadata carveouts.
    - `:danger-no-sandbox` maps to `PermissionProfile::Disabled`, preserving
    the distinction between no sandbox and a broad managed sandbox.
    - Reserves the `:` prefix for built-in profiles so user-defined
    `[permissions]` profiles cannot collide with future built-ins.
    - Allows `default_permissions` to reference a built-in profile without
    requiring a `[permissions]` table.
    - Makes an otherwise empty config choose a built-in profile by
    trust/platform context: trusted or untrusted project roots use
    `:workspace` when the platform supports that sandbox, while roots
    without a trust decision use `:read-only`.
    - Keeps legacy `sandbox_mode` configs on the legacy path, and still
    rejects user-defined `[permissions]` profiles that omit
    `default_permissions` so we do not silently guess among custom profiles.
    - Preserves compatibility behavior for implicit defaults: bare
    `network.enabled = true` allows runtime network without starting the
    managed proxy, explicit profile proxy policy still starts the proxy, and
    implicit workspace/add-dir roots keep legacy metadata carveouts.
    
    ## Verification
    
    - `cargo test -p codex-core builtin --lib`
    - `cargo test -p codex-core profile_network_proxy_config`
    - `cargo test -p codex-core
    implicit_builtin_workspace_profile_preserves_add_dir_metadata_carveouts`
    - `cargo test -p codex-core
    permissions_profiles_network_enabled_allows_runtime_network_without_proxy`
    - `cargo test -p codex-core
    permissions_profiles_proxy_policy_starts_managed_network_proxy`
    
    ## Documentation
    
    Public Codex config docs should mention these built-in names when the
    `[permissions]` config format is ready to document as stable.
    
    
    
    
    
    
    
    
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/19900).
    * #20041
    * #20040
    * #20037
    * #20035
    * #20034
    * #20033
    * #20032
    * #20030
    * #20028
    * #20027
    * #20026
    * #20024
    * #20021
    * #20018
    * #20016
    * #20015
    * #20013
    * #20011
    * #20010
    * #20008
    * __->__ #19900
  • [codex] Move config loading into codex-config (#19487)
    ## Why
    
    Config loading had become split across crates: `codex-config` owned the
    config types and merge logic, while `codex-core` still owned the loader
    that assembled the layer stack. This change consolidates that
    responsibility in `codex-config`, so the crate that defines config
    behavior also owns how configs are discovered and loaded.
    
    To make that move possible without reintroducing the old dependency
    cycle, the shell-environment policy types and helpers that
    `codex-exec-server` needs now live in `codex-protocol` instead of
    flowing through `codex-config`.
    
    This also makes the migrated loader tests more deterministic on machines
    that already have managed or system Codex config installed by letting
    tests override the system config and requirements paths instead of
    reading the host's `/etc/codex`.
    
    ## What Changed
    
    - moved the config loader implementation from `codex-core` into
    `codex-config::loader` and deleted the old `core::config_loader` module
    instead of leaving a compatibility shim
    - moved shell-environment policy types and helpers into
    `codex-protocol`, then updated `codex-exec-server` and other downstream
    crates to import them from their new home
    - updated downstream callers to use loader/config APIs from
    `codex-config`
    - added test-only loader overrides for system config and requirements
    paths so loader-focused tests do not depend on host-managed config state
    - cleaned up now-unused dependency entries and platform-specific cfgs
    that were surfaced by post-push CI
    
    ## Testing
    
    - `cargo test -p codex-config`
    - `cargo test -p codex-core config_loader_tests::`
    - `cargo test -p codex-protocol -p codex-exec-server -p
    codex-cloud-requirements -p codex-rmcp-client --lib`
    - `cargo test --lib -p codex-app-server-client -p codex-exec`
    - `cargo test --no-run --lib -p codex-app-server`
    - `cargo test -p codex-linux-sandbox --lib`
    - `cargo shear`
    - `just bazel-lock-check`
    
    ## Notes
    
    - I did not chase unrelated full-suite failures outside the migrated
    loader surface.
    - `cargo test -p codex-core --lib` still hits unrelated proxy-sensitive
    failures on this machine, and Windows CI still shows unrelated
    long-running/timeouting test noise outside the loader migration itself.
  • permissions: remove legacy read-only access modes (#19449)
    ## Why
    
    `ReadOnlyAccess` was a transitional legacy shape on `SandboxPolicy`:
    `FullAccess` meant the historical read-only/workspace-write modes could
    read the full filesystem, while `Restricted` tried to carry partial
    readable roots. The partial-read model now belongs in
    `FileSystemSandboxPolicy` and `PermissionProfile`, so keeping it on
    `SandboxPolicy` makes every legacy projection reintroduce lossy
    read-root bookkeeping and creates unnecessary noise in the rest of the
    permissions migration.
    
    This PR makes the legacy policy model narrower and explicit:
    `SandboxPolicy::ReadOnly` and `SandboxPolicy::WorkspaceWrite` represent
    the old full-read sandbox modes only. Split readable roots, deny-read
    globs, and platform-default/minimal read behavior stay in the runtime
    permissions model.
    
    ## What changed
    
    - Removes `ReadOnlyAccess` from
    `codex_protocol::protocol::SandboxPolicy`, including the generated
    `access` and `readOnlyAccess` API fields.
    - Updates legacy policy/profile conversions so restricted filesystem
    reads are represented only by `FileSystemSandboxPolicy` /
    `PermissionProfile` entries.
    - Keeps app-server v2 compatible with legacy `fullAccess` read-access
    payloads by accepting and ignoring that no-op shape, while rejecting
    legacy `restricted` read-access payloads instead of silently widening
    them to full-read legacy policies.
    - Carries Windows sandbox platform-default read behavior with an
    explicit override flag instead of depending on
    `ReadOnlyAccess::Restricted`.
    - Refreshes generated app-server schema/types and updates tests/docs for
    the simplified legacy policy shape.
    
    ## Verification
    
    - `cargo check -p codex-app-server-protocol --tests`
    - `cargo check -p codex-windows-sandbox --tests`
    - `cargo test -p codex-app-server-protocol sandbox_policy_`
    
    
    ---
    [//]: # (BEGIN SAPLING FOOTER)
    Stack created with [Sapling](https://sapling-scm.com). Best reviewed
    with [ReviewStack](https://reviewstack.dev/openai/codex/pull/19449).
    * #19395
    * #19394
    * #19393
    * #19392
    * #19391
    * __->__ #19449
  • codex: support hooks in config.toml and requirements.toml (#18893)
    ## Summary
    
    Support the existing hooks schema in inline TOML so hooks can be
    configured from both `config.toml` and enterprise-managed
    `requirements.toml` without requiring a separate `hooks.json` payload.
    
    This gives enterprise admins a way to ship managed hook policy through
    the existing requirements channel while still leaving script delivery to
    MDM or other device-management tooling, and it keeps `hooks.json`
    working unchanged for existing users.
    
    This also lays the groundwork for follow-on managed filtering work such
    as #15937, while continuing to respect project trust gating from #14718.
    It does **not** implement `allow_managed_hooks_only` itself.
    
    NOTE: yes, it's a bit unfortunate that the toml isn't formatted as
    closely as normal to our default styling. This is because we're trying
    to stay compatible with the spec for plugins/hooks that we'll need to
    support & the main usecase here is embedding into requirements.toml
    
    ## What changed
    
    - moved the shared hook serde model out of `codex-rs/hooks` into
    `codex-rs/config` so the same schema can power `hooks.json`, inline
    `config.toml` hooks, and managed `requirements.toml` hooks
    - added `hooks` support to both `ConfigToml` and
    `ConfigRequirementsToml`, including requirements-side `managed_dir` /
    `windows_managed_dir`
    - treated requirements-managed hooks as one constrained value via
    `Constrained`, so managed hook policy is merged atomically and cannot
    drift across requirement sources
    - updated hook discovery to load requirements-managed hooks first, then
    per-layer `hooks.json`, then per-layer inline TOML hooks, with a warning
    when a single layer defines both representations
    - threaded managed hook metadata through discovered handlers and exposed
    requirements hooks in app-server responses, generated schemas, and
    `/debug-config`
    - added hook/config coverage in `codex-rs/config`, `codex-rs/hooks`,
    `codex-rs/core/src/config_loader/tests.rs`, and
    `codex-rs/core/tests/suite/hooks.rs`
    
    ## Testing
    
    - `cargo test -p codex-config`
    - `cargo test -p codex-hooks`
    - `cargo test -p codex-app-server config_api`
    
    ## Documentation
    
    Companion updates are needed in the developers website repo for:
    
    - the hooks guide
    - the config reference, sample, basic, and advanced pages
    - the enterprise managed configuration guide
    
    ---------
    
    Co-authored-by: Michael Bolin <mbolin@openai.com>
  • hooks: emit Bash PostToolUse when exec_command completes via write_stdin (#18888)
    Fixes #16246.
    
    ## Why
    
    `exec_command` already emits `PreToolUse`, but long-running unified exec
    commands that finish on a later `write_stdin` poll could miss the
    matching `PostToolUse`. That left the Bash hook lifecycle inconsistent,
    broke expectations around `tool_use_id` and `tool_input.command`, and
    meant `PostToolUse` block/replacement feedback could fail to replace the
    final session output before it reached model context.
    
    This keeps the fix scoped to the `exec_command` / `write_stdin`
    lifecycle. Broader non-Bash hook expansion is still out of scope here
    and remains tracked separately in #16732.
    
    ## What changed
    
    - Compute and store `PostToolUsePayload` while handlers still have
    access to their concrete output type, and carry `tool_use_id` through
    that payload.
    - Preserve the original hook-facing `exec_command` string through
    unified exec state (`ExecCommandRequest`, `ProcessEntry`,
    `PreparedProcessHandles`, and `ExecCommandToolOutput`) via
    `hook_command`, and remove the now-unused `session_command` output
    metadata.
    - Emit exactly one Bash `PostToolUse` for long-running `exec_command`
    sessions when a later `write_stdin` poll observes final completion,
    using the original `exec_command` call id and hook-facing command.
    - Keep one-shot `exec_command` behavior aligned with the same payload
    construction, including interactive completions that return a final
    result directly.
    - Apply `PostToolUse` block/replacement feedback before the final
    `write_stdin` completion output is sent back to the model.
    - Keep `write_stdin` itself out of `PreToolUse` matching so it continues
    to act as transport/polling for the original Bash tool call.
    - Restore plain matcher behavior for tool-name matchers such as `Bash`
    and `Edit|Write`, while still treating patterns with regex characters
    (for example `mcp__.*`) as regexes.
    - Add unit coverage for unified exec payload construction and parallel
    session separation, plus a core integration regression that verifies a
    blocked `PostToolUse` replaces the final `write_stdin` output in model
    context.
    
    ## Testing
    
    - `cargo test -p codex-hooks`
    - `cargo test -p codex-core post_tool_use_payload`
    - `cargo test -p codex-core
    post_tool_use_blocks_when_exec_session_completes_via_write_stdin`
  • fix(core): emit hooks for apply_patch edits (#18391)
    Fixes https://github.com/openai/codex/issues/16732.
    
    ## Why
    
    `apply_patch` is Codex's primary file edit path, but it was not emitting
    `PreToolUse` or `PostToolUse` hook events. That meant hook-based policy,
    auditing, and write coordination could observe shell commands while
    missing the actual file mutation performed by `apply_patch`.
    
    The issue also exposed that the hook runtime serialized command hook
    payloads with `tool_name: "Bash"` unconditionally. Even if `apply_patch`
    supplied hook payloads, hooks would either fail to match it directly or
    receive misleading stdin that identified the edit as a Bash tool call.
    
    ## What Changed
    
    - Added `PreToolUse` and `PostToolUse` payload support to
    `ApplyPatchHandler`.
    - Exposed the raw patch body as `tool_input.command` for both
    JSON/function and freeform `apply_patch` calls.
    - Taught tool hook payloads to carry a handler-supplied hook-facing
    `tool_name`.
    - Preserved existing shell compatibility by continuing to emit `Bash`
    for shell-like tools.
    - Serialized the selected hook `tool_name` into hook stdin instead of
    hardcoding `Bash`.
    - Relaxed the generated hook command input schema so `tool_name` can
    represent tools other than `Bash`.
    
    ## Verification
    
    Added focused handler coverage for:
    
    - JSON/function `apply_patch` calls producing a `PreToolUse` payload.
    - Freeform `apply_patch` calls producing a `PreToolUse` payload.
    - Successful `apply_patch` output producing a `PostToolUse` payload.
    - Shell and `exec_command` handlers continuing to expose `Bash`.
    
    Added end-to-end hook coverage for:
    
    - A `PreToolUse` hook matching `^apply_patch$` blocking the patch before
    the target file is created.
    - A `PostToolUse` hook matching `^apply_patch$` receiving the patch
    input and tool response, then adding context to the follow-up model
    request.
    - Non-participating tools such as the plan tool continuing not to emit
    `PreToolUse`/`PostToolUse` hook events.
    
    Also validated manually with a live `codex exec` smoke test using an
    isolated temp workspace and temp `CODEX_HOME`. The smoke test confirmed
    that a real `apply_patch` edit emits `PreToolUse`/`PostToolUse` with
    `tool_name: "apply_patch"`, a shell command still emits `tool_name:
    "Bash"`, and a denying `PreToolUse` hook prevents the blocked patch file
    from being created.
  • Add turn-scoped environment selections (#18416)
    ## Summary
    - add experimental turn/start.environments params for per-turn
    environment id + cwd selections
    - pass selections through core protocol ops and resolve them with
    EnvironmentManager before TurnContext creation
    - treat omitted selections as default behavior, empty selections as no
    environment, and non-empty selections as first environment/cwd as the
    turn primary
    
    ## Testing
    - ran `just fmt`
    - ran `just write-app-server-schema`
    - not run: unit tests for this stacked PR
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • Update models.json (#18586)
    - Replace the active models-manager catalog with the deleted core
    catalog contents.
    - Replace stale hardcoded test model slugs with current bundled model
    slugs.
    - Keep this as a stacked change on top of the cleanup PR.
  • Add PermissionRequest hooks support (#17563)
    ## Why
    
    We need `PermissionRequest` hook support!
    
    Also addresses:
    - https://github.com/openai/codex/issues/16301
    - run a script on Hook to do things like play a sound to draw attention
    but actually no-op so user can still approve
    - can omit the `decision` object from output or just have the script
    exit 0 and print nothing
    - https://github.com/openai/codex/issues/15311
      - let the script approve/deny on its own
      - external UI what will run on Hook and relay decision back to codex
    
    
    ## Reviewer Note
    
    There's a lot of plumbing for the new hook, key files to review are:
    - New hook added in `codex-rs/hooks/src/events/permission_request.rs`
    - Wiring for network approvals
    `codex-rs/core/src/tools/network_approval.rs`
    - Wiring for tool orchestrator `codex-rs/core/src/tools/orchestrator.rs`
    - Wiring for execve
    `codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs`
    
    ## What
    
    - Wires shell, unified exec, and network approval prompts into the
    `PermissionRequest` hook flow.
    - Lets hooks allow or deny approval prompts; quiet or invalid hooks fall
    back to the normal approval path.
    - Uses `tool_input.description` for user-facing context when it helps:
      - shell / `exec_command`: the request justification, when present
      - network approvals: `network-access <domain>`
    - Uses `tool_name: Bash` for shell, unified exec, and network approval
    permission-request hooks.
    - For network approvals, passes the originating command in
    `tool_input.command` when there is a single owning call; otherwise falls
    back to the synthetic `network-access ...` command.
    
    <details>
    <summary>Example `PermissionRequest` hook input for a shell
    approval</summary>
    
    ```json
    {
      "session_id": "<session-id>",
      "turn_id": "<turn-id>",
      "transcript_path": "/path/to/transcript.jsonl",
      "cwd": "/path/to/cwd",
      "hook_event_name": "PermissionRequest",
      "model": "gpt-5",
      "permission_mode": "default",
      "tool_name": "Bash",
      "tool_input": {
        "command": "rm -f /tmp/example"
      }
    }
    ```
    
    </details>
    
    <details>
    <summary>Example `PermissionRequest` hook input for an escalated
    `exec_command` request</summary>
    
    ```json
    {
      "session_id": "<session-id>",
      "turn_id": "<turn-id>",
      "transcript_path": "/path/to/transcript.jsonl",
      "cwd": "/path/to/cwd",
      "hook_event_name": "PermissionRequest",
      "model": "gpt-5",
      "permission_mode": "default",
      "tool_name": "Bash",
      "tool_input": {
        "command": "cp /tmp/source.json /Users/alice/export/source.json",
        "description": "Need to copy a generated file outside the workspace"
      }
    }
    ```
    
    </details>
    
    <details>
    <summary>Example `PermissionRequest` hook input for a network
    approval</summary>
    
    ```json
    {
      "session_id": "<session-id>",
      "turn_id": "<turn-id>",
      "transcript_path": "/path/to/transcript.jsonl",
      "cwd": "/path/to/cwd",
      "hook_event_name": "PermissionRequest",
      "model": "gpt-5",
      "permission_mode": "default",
      "tool_name": "Bash",
      "tool_input": {
        "command": "curl http://codex-network-test.invalid",
        "description": "network-access http://codex-network-test.invalid"
      }
    }
    ```
    
    </details>
    
    ## Follow-ups
    
    - Implement the `PermissionRequest` semantics for `updatedInput`,
    `updatedPermissions`, `interrupt`, and suggestions /
    `permission_suggestions`
    - Add `PermissionRequest` support for the `request_permissions` tool
    path
    
    ---------
    
    Co-authored-by: Codex <noreply@openai.com>
  • Forward app-server turn clientMetadata to Responses (#16009)
    ## Summary
    App-server v2 already receives turn-scoped `clientMetadata`, but the
    Rust app-server was dropping it before the outbound Responses request.
    This change keeps the fix lightweight by threading that metadata through
    the existing turn-metadata path rather than inventing a new transport.
    
    ## What we're trying to do and why
    We want turn-scoped metadata from the app-server protocol layer,
    especially fields like Hermes/GAAS run IDs, to survive all the way to
    the actual Responses API request so it is visible in downstream
    websocket request logging and analytics.
    
    The specific bug was:
    - app-server protocol uses camelCase `clientMetadata`
    - Responses transport already has an existing turn metadata carrier:
    `x-codex-turn-metadata`
    - websocket transport already rewrites that header into
    `request.request_body.client_metadata["x-codex-turn-metadata"]`
    - but the Rust app-server never parsed or stored `clientMetadata`, so
    nothing from the app-server request was making it into that existing
    path
    
    This PR fixes that without adding a new header or a second metadata
    channel.
    
    ## How we did it
    ### Protocol surface
    - Add optional `clientMetadata` to v2 `TurnStartParams` and
    `TurnSteerParams`
    - Regenerate the JSON schema / TypeScript fixtures
    - Update app-server docs to describe the field and its behavior
    
    ### Runtime plumbing
    - Add a dedicated core op for app-server user input carrying turn-scoped
    metadata: `Op::UserInputWithClientMetadata`
    - Wire `turn/start` and `turn/steer` through that op / signature path
    instead of dropping the metadata at the message-processor boundary
    - Store the metadata in `TurnMetadataState`
    
    ### Transport behavior
    - Reuse the existing serialized `x-codex-turn-metadata` payload
    - Merge the new app-server `clientMetadata` into that JSON additively
    - Do **not** replace built-in reserved fields already present in the
    turn metadata payload
    - Keep websocket behavior unchanged at the outer shape level: it still
    sends only `client_metadata["x-codex-turn-metadata"]`, but that JSON
    string now contains the merged fields
    - Keep HTTP fallback behavior unchanged except that the existing
    `x-codex-turn-metadata` header now includes the merged fields too
    
    ### Request shape before / after
    Before, a websocket `response.create` looked like:
    ```json
    {
      "type": "response.create",
      "client_metadata": {
        "x-codex-turn-metadata": "{\"session_id\":\"...\",\"turn_id\":\"...\"}"
      }
    }
    ```
    Even if the app-server caller supplied `clientMetadata`, it was not
    represented there.
    
    After, the same request shape is preserved, but the serialized payload
    now includes the new turn-scoped fields:
    ```json
    {
      "type": "response.create",
      "client_metadata": {
        "x-codex-turn-metadata": "{\"session_id\":\"...\",\"turn_id\":\"...\",\"fiber_run_id\":\"fiber-start-123\",\"origin\":\"gaas\"}"
      }
    }
    ```
    
    ## Validation
    ### Targeted tests added / updated
    - protocol round-trip coverage for `clientMetadata` on `turn/start` and
    `turn/steer`
    - protocol round-trip coverage for `Op::UserInputWithClientMetadata`
    - `TurnMetadataState` merge test proving client metadata is added
    without overwriting reserved built-in fields
    - websocket request-shape test proving outbound `response.create`
    contains merged metadata inside
    `client_metadata["x-codex-turn-metadata"]`
    - app-server integration tests proving:
    - `turn/start` forwards `clientMetadata` into the outbound Responses
    request path
      - websocket warmup + real turn request both behave correctly
      - `turn/steer` updates the follow-up request metadata
    
    ### Commands run
    - `just write-app-server-schema`
    - `cargo test -p codex-app-server-protocol`
    - `cargo test -p codex-protocol`
    - `cargo test -p codex-core
    turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields
    --lib`
    - `cargo test -p codex-core --test all
    responses_websocket_preserves_custom_turn_metadata_fields`
    - `cargo test -p codex-app-server --test all client_metadata`
    - `cargo test -p codex-app-server --test all
    turn_start_forwards_client_metadata_to_responses_websocket_request_body_v2
    -- --nocapture`
    - `just fmt`
    - `just fix -p codex-core -p codex-protocol -p codex-app-server-protocol
    -p codex-app-server`
    - `just fix -p codex-exec -p codex-tui-app-server`
    - `just argument-comment-lint`
    
    ### Full suite note
    `cargo test` in `codex-rs` still fails in:
    -
    `suite::v2::turn_interrupt::turn_interrupt_resolves_pending_command_approval_request`
    
    I verified that same failure on a clean detached `HEAD` worktree with an
    isolated `CARGO_TARGET_DIR`, so it is not caused by this patch.
  • chore: clean up argument-comment lint and roll out all-target CI on macOS (#16054)
    ## Why
    
    `argument-comment-lint` was green in CI even though the repo still had
    many uncommented literal arguments. The main gap was target coverage:
    the repo wrapper did not force Cargo to inspect test-only call sites, so
    examples like the `latest_session_lookup_params(true, ...)` tests in
    `codex-rs/tui_app_server/src/lib.rs` never entered the blocking CI path.
    
    This change cleans up the existing backlog, makes the default repo lint
    path cover all Cargo targets, and starts rolling that stricter CI
    enforcement out on the platform where it is currently validated.
    
    ## What changed
    
    - mechanically fixed existing `argument-comment-lint` violations across
    the `codex-rs` workspace, including tests, examples, and benches
    - updated `tools/argument-comment-lint/run-prebuilt-linter.sh` and
    `tools/argument-comment-lint/run.sh` so non-`--fix` runs default to
    `--all-targets` unless the caller explicitly narrows the target set
    - fixed both wrappers so forwarded cargo arguments after `--` are
    preserved with a single separator
    - documented the new default behavior in
    `tools/argument-comment-lint/README.md`
    - updated `rust-ci` so the macOS lint lane keeps the plain wrapper
    invocation and therefore enforces `--all-targets`, while Linux and
    Windows temporarily pass `-- --lib --bins`
    
    That temporary CI split keeps the stricter all-targets check where it is
    already cleaned up, while leaving room to finish the remaining Linux-
    and Windows-specific target-gated cleanup before enabling
    `--all-targets` on those runners. The Linux and Windows failures on the
    intermediate revision were caused by the wrapper forwarding bug, not by
    additional lint findings in those lanes.
    
    ## Validation
    
    - `bash -n tools/argument-comment-lint/run.sh`
    - `bash -n tools/argument-comment-lint/run-prebuilt-linter.sh`
    - shell-level wrapper forwarding check for `-- --lib --bins`
    - shell-level wrapper forwarding check for `-- --tests`
    - `just argument-comment-lint`
    - `cargo test` in `tools/argument-comment-lint`
    - `cargo test -p codex-terminal-detection`
    
    ## Follow-up
    
    - Clean up remaining Linux-only target-gated callsites, then switch the
    Linux lint lane back to the plain wrapper invocation.
    - Clean up remaining Windows-only target-gated callsites, then switch
    the Windows lint lane back to the plain wrapper invocation.
  • [hooks] add non-streaming (non-stdin style) shell-only PostToolUse support (#15531)
    CHAINED PR - note that base is eternal/hooks-pretooluse-bash, not main
    -- so the following PR should be first
    
    Matching post-tool hook to the pre-tool functionality here:
    https://github.com/openai/codex/pull/15211
    
    So, PreToolUse calls for plain shell calls, allows blocking. This
    PostToolUse call runs after the command executed
    
    example run:
    
    ```
    › as a test, run in parallel the following commands:
      - echo 'one'
      - echo '[block-pre-tool-use]'
      - echo '[block-post-tool-use]'
    
    
    ⚠ MCP startup incomplete (failed: notion, linear)
    
    • Cruising through those three commands in parallel now, and I’ll share the exact outputs right after
      they land.
    
    • Running PreToolUse hook: checking the observatory runes
    
    • Running PreToolUse hook: checking the observatory runes
    
    • Running PreToolUse hook: checking the observatory runes
    
    PreToolUse hook (blocked)
      warning: wizard-tower PreToolUse demo blocked a Bash command on purpose.
      feedback: PreToolUse demo blocked the command. Remove [block-pre-tool-use] to continue.
    
    PreToolUse hook (completed)
      warning: wizard-tower PreToolUse demo inspected Bash: echo 'one'
    
    PreToolUse hook (completed)
      warning: wizard-tower PreToolUse demo inspected Bash: echo '[block-post-tool-use]'
    
    • Ran echo '[block-post-tool-use]'
      └ [block-post-tool-use]
    
    • Ran echo 'one'
      └ one
    
    • Running PostToolUse hook: reading the observatory aftermath
    
    • Running PostToolUse hook: reading the observatory aftermath
    
    PostToolUse hook (blocked)
      warning: wizard-tower PostToolUse demo blocked the Bash follow-through on purpose.
      feedback: PostToolUse demo blocked the result after execution. Remove [block-post-tool-use] to
    continue.
    
    PostToolUse hook (completed)
      warning: wizard-tower PostToolUse demo inspected Bash: echo 'one'
      hook context: PostToolUse demo saw the Bash result. Response preview: one
    
    ──────────────────────────────────────────────────────────────────────────────────────────────────────
    
    • Ran them in parallel. Little wave report:
    
      - echo 'one' completed and returned one.
      - echo '[block-pre-tool-use]' was blocked before execution by the PreToolUse hook.
      - echo '[block-post-tool-use]' ran, but its result was blocked afterward by the PostToolUse hook.
    
      The hook messages were:
    
      - PreToolUse demo blocked the command. Remove [block-pre-tool-use] to continue.
      - PostToolUse demo blocked the result after execution. Remove [block-post-tool-use] to continue.
    
    
    › for the blocked messages (correctly blocked in this test) did the tool output land back into
      context?
    
    
    • Yeah, from this run:
    
      - PreToolUse block: the command itself did not execute, so no command output landed in context. Only
        the block message did.
      - PostToolUse block: the command did execute, but the actual stdout did not come back into context.
        Only the post-tool block message did.
    
      So the blocked notifications landed in context, but the blocked command payloads themselves did not.
      Nice clean guardrail, aloha.
    ```
  • [hooks] add non-streaming (non-stdin style) shell-only PreToolUse support (#15211)
    - add `PreToolUse` hook for bash-like tool execution only at first
    - block shell execution before dispatch with deny-only hook behavior
    - introduces common.rs matcher framework for matching when hooks are run
    
    example run:
    
    ```
    › run three parallel echo commands, and the second one should echo "[block-pre-tool-use]" as a test
    
    
    • Running the three echo commands in parallel now and I’ll report the output directly.
    
    • Running PreToolUse hook: name for demo pre tool use hook
    
    • Running PreToolUse hook: name for demo pre tool use hook
    
    • Running PreToolUse hook: name for demo pre tool use hook
    
    PreToolUse hook (completed)
      warning: wizard-tower PreToolUse demo inspected Bash: echo "first parallel echo"
      
    PreToolUse hook (blocked)
      warning: wizard-tower PreToolUse demo blocked a Bash command on purpose.
      feedback: PreToolUse demo blocked the command. Remove [block-pre-tool-use] to continue.
    
    PreToolUse hook (completed)
      warning: wizard-tower PreToolUse demo inspected Bash: echo "third parallel echo"
    
    • Ran echo "first parallel echo"
      └ first parallel echo
    
    • Ran echo "third parallel echo"
      └ third parallel echo
    
    • Three little waves went out in parallel.
    
      1. printed first parallel echo
      2. was blocked before execution because it contained the exact test string [block-pre-tool-use]
      3. printed third parallel echo
    
      There was also an unrelated macOS defaults warning around the successful commands, but the echoes
      themselves worked fine. If you want, I can rerun the second one with a slightly modified string so
      it passes cleanly.
    ```
  • Split features into codex-features crate (#15253)
    - Split the feature system into a new `codex-features` crate.
    - Cut `codex-core` and workspace consumers over to the new config and
    warning APIs.
    
    Co-authored-by: Ahmed Ibrahim <219906144+aibrahim-oai@users.noreply.github.com>
    Co-authored-by: Codex <noreply@openai.com>
  • [hooks] use a user message > developer message for prompt continuation (#14867)
    ## Summary
    
    Persist Stop-hook continuation prompts as `user` messages instead of
    hidden `developer` messages + some requested integration tests
    
    This is a followup to @pakrym 's comment in
    https://github.com/openai/codex/pull/14532 to make sure stop-block
    continuation prompts match training for turn loops
    
    - Stop continuation now writes `<hook_prompt hook_run_id="...">stop
    hook's user prompt<hook_prompt>`
    - Introduces quick-xml dependency, though we already indirectly depended
    on it anyway via syntect
    - This PR only has about 500 lines of actual logic changes, the rest is
    tests/schema
    
    ## Testing
    
    Example run (with a sessionstart hook and 3 stop hooks) - this shows
    context added by session start, then two stop hooks sending their own
    additional prompts in a new turn. The model responds with a single
    message addressing both. Then when that turn ends, the hooks detect that
    they just ran using `stop_hook_active` and decide not to infinite loop
    
    test files for this (unzip, move codex -> .codex):
    [codex.zip](https://github.com/user-attachments/files/26075806/codex.zip)
    
    ```
    › cats
    
    
    • Running SessionStart hook: lighting the observatory
    
    SessionStart hook (completed)
      warning: Hi, I'm a session start hook for wizard-tower (startup).
      hook context: A wimboltine stonpet is an exotic cuisine from hyperspace
    
    • Cats are tiny zen wizards, my friend: equal parts nap, mystery, and chaos. If you want, we can talk
      cat facts, cat breeds, cat names, or build something cat-themed in this repo.
    
    • Running Stop hook: checking the tower wards
    
    • Running Stop hook: sacking the guards
    
    • Running Stop hook: hiring the guards
    
    Stop hook (completed)
      warning: Wizard Tower Stop hook reviewed the completed reply (177 chars).
    
    Stop hook (blocked)
      warning: Wizard Tower Stop hook continuing conversation
      feedback: cook the stonpet
    
    Stop hook (blocked)
      warning: Wizard Tower Stop hook continuing conversation
      feedback: eat the cooked stonpet
    
    • Stonpet’s cooked, aloha style: flash-seared over a blue quasiflame, glazed with nebula salt, and
      rested until the hyperspace juices settle.
    
      Now we eat with gratitude, my friend. One mindful bite in, and the flavor is pure cosmic surf:
      smoky, bright, and totally out of this dimension.
    
    • Running Stop hook: checking the tower wards
    
    • Running Stop hook: sacking the guards
    
    • Running Stop hook: hiring the guards
    
    Stop hook (completed)
      warning: Wizard Tower Stop hook reviewed the completed reply (285 chars).
    
    Stop hook (completed)
      warning: Wizard Tower Stop hook saw a second pass and stayed calm to avoid a loop.
    
    Stop hook (completed)
      warning: Wizard Tower Stop hook saw a second pass and stayed calm to avoid a loop.
    ```
  • [hooks] turn_id extension for Stop & UserPromptSubmit (#15118)
    ## Description
    
    Adding an extension to the spec that exposes the turn_id to hook
    scripts. This is a codex-specific mechanic that allows connecting the
    hook runs with particular turns
    
    ## Testing
    
    hooks config / sample hooks to use. Extract this, rename codex ->
    .codex, and place this into a repo or your home folder. It includes:
    config.toml that enables hooks, hooks.json, and sample python hooks:
    
    
    [codex.zip](https://github.com/user-attachments/files/26102671/codex.zip)
    
    example run (note the turn_ids change between turns):
    
    ```
    › hello
    
    
    • 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
    
    • Running UserPromptSubmit hook: lighting the observatory lanterns
    
    UserPromptSubmit hook (completed)
      warning: wizard-tower UserPromptSubmit demo inspected: hello for turn: 019d036d-c7fa-72d2-b6fd-
    78878bfe34e4
      hook context: Wizard Tower UserPromptSubmit demo fired. For this reply only, include the exact
    phrase 'observatory lanterns lit' near the end.
    
    • Aloha! Grateful to be here and ready to build with you. Show me what you want to tackle in wizard-
      tower, and we’ll surf the next wave together. observatory lanterns lit
    
    • Running Stop hook: back to shore
    
    Stop hook (completed)
      warning: Wizard Tower Stop hook reviewed the completed reply (170 chars) for turn: 019d036d-c7fa-
    72d2-b6fd-78878bfe34e4
    
    
    › what's a stonpet?
    
    
    • Running UserPromptSubmit hook: lighting the observatory lanterns
    
    UserPromptSubmit hook (completed)
      warning: wizard-tower UserPromptSubmit demo inspected: what's a stonpet? for turn: 019d036e-3164-
    72c3-a170-98925564c4fc
      hook context: Wizard Tower UserPromptSubmit demo fired. For this reply only, include the exact
    phrase 'observatory lanterns lit' near the end.
    
    • A stonpet isn’t a standard real-world word, brah. In our shared context here, a wimboltine stonpet
      is an exotic cuisine from hyperspace, so “stonpet” sounds like the dish or food itself.
    
      If you want, we can totally invent the lore for it next. observatory lanterns lit
    
    • Running Stop hook: back to shore
    
    Stop hook (completed)
      warning: Wizard Tower Stop hook reviewed the completed reply (271 chars) for turn: 019d036e-3164-
    72c3-a170-98925564c4fc
    ```
  • [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())
    ```
  • [hooks] stop continuation & stop_hook_active mechanics (#14532)
    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())
    ```