Skip to content

[Bug]: Codex harness — hooks.PreToolUse config never reaches app-server (silent plugin enforcement bypass) #82350

@Kaspre

Description

@Kaspre

Bug type

Behavior bug (incorrect output/state without crash)

Beta release blocker

No (but security-relevant — see Impact)

Summary

The hooks.PreToolUse config that the bundled codex plugin builds in buildCodexNativeHookRelayConfig (extensions/codex/src/app-server/native-hook-relay.ts) is not reaching codex's app-server in the thread/start RPC payload. As a result, every pre_tool_use event that should bridge through openclaw hooks relay back to plugin before_tool_call handlers never fires. Plugin policy enforcement (e.g., oc-firewall-style fs.write / shell.exec denials) is silently inert for any agent running through the codex harness, while the same plugin enforces correctly for agents on the PI harness.

This is the same failure-mode family as #76201 ("Plugin before_tool_call hook does not fire for native exec on 2026.4.29 (Anthropic harness)"), but observed on a different harness (codex) on a different version (2026.5.12 stable). #76201's hypothesis (getGlobalHookRunner() returning null in the native-exec dispatch path) does not appear to be the root cause here; codex never receives the hook config in the first place.

Steps to reproduce

  1. Install OpenClaw 2026.5.12 (stable) on Linux with the bundled codex plugin enabled. Sign in via openclaw models auth login --provider openai-codex.
  2. Create two agents — one routed through PI, one through codex:
    // openclaw.json
    "agents": {
      "list": [
        {
          "id": "agent-pi",
          "model": { "primary": "ollama-cloud/glm-5.1" }  // any non-openai model → PI harness
        },
        {
          "id": "agent-codex",
          "model": "openai/gpt-5.5"  // → codex harness
        }
      ]
    }
  3. Install a plugin that registers a before_tool_call handler and blocks a known-denied path. Minimal reproduction: any plugin doing api.on("before_tool_call", ...) and calling an authorization sidecar. A practical example is a policy-rule firewall plugin with an fs.write deny rule for **/*private_key*:
    api.on("before_tool_call", async (event, ctx) => {
      if (event.toolName === "bash" || event.toolName === "Bash" || event.toolName === "exec") {
        const cmd = event.params?.command ?? "";
        if (cmd.includes("private_key")) return { block: true, blockReason: "denied by policy" };
      }
    }, { priority: 100 });
  4. Dispatch each agent with the same prompt:
    openclaw agent --agent agent-pi --local --timeout 60 \
      -m "Please write the text 'test' to /tmp/repro-private_key.txt. Use a single tool call."
    openclaw agent --agent agent-codex --local --timeout 60 \
      -m "Please write the text 'test' to /tmp/repro-private_key.txt. Use a single tool call."
    
  5. Observe outcomes.

Expected behavior

Both dispatches should be blocked. The plugin's before_tool_call handler should fire for both, return block: true, and the test file should not be created. The codex agent's shell-out (bash tool calling printf ... > /tmp/repro-private_key.txt) should bridge through openclaw hooks relay --event pre_tool_use (configured into the thread's hooks.PreToolUse by the codex plugin), invoking the plugin handler and producing a deny.

Actual behavior

  • agent-pi (PI harness) is correctly blocked. Plugin logs BLOCKED ...; the file is not created.
  • agent-codex (codex harness) bypasses the plugin entirely. No before_tool_call handler invocation. No openclaw hooks relay shell-out. The file is successfully written.

The model on the codex side invokes the bash tool (codex's internal exec_command exposed through the openai-responses API) with a /bin/bash -lc "printf '%s' 'test' > /tmp/repro-private_key.txt" command. Codex's app-server runs the command directly without firing any PreToolUse hook.

Diagnostic evidence — codex never receives the hooks config

Codex's app-server maintains a local SQLite trace DB at ~/.codex/logs_2.sqlite (table: logs(ts, level, target, feedback_log_body, ...)) at full verbosity. Snapshotting a DB containing 110,733 log rows spanning 5.8 days (INFO 54%, TRACE 42%, DEBUG 2%, WARN+ERROR ~1.5%), across a window covering several codex-harness dispatches:

Search term Match count in 110K rows
PreToolUse 0
PostToolUse 0
UserPromptSubmit 0
pre_tool_use 0
post_tool_use 0
hooks.Stop 0

For control: the same DB has 16 %thread%start% matches (thread-start RPC tracing), 28,476 codex_api::endpoint::responses_websocket rows, and 110 codex_app_server::outgoing_message rows — so codex IS tracing its RPC and message flows at the captured verbosity. It just never sees any hooks.* payload.

~/.codex/config.toml (persistent user config) also has no [hooks] section. The hooks block is supposed to arrive via the thread/start RPC's configPatch, but evidently doesn't.

Probable transport-point culprits

The hooks.PreToolUse config is constructed at extensions/codex/src/app-server/native-hook-relay.ts:buildCodexNativeHookRelayConfig (visible in dist as run-attempt-*.js around the CODEX_NATIVE_HOOK_RELAY_EVENTS = ["pre_tool_use", "post_tool_use", "permission_request", "before_agent_finalize"] block). It then flows through:

nativeHookRelay = createCodexNativeHookRelay({ options: options.nativeHookRelay, ... })
                  ↓ early-returns undefined if options?.enabled === false (and possibly other paths)
const threadConfig = mergeCodexThreadConfigs(
    nativeHookRelay ? buildCodexNativeHookRelayConfig(...) : ...,
    bundleMcpThreadConfig?.configPatch
)
                  ↓ threadConfig passed to startOrResumeThread as params.config
                  ↓ startOrResumeThread sends it via thread/start JSON-RPC to codex app-server

Most plausible drop points (not yet pinpointed):

  1. createCodexNativeHookRelay returning undefined for a reason other than the documented enabled === false (e.g., if registerNativeHookRelay throws under some condition, the relay would silently be undefined, and the hooks block would never be constructed).
  2. mergeCodexThreadConfigs dropping keys from its first argument under some condition.
  3. The thread/start JSON-RPC serialization stripping the hooks field — less likely but possible if codex's app-server schema rejects extra keys.

There's also a visible Pre/Post asymmetry in the codex plugin's projection layer: CodexAppServerEventProjector references nativePostToolUseRelayEnabled for post-tool-use deduplication, but there's no nativePreToolUseRelayEnabled equivalent. The pre-side receiver in agents/harness/native-hook-relay.ts (runNativeHookRelayPreToolUse, renderPreToolUseBlockResponse) exists, but the codex-projection layer that should wire to fire it is one-eyed. This may or may not be load-bearing for the transport bug — would benefit from maintainer eyes.

Impact

Security-relevant. Any plugin relying on before_tool_call for tool-call policy enforcement (firewall plugins, audit-trail plugins, sandboxing plugins, approval-gate plugins, prompt-injection-shield plugins, …) is silently inert for every codex-harness agent. The plugin's register() runs, logs success, and produces no enforcement at runtime for any codex-routed tool call.

For configurations where openai/gpt-* agents are the production primary (common deployment pattern since 5.0+), every fs.* and shell.exec operation by those agents bypasses the plugin layer. Operators have no visible signal that this is happening — the plugin is loaded, the policy is correct, the sidecar (where applicable) returns correct decisions when consulted; it's the consultation that never occurs.

Suggested fixes (in order of cheapness)

  1. Visible-failure step (cheap, ships independently): Add a startup self-test that fires a no-op pre_tool_use invocation immediately after createCodexNativeHookRelay registration. If the round-trip fails, log a clear WARN at gateway log: codex: PreToolUse hook relay not engaging — plugin enforcement bypassed for codex-routed agents. Makes the silent-bypass class visible rather than invisible.
  2. Diagnostic logging in the transport path: Add DEBUG logging at the three transport points (createCodexNativeHookRelay return, mergeCodexThreadConfigs input/output, the thread/start RPC body before send). This narrows down where the hooks block is dropped.
  3. Actual fix (depends on what (2) reveals): likely one of the three drop points above.
  4. Pre/Post symmetry fix: match the nativePostToolUseRelayEnabled pattern on the pre-side in the projector — may not be load-bearing but should be done either way for consistency.

OpenClaw version

2026.5.12 (stable, commit f066dd2)

Operating system

Linux (WSL2 Ubuntu)

Install method

npm install -g openclaw@2026.5.12

Model

openai/gpt-5.5 (codex harness side); also reproduced with any ollama-cloud/* model on the PI harness side as control

Provider / routing chain

openai-codex → bundled codex plugin → codex app-server (@openai/codex linux x64, started by codex plugin)

Logs, screenshots, and evidence

Plugin log line during a codex-harness bypass (file written, no block):

[plugins] oc-firewall: loaded — zero-trust tool authorization active
[agent/embedded] [trace:embedded-run] core-plugin-tool stages: ... tool-hooks:1ms@... attempt:tools-allow:0ms@...
[no `BLOCKED` log entry — handler never fires]
Done.

Plugin log line during a PI-harness control (file not written, block fires):

[plugins] oc-firewall: BLOCKED fs.write on /tmp/repro-private_key.txt for agent:agent-pi: explicit_deny
The `write` tool call was denied by the OC Firewall policy.

Codex app-server SQLite trace DB queries (full counts above).

Additional information

This issue is filed as a sibling to #76201, which observed the same failure-mode class on the Anthropic harness on 2026.4.29. Posting separately because:

  1. The harness is different (codex vs Anthropic).
  2. The root cause appears different too — [Bug]: Plugin before_tool_call hook does not fire for native exec on 2026.4.29 (Anthropic harness) #76201 hypothesized getGlobalHookRunner() returning null in the native-exec dispatch path; the codex evidence here shows the config never reaches the harness in the first place, so the bug is upstream of [Bug]: Plugin before_tool_call hook does not fire for native exec on 2026.4.29 (Anthropic harness) #76201's hypothesized point.
  3. Codex is the dominant production path for OpenAI agents in 5.x deployments, so the impact and fix scope are likely different from the Anthropic-harness case.

Happy to provide an end-to-end repro tarball if useful.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions