## Why
app-server may run on a different OS from the selected exec-server
environment. Parsing that environment’s cwd with the Codex host’s path
rules prevents thread startup.
## What
Carry environment cwd values as `LegacyAppPathString` at the app-server
boundary and `PathUri` internally. Existing tool-call schemas and
relative-path behavior stay host-native; remaining local-only consumers
convert explicitly and leave follow-up TODOs.
The Wine integration test verifies app-server can start a thread and
complete an ordinary turn with a Windows environment cwd from Linux.
## Validation
- `bazel test //codex-rs/core/tests/remote_env_windows:smoke-test
--test_output=errors`
- focused app-server environment-selection and protocol schema tests
- scoped Clippy for `codex-core` and `codex-app-server-protocol`
## Why
Help Codex understand how to execute the migration to support cross-OS
paths.
## What
Expand the path-types skill with our goals and constraints.
## Why
`ApiPathString` kind of implies that it can be used anywhere we pull a
path out of JSON, but it's not really appropriate for tool arguments
when the model might generate relative paths.
Prefer `String` for model-generated paths and we can handle the
conversion per feature for now and define a shared abstraction later if
it makes sense.
# What
Rename `ApiPathString` to `AppLegacyPathString` to clarify its role.
Expand the `path-types` skill to tell the model to leave tool args as
bare strings.
## Why
Review caught some would-be regressions in changes to unified_exec that
weren't surfaced in CI.
## What
Add coverage for requesting permissions through unified exec when there
are additional permissions. Previously this flow was only tested against
shell_command.
This PR establishes the intended behavior as an executable contract
before a refactor of the cell runtime begins. It also fixes cases where
a second observer or termination request could replace an existing
response channel and leave the original caller unresolved.
### Behavior codified
- A cell can yield output and subsequently resume to completion.
- A caller can run a cell until it has no immediately runnable work,
receive its accumulated output and outstanding tool-call IDs, and then
resume the same cell when the awaited work is available.
- Each cell admits one active observer:
- a second observer receives an explicit busy error
- the existing observer remains registered and is not displaced
- A natural result (conclusion of the js module) that has already
reached the cell controller wins over a later termination request.
- Otherwise, termination preempts execution and resolves both:
- the active observer, if present
- the caller requesting termination
- Repeated termination requests are rejected while termination is
already in progress.
- Terminal responses are sent only after outstanding callback work has
been handled:
- natural completion drains notifications and cancels outstanding tool
calls
- termination cancels and drains both notification and tool callbacks.
- Cell removal and cell_closed notification happen after callback
cleanup
## Why
In #28146 I missed the invariant that an absolute `exec_command` workdir
must override the environment cwd. The existing integration test would
have caught that regression, but it was ignored as flaky.
## What
Re-enable `unified_exec_respects_workdir_override`.
## Validation
`just test -p codex-core unified_exec_respects_workdir_override`
## This PR
The original [combined remote plugin analytics PR
#26281](https://github.com/openai/codex/pull/26281) mixed reusable
analytics test infrastructure, two manual smoke workflows, a metadata
refactor, and the final identity behavior. This PR adds the
account-mutating validation workflow separately so its cleanup and
recovery guarantees can be reviewed without the final analytics behavior
change.
- Add a manually invoked remote plugin install/uninstall smoke workflow.
- Require explicit account-mutation confirmation and an initially
uninstalled plugin.
- Validate the current `codex_plugin_installed` contract, where
`plugin_id` is the backend ID.
- Restore and verify the original uninstalled state, with a dedicated
recovery command.
This baseline intentionally does not require `codex_plugin_uninstalled`,
because production does not emit that event yet. The final PR will
update this smoke to require local `plugin_id`, `remote_plugin_id`, and
uninstall emission. Review this PR as the net diff against #27099.
## Testing
- `just test -p codex-app-server-test-client` (3 focused
capture/validation tests passed)
- The live workflow was previously exercised on the green combined
reference branch, and the original uninstalled account state was
restored.
- CI is green across the required platform matrix.
## Split Overview
```text
main
├── #27093 Debug analytics capture
│ └── #27099 Non-mutating plugin smoke
│ └── #27100 Remote install/uninstall smoke ← you are here
└── #27102 Plugin telemetry metadata refactor
After #27093, #27099, #27100, and #27102 merge:
└── Final PR: add remote_plugin_id to plugin analytics
```
Review order and dependencies:
1. [#27093 Add debug-only analytics event
capture](https://github.com/openai/codex/pull/27093) (based on `main`)
2. [#27099 Add a plugin analytics smoke
workflow](https://github.com/openai/codex/pull/27099) (stacked on
#27093)
3. [#27100 Add a remote plugin analytics mutation smoke
workflow](https://github.com/openai/codex/pull/27100) **(this PR,
stacked on #27099)**
4. [#27102 Centralize plugin telemetry metadata
construction](https://github.com/openai/codex/pull/27102) (independent,
based on `main`)
5. Final remote-ID behavior PR (created after PRs 1-4 merge)
The original [#26281](https://github.com/openai/codex/pull/26281)
remains open as the green aggregate reference until the final PR is
published.
## Why
Codex Apps tools can mark arguments with `openai/fileParams`, but the
execution path resolved and opened those files directly on the host.
That bypassed the selected turn environment and prevented annotated file
arguments from working with remote environments.
## What changed
- resolve annotated file arguments against the primary turn environment
- read file metadata and contents through that environment's sandboxed
`ExecutorFileSystem`
- reject files over the 512 MiB limit from metadata before reading or
transferring them
- retain the buffered upload-size check as defense in depth
- make the OpenAI upload API accept a filename and buffered contents
instead of owning local filesystem access
- describe the model-visible argument as a path in the primary
environment
This builds on #27927, which added `size` to internal filesystem
metadata.
## Testing
- `just test -p codex-api upload_openai_file_returns_canonical_uri`
- `just test -p codex-mcp
tool_with_model_visible_input_schema_masks_file_params`
- `just test -p codex-core mcp_openai_file`
- `just test -p codex-core
codex_apps_file_params_upload_environment_files_before_mcp_tool_call`
Add ThreadHistoryBuilder APIs for collecting incremental thread item and turn changes while applying rollout items.
Batch handling coalesces repeated changes so callers can get the latest incremental thread item changes for a set of rollout items without rebuilding full history.
## Why
Codex syntax-highlights diffs for conventional C++ extensions such as
`.cpp` and `.cxx`, but C++ module interface files using `.cppm`, `.ixx`,
or `.cxxm` fall back to plain diff coloring. The bundled syntax set
already includes C++, but it does not resolve those module extensions by
itself.
Closes#28223.
## What changed
- map `.cppm`, `.ixx`, and `.cxxm` to the existing `cpp` syntax in
`render/highlight.rs`
- extend alias-resolution coverage for all three module extensions
- verify `.cpp`, `.cppm`, `.ixx`, and `.cxxm` diffs produce
syntax-highlighted RGB spans while unknown extensions retain the plain
fallback
- snapshot the syntax-colored token segmentation for the supported C++
module extensions
## How to Test
1. Ask Codex to create or modify a C++ module interface file using
`.cppm`, `.ixx`, or `.cxxm`.
2. Confirm C++ tokens in the rendered diff receive syntax colors instead
of only the red/green diff treatment.
3. Modify an equivalent `.cpp` file and confirm its existing
highlighting remains unchanged.
4. Modify a file with an unknown extension and confirm it still uses the
plain diff fallback.
Targeted tests:
- `just test -p codex-tui -E
'test(find_syntax_resolves_languages_and_aliases) |
test(cpp_module_extensions_use_cpp_highlighting) |
test(unknown_extension_falls_back_without_syntax_highlighting)'`
## This PR
The original [combined remote plugin analytics PR
#26281](https://github.com/openai/codex/pull/26281) mixed reusable
analytics test infrastructure, two manual smoke workflows, a metadata
refactor, and the final identity behavior. This PR establishes a
non-mutating end-to-end plugin smoke workflow before any analytics
identity semantics change.
- Add `plugin-analytics-smoke` to the existing app-server test client.
- Exercise plugin disable, enable, and use through production app-server
RPC paths.
- Isolate config writes in a temporary file and use a loopback Responses
API server.
- Capture analytics without sending them to the production analytics
backend.
- Validate the current local `plugin_id`, names, capability metadata,
thread, turn, and model fields.
This is intentionally a baseline smoke workflow. It does not assert
`remote_plugin_id`; the final PR will update it when that field exists.
Review this PR as the net diff against #27093.
## Testing
- The test-client target compiles successfully.
- The combined reference branch exercised the manual smoke against the
live remote plugin service.
- CI is green across the required platform matrix.
## Split Overview
```text
main
├── #27093 Debug analytics capture
│ └── #27099 Non-mutating plugin smoke ← you are here
│ └── #27100 Remote install/uninstall smoke
└── #27102 Plugin telemetry metadata refactor
After #27093, #27099, #27100, and #27102 merge:
└── Final PR: add remote_plugin_id to plugin analytics
```
Review order and dependencies:
1. [#27093 Add debug-only analytics event
capture](https://github.com/openai/codex/pull/27093) (based on `main`)
2. [#27099 Add a plugin analytics smoke
workflow](https://github.com/openai/codex/pull/27099) **(this PR,
stacked on #27093)**
3. [#27100 Add a remote plugin analytics mutation smoke
workflow](https://github.com/openai/codex/pull/27100) (stacked on this
PR)
4. [#27102 Centralize plugin telemetry metadata
construction](https://github.com/openai/codex/pull/27102) (independent,
based on `main`)
5. Final remote-ID behavior PR (created after PRs 1-4 merge)
The original [#26281](https://github.com/openai/codex/pull/26281)
remains open as the green aggregate reference until the final PR is
published.
## Why
`fs/readFile` buffers the entire file in one response, which makes large
remote reads expensive and prevents callers from applying backpressure.
We need an opt-in streaming path with bounded block sizes while
preserving the existing single-call API for small and sandboxed reads.
## What changed
- Add `ExecServerClient::stream`, returning a named `FileReadStream`
that implements `futures::Stream` and yields immutable 1 MiB byte
blocks.
- Add internal `fs/open`, `fs/readBlock`, and `fs/close` RPCs.
`fs/readBlock` accepts an explicit offset and length.
- Keep unsandboxed files open between block reads, cap open handles per
connection, and clean them up on EOF, error, stream drop, explicit
close, or connection shutdown.
- Reject platform-sandboxed streaming opens instead of turning the
one-shot sandbox helper into a persistent server. Existing `fs/readFile`
behavior is unchanged.
## Testing
- `just test -p codex-exec-server`
- Integration coverage for 1 MiB chunking, exact block-boundary EOF,
sandbox rejection, and continued reads from the opened file after path
replacement.
- Handle-manager coverage for non-sequential offsets, variable block
lengths, the 128-handle limit, and capacity release after close.
## Why
On Linux, suspending Codex with `Ctrl+Z` and returning with `fg` can
leave the composer misaligned or inject terminal response bytes such as
focus reports into the prompt. Shell job-control output moves the cursor
while Codex is suspended, and terminal input polling can race with the
responses used to restore the inline viewport.
Fixes#26564.
## What changed
- preserve and restore keyboard reporting without disturbing the parent
terminal stack
- pause terminal event polling while Codex is suspended and flush
buffered input before resuming it
- force crossterm's cached raw-mode state back in sync after the shell
completes its `fg` handoff
- probe the actual post-`fg` cursor position with the tolerant
terminal-response parser, then realign the inline viewport before
redrawing
## How to Test
1. On Linux, start the development TUI with `just c`.
2. Type text into the composer without submitting it.
3. Press `Ctrl+Z`, run any harmless shell command, then run `fg`.
4. Confirm the composer redraws below the shell output, the draft text
is preserved, and no raw escape sequences appear.
5. Repeat the suspend/resume cycle and confirm normal typing still
works.
Targeted tests:
- `cargo test -p codex-tui --lib parses_cursor_position_as_zero_based -j
1`
- `cargo test -p codex-tui --lib tui::event_stream::tests -j 1`
## Why
Ensure a consistent string format when exposing path conversion errors
to the model.
## What
- Render `PathUriParseError::InvalidFileUriPath` as `'$PATH' is invalid
on '$OS'`.
## Why
[#18763](https://github.com/openai/codex/pull/18763) added canonical
hostname resolution for `remote_sandbox_config`. Requirements
composition currently performs that synchronous DNS lookup on every
fresh process, even when none of the loaded requirements layers contains
`[[remote_sandbox_config]]`. On hosts with slow local DNS resolution,
this can add several seconds to Codex startup.
## What
- defer hostname resolution until a parsed requirements layer actually
contains `remote_sandbox_config`
- cache the resolver result once per requirements composition,
preserving the existing single-lookup behavior across multiple layers
- keep the existing FQDN resolution and per-layer requirements
precedence unchanged
- cover both the ordinary no-lookup path and the multi-layer
single-lookup path
## How to Test
On a host where local canonical-name resolution is slow:
1. Start Codex without `[[remote_sandbox_config]]` in any managed
requirements layer and confirm startup no longer waits for hostname
resolution.
2. Add a matching `[[remote_sandbox_config]]` entry and confirm its
`allowed_sandbox_modes` still overrides the layer's top-level value.
3. Add remote sandbox entries to multiple requirements layers and
confirm precedence remains unchanged while the hostname is resolved only
once.
Targeted tests:
- `just test -p codex-config hostname_resolver`
- `just test -p codex-config` (181 passed)
## Why
When a subagent exhausts its retries, it emits an `Error`, but the
generic task lifecycle then emits `TurnComplete(None)`. That completion
used to overwrite the subagent's `Errored` status with
`Completed(None)`, so the parent received an empty completion
notification.
This made a failed child look indistinguishable from a child that
completed without an answer. In unattended or long-running multi-agent
work, the root could silently continue without knowing that delegated
work failed or how to restart it.
## Behavior
Before, a terminal stream failure was reduced to an empty completion:
```text
<subagent_notification>
{"agent_path":"/root/worker","status":{"completed":null}}
</subagent_notification>
```
Now the parent receives the actual terminal error, bounded to 1,000
tokens, together with an actionable recovery hint:
```text
<subagent_notification>
{
"agent_path": "/root/worker",
"status": {
"errored": "stream disconnected before completion: stream closed before response.completed"
},
"next_action": "This agent's turn failed. If you still need this agent, use `followup_task` to give it another task."
}
</subagent_notification>
```
The notification remains queue-only: it does not wake the root or replay
the failed request. The root sees it at the next sampling boundary and
can use `followup_task` to start a new turn for that agent.
## What changed
- Added terminal-error precedence to the [agent status
reducer](https://github.com/openai/codex/blob/e95fcfe2bb6a02f1a75650afa20048859f556511/codex-rs/core/src/agent/status.rs#L23-L34),
so a closing `TurnComplete` cannot erase an immediately preceding
`Errored` status.
- Made MultiAgentV2 completion forwarding use the retained session
status instead of re-deriving `Completed(None)` from the final event.
- Extended the [subagent notification
fragment](https://github.com/openai/codex/blob/e95fcfe2bb6a02f1a75650afa20048859f556511/codex-rs/core/src/context/subagent_notification.rs#L6-L60)
with a `next_action` for terminal errors and a hard cap on model-visible
error text.
- Kept successful completions and interrupted turns unchanged.
## Verification
- Added a status-reducer test proving that `Errored` survives the
trailing `TurnComplete`.
- Added an integration test that exhausts a subagent's stream retries
and verifies the exact `agent_message` delivered to the parent,
including the error and `followup_task` guidance.
- Re-ran the existing successful-completion and interrupted-turn
notification tests.
## Summary
Plugin loading and auth projection both previously produced
`PluginLoadOutcome`. That made an unfiltered load result look like
runtime-ready capabilities and generated capability summaries before
auth routing had run.
This change keeps loaded plugin records in the cache, applies the
current auth policy in `PluginsManager`, and only then builds
`PluginLoadOutcome` and its summaries. Auth changes still reuse the
cached disk load and re-resolve apps and MCP servers without reloading
plugins.
The updated tests cover cached auth changes and verify that capability
summaries match the effective app/MCP surface.
## Testing
- `just test -p codex-core-plugins`
- `just test -p codex-plugin`
- `just fix -p codex-core-plugins`
## Summary
- disable the stable Apps feature in the generic `test_codex()`
integration-test harness
- keep Apps-specific tests explicit: their builders re-enable Apps and
point it at a local mock server
## Why
Generic tests that use dummy ChatGPT auth were also enabling the
host-owned `codex_apps` MCP server. That made unrelated tests contact
`chatgpt.com` and wait for MCP startup, causing the Bazel timeouts
observed on #28368.
The generic harness should be hermetic and should not start an external
service that the test did not request. This is test-only; production
Apps behavior is unchanged. The broader optional-MCP startup behavior is
being handled separately in #28407.
## Testing
- `just test -p codex-core -E
'test(pre_sampling_compact_runs_when_comp_hash_changes) |
test(model_switch_to_smaller_model_updates_token_context_window) |
test(codex_apps_file_params_upload_local_paths_before_mcp_tool_call)'`
- `just fix -p codex-core`
- `just fmt`
## Why
Multi-agent v2 messages need a consistent, model-visible envelope that
identifies what kind of interaction occurred, who sent it, and which
agent it targets. Previously, encrypted deliveries exposed only
`encrypted_content`, while child completion used the legacy
`<subagent_notification>` shape. That meant the client could not
consistently present `NEW_TASK`, `MESSAGE`, and `FINAL_ANSWER` using the
same format.
This change adds the routing envelope as plaintext while keeping task
and message payloads encrypted. No new Responses API field is required:
an encrypted delivery is represented as an `input_text` header
immediately followed by its existing `encrypted_content` item.
Every envelope now follows this shape:
```text
Message Type: <NEW_TASK | MESSAGE | FINAL_ANSWER>
Task name: <recipient agent path>
Sender: <author agent path>
Payload:
<message payload>
```
## Message types
### `NEW_TASK`
`NEW_TASK` is used when the recipient should begin a new turn, including
an initial `spawn_agent` task and a later `followup_task`.
For a root agent spawning `/root/worker`, the request contains a
plaintext envelope followed by the encrypted task:
```json
{
"type": "agent_message",
"author": "/root",
"recipient": "/root/worker",
"content": [
{
"type": "input_text",
"text": "Message Type: NEW_TASK\nTask name: /root/worker\nSender: /root\nPayload:\n"
},
{
"type": "encrypted_content",
"encrypted_content": "<encrypted task payload>"
}
]
}
```
Conceptually, the model receives:
```text
Message Type: NEW_TASK
Task name: /root/worker
Sender: /root
Payload:
Review the authentication changes and report any regressions.
```
### `MESSAGE`
`MESSAGE` is used for a queued `send_message` delivery. It communicates
with an existing agent without starting a new turn.
For `/root/worker` reporting progress to the root agent, the request
contains:
```json
{
"type": "agent_message",
"author": "/root/worker",
"recipient": "/root",
"content": [
{
"type": "input_text",
"text": "Message Type: MESSAGE\nTask name: /root\nSender: /root/worker\nPayload:\n"
},
{
"type": "encrypted_content",
"encrypted_content": "<encrypted message payload>"
}
]
}
```
Conceptually, the model receives:
```text
Message Type: MESSAGE
Task name: /root
Sender: /root/worker
Payload:
The protocol tests pass; I am checking the resume path now.
```
### `FINAL_ANSWER`
`FINAL_ANSWER` is emitted when a child agent reaches a terminal state
and reports its result to its parent. Completion payloads are already
available locally, so the complete envelope is represented as plaintext
rather than as a plaintext header plus encrypted content.
For `/root/worker` completing work for the root agent, the request
contains:
```json
{
"type": "agent_message",
"author": "/root/worker",
"recipient": "/root",
"content": [
{
"type": "input_text",
"text": "Message Type: FINAL_ANSWER\nTask name: /root\nSender: /root/worker\nPayload:\nNo regressions found."
}
]
}
```
The model-visible form is:
```text
Message Type: FINAL_ANSWER
Task name: /root
Sender: /root/worker
Payload:
No regressions found.
```
Errored, shut down, and missing agents also use `FINAL_ANSWER`, with a
terminal-status description in the payload.
## What changed
- Render `NEW_TASK` or `MESSAGE` in
`InterAgentCommunication::to_model_input_item`, based on whether the
encrypted delivery starts a turn.
- Replace the multi-agent v2 `<subagent_notification>` completion
payload with a model-visible `FINAL_ANSWER` envelope.
- Document `Task name`, `Sender`, and `Payload` consistently in the
multi-agent developer instructions.
- Prevent local-only history projections from treating an encrypted
message's plaintext header as the complete assistant message.
- Preserve rollout-trace interaction edges when an agent message
contains both plaintext and encrypted content.
Legacy multi-agent behavior remains unchanged.
## Verification
- `just test -p codex-protocol`
- `just test -p codex-rollout-trace`
- `just test -p codex-web-search-extension`
- `just test -p codex-core
encrypted_multi_agent_v2_spawn_sends_agent_message_to_child`
- `just test -p codex-core
plaintext_multi_agent_v2_completion_sends_agent_message`
- `just test -p codex-core
multi_agent_v2_followup_task_completion_notifies_parent_on_every_turn`
- `just test -p codex-core
multi_agent_v2_completion_queues_message_for_direct_parent`
## Why
The local rollout compression worker currently scans only
`archived_sessions`, so cold unarchived thread history remains expanded
indefinitely.
## What changed
- Scan `sessions` after `archived_sessions` within the existing worker
runtime budget.
- Update rollout compression coverage to require both cold active and
archived rollouts to be compressed while fresh active rollouts remain
plain.
The worker remains behind the disabled-by-default
`local_thread_store_compression` feature, and the existing seven-day
cold-file threshold is unchanged.
## Validation
- `just test -p codex-rollout` (69 passed)
- `just fmt`
- `git diff --check`
## Why
`account/read` currently reports only `type: "amazonBedrock"`, so
clients cannot distinguish a Codex-managed Bedrock API key from
credentials supplied by AWS. The app UI needs that distinction to render
the appropriate account state without duplicating provider-auth logic.
Credential-source selection belongs to the Bedrock model provider
because it already owns the precedence between managed Bedrock auth and
the external AWS credential path. This builds on #27443 and #27689.
## What changed
- Added `AmazonBedrockCredentialSource` with `codexManaged` and
`awsManaged` values.
- Included the selected credential source in
`ProviderAccount::AmazonBedrock` and the app-server `Account` response.
- Made `AmazonBedrockModelProvider::account_state()` classify the source
from its managed-auth state.
- Regenerated the app-server JSON and TypeScript schemas.
- Updated app-server account documentation and downstream TUI matches.
`codexManaged` means the provider found a managed Bedrock API key.
`awsManaged` identifies the provider's external AWS credential path; it
does not assert that the AWS credential chain has been validated.
## Testing
- Added model-provider coverage for Codex-managed precedence and
AWS-managed fallback.
- Added app-server protocol serialization coverage for both wire values.
- Added app-server integration coverage for both `account/read`
responses.
- `just test -p codex-protocol -p codex-model-provider -p
codex-app-server-protocol` (497 tests passed).
After rebasing onto #27711, the `codex-app-server` test target compiled
past the image-generation `PathUri` migration. Local linking was then
interrupted by disk exhaustion (`No space left on device`).
## Summary
- restore `externalAgentConfig/import/progress` notifications while
keeping `externalAgentConfig/import/completed` as the must-deliver event
- persist completed external-agent config imports in state DB by
`importId`, including concrete success/failure details for config,
AGENTS.md, skills, plugins, MCP servers, subagents, hooks, commands, and
sessions
- add `externalAgentConfig/import/readHistories` so clients can recover
persisted import results after missing the live completion notification
- include `errorType` on import failures in protocol
responses/notifications and persisted DB JSON so future code can
classify failures without another wire/storage shape change
## Validation
- `git diff --check`
- `just test -p codex-state external_agent_config_imports`
- `just test -p codex-app-server-protocol`
- `CODEX_SQLITE_HOME=/private/tmp/codex-app-server-sqlite-read-details
just test -p codex-app-server
external_agent_config_import_sends_completion_notification_for_sync_only_import`
Also ran earlier broader checks before publishing:
- `just test -p codex-state`
-
`CODEX_SQLITE_HOME=/private/tmp/codex-app-server-external-agent-test-sqlite
just test -p codex-app-server external_agent_config`
- `just test -p codex-external-agent-migration`
## Why
User shell commands still read the legacy turn cwd and session shell
even though execution context is now owned by selected turn
environments. App-server also defines `thread/shellCommand` as a
local-host escape hatch, so it must use an available local environment
even when a remote environment is primary.
## What changed
- Add `ResolvedTurnEnvironments::local()` to find the selected local
environment.
- Resolve the user shell command cwd and shell from that local
`TurnEnvironment`.
- Emit the standard `shell is unavailable in this session` error when no
selected local environment or resolved local shell is available.
- Add an integration test covering `/shell` without a local environment.
## Test plan
- `just test -p codex-core
user_shell_command_without_local_environment_emits_error`
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.
## Why
Models sometimes need to pause briefly while waiting for external work,
but using a shell command for that delay ties the wait to a process and
does not naturally resume when new turn input arrives.
## What changed
- add a built-in `sleep` tool behind the under-development `sleep_tool`
feature
- accept a bounded `duration_ms` argument, matching the millisecond
convention used by unified exec
- end the sleep early when either steered user input or mailbox input
arrives
- include elapsed wall-clock time in completed and interrupted outputs
- emit a dedicated core `SleepItem` through `item/started` and
`item/completed`
- expose the sleep item as app-server v2 `ThreadItem::Sleep` and retain
it in reconstructed thread history
- regenerate the configuration schema for the new feature flag
- regenerate app-server JSON and TypeScript schema fixtures
## Test plan
- `just test -p codex-core sleep_tool_follows_feature_gate`
- `just test -p codex-core any_new_input_interrupts_sleep`
- `just test -p codex-app-server-protocol`
- `just test -p codex-app-server
sleep_emits_started_and_completed_items`
## Why
Shell snapshots are currently session-scoped even though shell and cwd
are properties of a selected turn environment. That makes snapshot
refresh depend on separate session-cwd plumbing, prevents retained
environments from retaining their snapshot work, and can make snapshot
construction use a different shell than command execution.
This follows #27955 by making the retained thread-environment service
own environment snapshot lifecycles. Session configuration remains the
requested selection state, while `ThreadEnvironments` remains the source
of successfully resolved environments.
## What changed
- Configure the shell-snapshot builder before initial environment
resolution.
- Start each local environment snapshot task when its `TurnEnvironment`
is built and retain that shared task while environment ID and cwd still
match.
- Inherit retained environment snapshots into spawned child threads.
- Carry the selected `TurnEnvironment` through shell runtimes so
snapshot construction and command execution use the same
environment-specific shell and cwd.
- Load project instructions and warm plugins/skills after initial
environment resolution.
- Continue decoding invalid UTF-8 instruction files lossily without
emitting a startup warning.
- Keep requested selections in `SessionConfiguration`; failed or
duplicate resolutions only affect the resolved environment snapshot.
## Validation
- `cargo check -p codex-core --tests`
- `just test -p codex-home instructions` (6 passed)
- Focused environment, instruction, shell-snapshot, and user-shell tests
(84 passed)
- Focused shell-snapshot, user-shell, and unified-exec tests (126
passed; two event-timing tests passed on retry)
## Why
Clients running an app-server on one OS and an exec-server on another OS
need to be able to pass sandbox config to app-server that refers to
resources on the executor's foreign OS.
## What
`AbsolutePathBuf` can't represent these paths and we don't want users to
be exposed to `PathUri` yet, so this moves the public app-server API to
be expressed in terms of `ApiPathString`.
Stacked on #28165.
- change app-server v2 filesystem permission paths, including legacy
read/write roots, to `ApiPathString`
- localize API paths through `PathUri` when converting into the current
native core permission types
- make path-bearing permission conversions fallible and surface
localization failures instead of silently treating malformed grants as
ordinary denials
- propagate conversion failures through app-server and TUI approval
handling
- regenerate the app-server JSON and TypeScript schemas
- leave migration TODOs on native-path conversions so they can be
removed once core permission paths use `PathUri`
## Summary
Makes plugin details/read flows capability-aware so auth-filtered plugin
surfaces report the same usable app/MCP/skill shape as the marketplace
and install flows.
## Validation
Not run; this change was rebased onto the current plugin auth stack and
pushed as a draft PR.
**Manual test**
1. set up a local marketplace with a plugin that has both app and mcp
declarations
```
// .app.json
{
"apps": {
"linear": {
"id": "some_id"
}
}
}
```
```
// .mcp.json
{
"mcpServers": {
"linear": {
"type": "http",
"url": "https://mcp.linear.app/mcp",
"oauth_resource": "https://mcp.linear.app/mcp"
},
"linear2": {
"type": "http",
"url": "https://mcp.linear2.app/mcp",
"oauth_resource": "https://mcp.linear2.app/mcp"
}
}
}
```
2a. **login in with api key** and observe plugin details page which
shows no apps (note we don't show "app not available due to api key log
in as there's no way to differentiate between no apps and app without
substitute mcp exists" without significantly more code changes, i've
separated this to a follow up if we want that behaviour.
<img width="1170" height="279" alt="Screenshot 2026-06-15 at 23 45 40"
src="https://github.com/user-attachments/assets/d36cb160-fbec-461e-9643-9c761dbae7bb"
/>
<img width="975" height="640" alt="Screenshot 2026-06-15 at 18 40 30"
src="https://github.com/user-attachments/assets/90ec0bc8-7506-4b90-bbd3-070720de799e"
/>
2b. **log in with chat** and observe intended conflict resolution logic
<img width="1165" height="224" alt="Screenshot 2026-06-15 at 17 17 30"
src="https://github.com/user-attachments/assets/80adfbf2-7dac-4f08-8b76-8eeeab6c95e7"
/>
<img width="968" height="567" alt="Screenshot 2026-06-15 at 18 38 59"
src="https://github.com/user-attachments/assets/9ea92c5e-535b-4aa4-8ad0-ee513b57bc3c"
/>
## Summary
- choose the local OpenAI curated marketplace manifest based on auth:
Codex backend auth gets the existing marketplace, direct provider auth
gets `api_marketplace.json`
- include Bedrock API key auth in the direct-provider API marketplace
path
- safely skip the API marketplace when `api_marketplace.json` is absent
## Validation
- `just fmt`
- `git diff --check origin/main...HEAD`
- CI should run the full validation
## Manual Testing
### - New api marketplace not available for API key sign
1. Safely not display anything from api marketplace
<img width="1161" height="289" alt="Screenshot 2026-06-15 at 21 37 43"
src="https://github.com/user-attachments/assets/a5f16642-8a20-4ac1-a0de-1274a4c7b5b2"
/>
### - New api marketplace for API key sign in
1. Setup api_marketplace.json
```
{
"name": "openai-curated",
"interface": {
"displayName": "Codex official"
},
"plugins": [
{
"name": "linear",
"source": {
"source": "local",
"path": "./plugins/linear"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_INSTALL"
},
"category": "Productivity"
}
]
}
```
2. Log in with API key, observe that only the defined plugin from
api_marketplace.json is available from "Codex Official" (outside of
local testing marketplaces)
<img width="1167" height="446" alt="Screenshot 2026-06-15 at 21 16 53"
src="https://github.com/user-attachments/assets/7cf61477-d826-4ef6-bc05-0a23ac1c0259"
/>
also checked functionality on codex app
### - SiWC users
Still uses 'default' marketplace.json and renders all plugins
<img width="1171" height="502" alt="Screenshot 2026-06-15 at 21 40 25"
src="https://github.com/user-attachments/assets/d212ea9b-0aa5-470b-8ea4-450efe65bb2b"
/>
also checked functionality on codex app
## Notes
- `just test -p codex-core-plugins` was started locally before splitting
branches, but I stopped relying on local tests per follow-up and left
final validation to PR CI.
## Why
The transport in
[openai/codex#26242](https://github.com/openai/codex/pull/26242) needs
to be used by every remote orchestrator-to-executor connection before
JSON-RPC traffic starts.
## Changes
- Generates one executor Noise identity when remote exec-server starts
and registers its public key.
- Creates a harness identity for each physical remote environment
connection.
- Fetches a fresh registry bundle before connecting and validates the
authenticated harness key before completing the executor handshake.
- Multiplexes encrypted logical streams over the existing executor
WebSocket.
- Adds bounded stream, handshake-failure, and reassembly state.
- Adds safe lifecycle diagnostics without logging keys, authorizations,
plaintext, or ciphertext.
- Covers reconnects, replay rejection, validation failure, framing
limits, and encrypted JSON-RPC tool traffic.
## Stack
1. [openai/codex#26242](https://github.com/openai/codex/pull/26242):
Noise channel and relay transport
2. **[openai/codex#26245](https://github.com/openai/codex/pull/26245)**:
remote registration and runtime activation
## Verification
- `just test -p codex-exec-server`
- `just fix -p codex-exec-server`
- `just bazel-lock-check`
- `cargo shear`
---------
Co-authored-by: Codex <noreply@openai.com>
## Why
We want to exercise a linux app-server against a windows exec-server
without having to repeat every test case. This approach has slight
precedent in the remote docker test setup.
## What
Run the shared `codex-core` integration suite against Windows
exec-server behavior from Linux. This makes cross-OS path and shell
regressions visible while keeping unsupported cases owned by individual
tests.
- Add `local`, `docker`, and `wine-exec` test environment selection with
legacy Docker compatibility.
- Extend `codex_rust_crate` to generate a sharded Wine-exec variant
using a cross-built Windows server and pinned Bazel Wine/PowerShell
runtimes.
- Teach remote-aware helpers about Windows paths and track temporary
incompatibilities with source-local `skip_if_wine_exec!` calls and
follow-up reasons.
Addresses #26383 and #26452
## Summary
`codex exec --dangerously-bypass-hook-trust` printed the bypass warning,
but valid untrusted hooks still did not run.
Exec applied the flag to its initial config, then lost it when
app-server reloaded config for the new or resumed thread.
## Fix
Forward `bypass_hook_trust: true` through the existing thread request
config override for both start and resume.
The override is omitted when the flag is not enabled, preserving normal
trust behavior.
## Testing
Added:
- A test confirming start and resume preserve the override.
- An end-to-end exec test confirming a `SessionStart` hook runs and
creates a marker file.
## Summary
- Add `includeStartupContext` to realtime start requests so callers can
explicitly skip Codex startup context while keeping the backend prompt
- Thread the new flag through protocol types, request processing, and
realtime session config
- Update app-server docs and coverage for the new default and opt-out
behavior
## Testing
- Added protocol serialization coverage for `includeStartupContext`
- Added realtime integration coverage for starting a session with
startup context disabled
## Summary
This is the first step in making plugin auth routing consistent. The
rule should not live as one-off checks in every place that loads or
displays plugin capabilities.
This PR introduces a small resolver for the auth-level policy: given a
plugin's declared apps, MCP servers, current auth mode, and active
state, return the capabilities that are actually usable in that context.
## Why
Product rule:
- SiWC auth can use app connectors, so app declarations stay available.
- API-key/direct auth cannot use app connectors, so app declarations are
removed.
- When an active plugin has both an app and an MCP server with the same
name, the app route wins for Codex-backed auth and the conflicting MCP
server is hidden.
Putting that rule in `capabilities.rs` gives the rest of the stack one
place to ask instead of duplicating auth checks in loader, manager,
marketplace, and details code.
## Validation
- `cargo fmt`
- `cargo test -p codex-core-plugins`
## Why
Exec-server request and connection latencies need fractional-second
histograms. The existing duration API records integer milliseconds and
uses millisecond-scale buckets.
## What changed
- Adds a described duration API that records `Duration` values as
fractional seconds.
- Uses second-scale explicit histogram boundaries.
- Caches duration histograms by name, unit, and description, matching
the existing instrument caching model.
- Covers exact boundaries, representative bucket placement, fractional
sums, and exported metadata.
This PR only adds the duration primitive. It does not add exec-server
adoption.
## Stack
1. #26091: counter descriptions
2. #27057: gauge instruments
3. **#27058: second-based duration histograms**
4. #25019: initialize exec-server OpenTelemetry at startup
Related independent coverage: #27059 tests OTLP HTTP log and trace event
export.
## Validation
- `just test -p codex-otel`
Summary
- Add the two missing `metadata: None` initializers after #28355 made
response-item metadata required.
- Restore test compilation for `codex-core` and `codex-api` on main.
Validation
- `git diff --check`
- `just fmt` (Rust formatting passed; unrelated Python formatter steps
could not use the sandboxed shared `uv` cache)
- Focused crate tests are running after PR creation.
## Why
Progress towards letting app-server and exec-server run on different
platforms, specifically for sandbox configuration.
## What
- Make the filesystem path containment hierarchy generic, defaulting to
`AbsolutePathBuf` for now.
- Have clients specify `AbsolutePathBuf` or `PathUri` directly where
needed.
- Use `PathUri` throughout exec-server filesystem protocol and trait
boundaries.
- Implement `From` for conversion to path URIs and `TryFrom` for
fallible conversion to absolute paths through the generic type
hierarchy.
## Why
Rendezvous forwards traffic between the orchestrator and exec-server.
The endpoints need to authenticate each other and encrypt that traffic
without trusting Rendezvous with plaintext or endpoint keys.
## Changes
- Adds a hybrid Noise IK channel through Clatter using X25519,
ML-KEM-768, AES-256-GCM, and SHA-256.
- Binds each handshake to `environment_id`, `executor_registration_id`,
and `stream_id`.
- Pins the registry-provided executor key and carries the harness
authorization inside the encrypted handshake.
- Orders relay frames before consuming Noise nonces and fragments large
JSON-RPC messages into bounded records.
- Bounds handshake payloads, frames, streams, and message reassembly.
Runtime activation is in
[openai/codex#26245](https://github.com/openai/codex/pull/26245).
## Stack
1. **[openai/codex#26242](https://github.com/openai/codex/pull/26242)**:
Noise channel and relay transport
2. [openai/codex#26245](https://github.com/openai/codex/pull/26245):
remote registration and runtime activation
## Verification
- `just test -p codex-exec-server`
- Oversized initiator payload regression coverage
- `just fix -p codex-exec-server`
- `just bazel-lock-check`
- `cargo shear`
---------
Co-authored-by: Codex <noreply@openai.com>
## This PR
The original [combined remote plugin analytics PR
#26281](https://github.com/openai/codex/pull/26281) mixed reusable
analytics test infrastructure, two manual smoke workflows, a metadata
refactor, and the final identity behavior. This PR isolates the generic
capture mechanism so it can be reviewed and landed before any
plugin-specific behavior.
- Add a debug-only analytics destination that writes final request
payloads as JSONL.
- Suppress HTTP delivery whenever capture mode is selected, including
after capture write failures.
- Keep release behavior unchanged even when the capture environment
variable is present.
- Keep the mechanism generic; this PR contains no plugin-specific
behavior.
Set `CODEX_ANALYTICS_EVENTS_CAPTURE_FILE=/path/events.jsonl` when
running a debug Codex binary to inspect the exact batched payload that
would otherwise be sent to the analytics endpoint.
## Testing
- `just test -p codex-analytics` (76 passed)
- `just test --release -p codex-analytics` (73 passed)
- CI is green across the required platform matrix.
## Split Overview
```text
main
├── #27093 Debug analytics capture ← you are here
│ └── #27099 Non-mutating plugin smoke
│ └── #27100 Remote install/uninstall smoke
└── #27102 Plugin telemetry metadata refactor
After #27093, #27099, #27100, and #27102 merge:
└── Final PR: add remote_plugin_id to plugin analytics
```
Review order and dependencies:
1. [#27093 Add debug-only analytics event
capture](https://github.com/openai/codex/pull/27093) **(this PR, based
on `main`)**
2. [#27099 Add a plugin analytics smoke
workflow](https://github.com/openai/codex/pull/27099) (stacked on
#27093)
3. [#27100 Add a remote plugin analytics mutation smoke
workflow](https://github.com/openai/codex/pull/27100) (stacked on
#27099)
4. [#27102 Centralize plugin telemetry metadata
construction](https://github.com/openai/codex/pull/27102) (independent,
based on `main`)
5. Final remote-ID behavior PR (created after PRs 1-4 merge)
The original [#26281](https://github.com/openai/codex/pull/26281)
remains open as the green aggregate reference until the final PR is
published.
## Why
Realtime voice harness tuning needs app-side control over what backend
Codex text is spoken. Backend orchestrator text is written for a reading
UI, so automatically speaking every preamble, progress update, or final
assistant message can make the realtime voice model too chatty.
For experimentation, clients need two simple controls: keep app/client
text-item injection on the existing item-create path, and add an
explicit speakable path that app code can call only when it wants
realtime to speak. Automatic Codex output also needs an opt-in way to
switch from the protocol's default speakable path to regular realtime
items, with a caller-provided prefix so prompt wording can be tuned
outside core.
The default remains unchanged: if a client omits the new start fields
and never calls `appendSpeech`, automatic backend output continues down
the existing speakable path for the selected realtime protocol.
## What Changed
- Adds experimental `thread/realtime/appendSpeech` for app-provided
speakable text.
- Keeps existing `thread/realtime/appendText` as the item-create API for
app-provided realtime text items.
- Adds `codexResponsesAsItems` / `codex_responses_as_items` on
`thread/realtime/start` to send automatic Codex responses with
`conversation.item.create` instead of the protocol's default speakable
output path.
- Adds `codexResponseItemPrefix` / `codex_response_item_prefix` so
clients can prepend experiment instructions to those automatic Codex
response items.
- Keeps literal `conversation.handoff.append` routing scoped to the v1
speakable path; v2 default speech uses its item/function-output plus
`response.create` behavior.
- Removes the earlier public silent-context API and hardcoded
silent-context prefix.
- Updates realtime tests to cover default automatic speakable behavior,
opt-in automatic item-create behavior, and explicit `appendSpeech`
behavior.
## Validation
- `cargo check -p codex-core -p codex-app-server -p codex-api`
- `just test -p codex-app-server realtime_conversation`
- `just test -p codex-core realtime_conversation` (50/51 passed in the
filtered parallel run; the lone failure passed when rerun in isolation)
- `just test -p codex-core
conversation_mirrors_assistant_message_text_to_realtime_handoff`
- `just test -p codex-api
e2e_connect_and_exchange_events_against_mock_ws_server`
- `just fix -p codex-core`
- `just fix -p codex-app-server`
- `cargo build -p codex-cli`
## Why
Selected execution environments are thread-scoped resources, but startup
and turn construction repeatedly resolved their IDs and working
directories. That discarded existing environment handles and shell
metadata even when a selection had not changed.
Session configuration updates also need to affect future turns without
changing the resolved environment set already captured by a running
turn.
## What changed
- Create a `ThreadEnvironments` service inside `Codex` from the spawned
`EnvironmentManager` and raw environment selections, then store it on
`SessionServices`.
- Split service construction from `update_selections`, allowing session
configuration updates to mutate the resolved set in place.
- Retain an existing `TurnEnvironment` when its environment ID and
working directory match; resolve only added or changed selections and
remove selections that are no longer present.
- Normalize duplicate IDs by keeping the first selection and skip
individual selections that fail to resolve instead of rejecting the
entire update.
- Give each `TurnContext` a cloned `TurnEnvironmentSnapshot`, so later
session configuration updates affect future turns without rewriting an
active turn.
- Reuse the service-owned environment manager and resolved snapshot for
startup work, MCP initialization, and child-thread spawning instead of
flowing resolved environments through spawn arguments.
## Test plan
- `cargo check -p codex-core --tests`
- `just test -p codex-core environment_selection`
- `just test -p codex-core turn_environments`
- `just test -p codex-core
session_update_settings_does_not_rewrite_sticky_environment_cwds`
- `just test -p codex-core
default_turn_does_not_overlay_legacy_fallback_cwd_onto_stored_thread_environments`
## Summary
- preserve the plugin directory endpoint's response order while merging
installed state
- append unmatched installed-only plugins afterward when requested
- add focused coverage for directory order and installed-only placement
## Why
The remote marketplace merge currently reconstructs plugins through
ordered maps and sets, then sorts the result alphabetically by display
name. That discards any ordering supplied by the plugin directory
endpoint before the list reaches Desktop.
## Implementation
Directory plugin IDs are unique, so the merge now iterates the directory
vector directly in response order. For each directory plugin, it removes
matching installed state from an ID-indexed map and builds the summary.
Any entries left in the installed map are installed-only plugins and are
appended when `include_installed_only` is enabled.
There is no separate rank field, rank map, or final sort. Desktop
therefore receives directory order—including any backend ranking—and can
preserve it within its existing stable UI state tiers.
## Testing
- `just test -p codex-core-plugins` (225 passed)
## Summary
- keep the realtime mock websocket open for the handoff steering test
after scripted responses
- avoid racing the mock server close before the standalone handoff
append is observed, which was showing up as a Windows timeout in CI
__Details__:
Failures in samples seem to be caused by:
1. The mock websocket sends conversation.handoff.requested.
2. The mock immediately closes the websocket because
start_websocket_server(...) defaults to close_after_requests: true.
3. On Windows, that close often surfaces as os error 10053 / 10054.
4. The realtime stream shuts down before the routed handoff finishes
creating/steering the follow-up request.
5. The test waits for the expected follow-up event and times out.
The PR changes only step 2: for this test, the mock websocket stays open
after sending the scripted handoff event. The same handoff event is
still sent, and the test still asserts the important steering behavior:
1. first Responses request has the original prompt
2. first request does not contain realtime delegation
3. second Responses request does contain the realtime delegation
## Validation
- `just fmt`
- `just test -p codex-core --test all
suite::realtime_conversation::inbound_handoff_request_steers_active_turn`
## Recent CI failures with the same signature
-
https://github.com/openai/codex/actions/runs/27538033492/job/81392362858
- 2026-06-15, `[codex] update multi-agent v2 prompts`
- same test failed after `conversation.handoff.requested`; websocket
read failed with `os error 10053`
-
https://github.com/openai/codex/actions/runs/27543877820/job/81412200651
- 2026-06-15, `feat: dispatch queued user messages through core idle
extensions`
- same test failed; websocket read failed with `os error 10054`
-
https://github.com/openai/codex/actions/runs/27544342375/job/81413801641
- 2026-06-15, `[codex] Make marketplace loading capability aware`
- same test failed; websocket read failed with `os error 10053`
## Summary
- move `AppToolPolicyEvaluator` and the Apps config/requirements policy
logic from `codex-core` into `codex-connectors`
- resolve one immutable policy snapshot per exposure build and reuse it
across every Codex Apps MCP tool
- keep core as a thin adapter from MCP metadata to connector-owned
policy input while preserving the call-time defense-in-depth check
## Why
`build_mcp_tool_exposure` evaluates every Codex Apps tool on each
sampling request. The old path rebuilt effective Apps configuration for
every tool, and the policy implementation lived in the already-large
core crate even though it is connector-specific.
The connector-owned evaluator keeps the expensive config merge/decode
out of the loop and gives core only the effective policy result it
needs.
## Performance
With the real 557-tool Apps corpus, `build_mcp_tool_exposure` measured
3.74 ms and 3.33 ms after the extraction (3.54 ms mean). The original
path measured 807 ms mean, so the final result retains the 99.6%
reduction.
## Validation
- `cargo check -p codex-connectors -p codex-core`
- `just test -p codex-connectors` — 15 passed
- `just test -p codex-core --lib connectors` — 35 passed
- `just test -p codex-core --lib mcp_tool_exposure` — 5 passed
- `just test -p codex-core --lib mcp_tool_call` — 72 passed
- `just bazel-lock-update`
- `just bazel-lock-check`
- `just fix -p codex-connectors`
- `just fix -p codex-core`
- `just fmt`
## 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