Hooks: mediate non-empty write_stdin before terminal input delivery
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 125k
- Forks
- 19.4k
- PR merge metrics
- PR metrics pending
Description
Version and environment
Codex CLI 0.154.0, Linux. Deterministic loopback Responses provider with a fixture model catalog; no model inference, credentials, or production data. Both direct tool calls and calls nested through code mode were exercised.
Problem
Non-empty write_stdin delivers bytes to a previously authorized exec_command process without invoking PreToolUse again. A hook that authorized process creation cannot inspect or deny later input. This is a missing enforcement point, not a claim that hooks provide complete sandbox confinement.
Reproduction and observed result
The reproducible harness and report are pinned here:
From that Guardrail revision, with Codex 0.154.0 installed:
go build -o /tmp/guardrail-codex ./cmd/guardrail
python3 test/smoke/codex_probe.py /tmp/guardrail-codex --mediation
python3 test/smoke/codex_probe.py /tmp/guardrail-codex --mediation --code-mode
The fixture starts a harmless cat > stdin.txt terminal, sends fixture bytes, polls, sends EOF, and applies a harmless patch. It verifies actual file contents and process termination, not just tool response strings.
| Action | Observed pre-hook |
|---|---|
Start process with exec_command |
Yes, canonical Bash |
Send non-empty input with write_stdin |
No; bytes reach the output file |
| Poll with empty input | No |
| Send EOF | No; process exits |
| Apply patch | Yes |
Both baseline runs pass with exactly two pre-hooks (Bash and patch). A passing baseline means the gap was reproduced, not that stdin is protected. All fixture state is disposable. Hook trust is explicitly configured by the harness.
Expected behavior
Offer a synchronous PreToolUse checkpoint for non-empty terminal input before any bytes or control characters are delivered. Include immutable target-process provenance (session/process ID, original exec call ID, original command and effective execution directory). A deny must deliver no bytes. If rewrites are supported, they must not retarget the terminal or change provenance; native stdin approval should see the rewritten bytes. Empty polls can remain distinct and unhooked.
Arbitrary terminal input is not necessarily shell syntax, so projecting it as a fresh Bash command would be incorrect. Process identity should be revalidated around any hook/approval wait.
Additional measurement and limitations
At 2026-09-18 05:16:14 UTC our ordinary Guardrail audit file contained 60,925 valid records, zero Codex-tagged records and zero Codex unknown-native-tool hits (record interval: 2026-09-04 03:04:16 UTC through 2026-09-18 05:09:27 UTC). This is an observability measurement, not causal proof: no events alone cannot distinguish an unused plane, untrusted/disabled hooks, another audit destination, or skipped dispatch. The isolated fixture above is the evidence that stdin executes without its own pre-hook.
The shell_tool=false restriction removes both initial exec and stdin, including code-mode calls, but also removes ordinary command work. We are validating a process-manager checkpoint patch against upstream base 7498521d288b9b3b96ffba4eedf089d8d6e06a84; new direct/nested deny-and-rewrite tests pass locally, while broader validation is ongoing. It is not shipped upstream.
Related but different: #43184 concerns terminal completion events; #32505 concerns code-mode provenance; #18491 tracks broader hook coverage. This report concerns pre-delivery mediation of subsequent terminal input.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the mediation probe report and run test/smoke/codex_probe.py using the commands in the issue, then inspect the process-manager checkpoint patch against upstream base 7498521d288b9b3b96ffba4eedf089d8d6e06a84. Done means non-empty write_stdin input is checked before delivery, denial sends no bytes, rewrites preserve target provenance, and empty polls remain distinct.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- cli, security
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100