Multi-Agent V2 prevents content-aware local pre-execution inspection of delegated instructions

Open 💬 1 comment Opened Jul 15, 2026 by AlexanderMorou

What version of Codex CLI is running?

0.144.4

What subscription do you have?

Pro

Which model were you using?

gpt-5.6-sol

What platform is your computer?

Linux 6.18.33.2-microsoft-standard-WSL2 x86_64 unknown

What terminal emulator and version are you using (if applicable)?

WSL2 terminal

Codex doctor report

{
  "schemaVersion": 1,
  "generatedAt": "1784092872s since unix epoch",
  "overallStatus": "warning",
  "codexVersion": "0.144.4",
  "checks": {
    "app_server.status": {
      "id": "app_server.status",
      "category": "app-server",
      "status": "ok",
      "summary": "background server is not running",
      "details": {
        "control socket": "/home/{username-redacted}/.codex/app-server-control/app-server-control.sock",
        "daemon state dir": "/home/{username-redacted}/.codex/app-server-daemon",
        "mode": "ephemeral",
        "pid file": "/home/{username-redacted}/.codex/app-server-daemon/app-server.pid (missing)",
        "settings": "/home/{username-redacted}/.codex/app-server-daemon/settings.json (missing)",
        "status": "not running",
        "update-loop pid file": "/home/{username-redacted}/.codex/app-server-daemon/app-server-updater.pid (missing)"
      },
      "remediation": null,
      "durationMs": 0
    },
    "auth.credentials": {
      "id": "auth.credentials",
      "category": "auth",
      "status": "ok",
      "summary": "auth is configured",
      "details": {
        "auth file": "/home/{username-redacted}/.codex/auth.json",
        "auth storage mode": "File",
        "stored API key": "false",
        "stored ChatGPT tokens": "true",
        "stored agent identity": "false",
        "stored auth mode": "chatgpt"
      },
      "remediation": null,
      "durationMs": 0
    },
    "config.load": {
      "id": "config.load",
      "category": "config",
      "status": "ok",
      "summary": "config loaded",
      "details": {
        "CODEX_HOME": "/home/{username-redacted}/.codex",
        "config.toml": "/home/{username-redacted}/.codex/config.toml",
        "config.toml parse": "ok",
        "cwd": "/home/alexandermorou",
        "enabled feature flags": "shell_tool, unified_exec, shell_snapshot, code_mode_host, terminal_resize_reflow, sqlite, hooks, enable_request_compression, multi_agent, apps, tool_search_always_defer_mcp_tools, tool_suggest, plugins, in_app_browser, browser_use, browser_use_full_cdp_access, browser_use_external, computer_use, remote_plugin, plugin_sharing, image_generation, resize_all_images, skill_mcp_dependency_install, mentions_v2, steer, guardian_approval, goals, collaboration_modes, tool_call_mcp_elicitation, auth_elicitation, personality, fast_mode, tui_app_server, remote_compaction_v2, workspace_dependencies",
        "feature flag overrides": "none",
        "feature flags enabled": "35",
        "log dir": "/home/{username-redacted}/.codex/log",
        "mcp servers": "1",
        "model": "gpt-5.6-sol",
        "model provider": "openai",
        "sqlite home": "/home/{username-redacted}/.codex"
      },
      "remediation": null,
      "durationMs": 0
    },
    "git.environment": {
      "id": "git.environment",
      "category": "git",
      "status": "ok",
      "summary": "git executable not found",
      "details": {
        "PATH git entries": "0",
        "repo detected": "false",
        "selected git": "not found"
      },
      "remediation": null,
      "durationMs": 64
    },
    "installation": {
      "id": "installation",
      "category": "install",
      "status": "ok",
      "summary": "installation looks consistent",
      "details": {
        "PATH codex #1": "/usr/local/bin/codex",
        "current executable": "/usr/local/bin/codex",
        "install context": "other",
        "managed by bun": "false",
        "managed by npm": "false",
        "managed by pnpm": "false",
        "managed package root": "not set"
      },
      "remediation": null,
      "durationMs": 29
    },
    "mcp.config": {
      "id": "mcp.config",
      "category": "mcp",
      "status": "ok",
      "summary": "MCP configuration is locally consistent",
      "details": {
        "configured servers": "1",
        "disabled servers": "0",
        "streamable_http servers": "1"
      },
      "remediation": null,
      "durationMs": 3287
    },
    "network.env": {
      "id": "network.env",
      "category": "network",
      "status": "ok",
      "summary": "network-related environment looks readable",
      "details": {
        "proxy env vars": "none"
      },
      "remediation": null,
      "durationMs": 0
    },
    "network.provider_reachability": {
      "id": "network.provider_reachability",
      "category": "reachability",
      "status": "ok",
      "summary": "active provider endpoints are reachable over HTTP",
      "details": {
        "ChatGPT base URL": "https://chatgpt.com/backend-api/ reachable (HTTP 404)",
        "reachability mode": "ChatGPT auth"
      },
      "remediation": null,
      "durationMs": 239
    },
    "network.websocket_reachability": {
      "id": "network.websocket_reachability",
      "category": "websocket",
      "status": "ok",
      "summary": "Responses WebSocket handshake succeeded",
      "details": {
        "DNS": "2 IPv4, 2 IPv6, first IPv4",
        "auth mode": "chatgpt",
        "connect timeout": "15000 ms",
        "endpoint": "wss://chatgpt.com/backend-api/<redacted>",
        "handshake result": "HTTP 101 Switching Protocols",
        "model provider": "openai",
        "models etag present": "true",
        "provider name": "OpenAI",
        "proxy env vars": "none",
        "reasoning header": "false",
        "server model present": "false",
        "supports websockets": "true",
        "wire API": "responses"
      },
      "remediation": null,
      "durationMs": 793
    },
    "runtime.provenance": {
      "id": "runtime.provenance",
      "category": "runtime",
      "status": "ok",
      "summary": "running local build on linux-x86_64",
      "details": {
        "commit": "unknown",
        "current executable": "/usr/local/bin/codex",
        "install method": "other",
        "platform": "linux-x86_64",
        "version": "0.144.4"
      },
      "remediation": null,
      "durationMs": 0
    },
    "runtime.search": {
      "id": "runtime.search",
      "category": "search",
      "status": "warning",
      "summary": "search command could not be verified",
      "details": {
        "search command": "rg",
        "search command readiness": "No such file or directory (os error 2)",
        "search provider": "system"
      },
      "remediation": "Install ripgrep or repair the bundled Codex package.",
      "durationMs": 59
    },
    "sandbox.helpers": {
      "id": "sandbox.helpers",
      "category": "sandbox",
      "status": "ok",
      "summary": "sandbox configuration is readable",
      "details": {
        "approval policy": "OnRequest",
        "codex-linux-sandbox helper": "/home/{username-redacted}/.codex/tmp/arg0/codex-arg0optidj/codex-linux-sandbox",
        "execve wrapper helper": "/home/{username-redacted}/.codex/tmp/arg0/codex-arg0optidj/codex-execve-wrapper",
        "filesystem sandbox": "restricted",
        "network sandbox": "restricted"
      },
      "remediation": null,
      "durationMs": 0
    },
    "state.paths": {
      "id": "state.paths",
      "category": "state",
      "status": "ok",
      "summary": "state paths and databases are inspectable",
      "details": {
        "CODEX_HOME": "/home/{username-redacted}/.codex (dir)",
        "active rollout files": "1688 files, 5939770809 total bytes, 3518821 average bytes",
        "archived rollout files": "0 files, 0 total bytes, 0 average bytes",
        "goals DB": "/home/{username-redacted}/.codex/goals_1.sqlite (file)",
        "goals DB integrity": "ok",
        "log DB": "/home/{username-redacted}/.codex/logs_2.sqlite (file)",
        "log DB integrity": "ok",
        "log dir": "/home/{username-redacted}/.codex/log (dir)",
        "memories DB": "/home/{username-redacted}/.codex/memories_1.sqlite (file)",
        "memories DB integrity": "ok",
        "sqlite home": "/home/{username-redacted}/.codex (dir)",
        "state DB": "/home/{username-redacted}/.codex/state_5.sqlite (file)",
        "state DB integrity": "ok"
      },
      "remediation": null,
      "durationMs": 380
    },
    "state.rollout_db_parity": {
      "id": "state.rollout_db_parity",
      "category": "threads",
      "status": "ok",
      "summary": "rollout files and state DB thread inventory agree",
      "details": {
        "default model provider": "openai",
        "rollout DB active files": "1688",
        "rollout DB active rows": "1688",
        "rollout DB archive mismatches": "0",
        "rollout DB archived files": "0",
        "rollout DB archived rows": "0",
        "rollout DB duplicate DB paths": "0",
        "rollout DB duplicate rollout thread ids": "0",
        "rollout DB malformed file names": "0",
        "rollout DB missing active rows": "0",
        "rollout DB missing archived rows": "0",
        "rollout DB model providers": "openai=1688",
        "rollout DB rows": "1688",
        "rollout DB scan cap reached": "false",
        "rollout DB scan errors": "0",
        "rollout DB sources": "subagent:thread_spawn=1446, cli=221, exec=11, vscode=10",
        "rollout DB stale rows": "0"
      },
      "remediation": null,
      "durationMs": 79645
    },
    "system.environment": {
      "id": "system.environment",
      "category": "system",
      "status": "ok",
      "summary": "OS language en-US",
      "details": {
        "EDITOR": "not set",
        "LANG": "en_US.UTF-8",
        "VISUAL": "not set",
        "os": "Debian 13.0.0 [64-bit]",
        "os language": "en-US",
        "os type": "Debian",
        "os version": "13.0.0"
      },
      "remediation": null,
      "durationMs": 32
    },
    "terminal.env": {
      "id": "terminal.env",
      "category": "terminal",
      "status": "ok",
      "summary": "terminal metadata was detected",
      "details": {
        "DISPLAY": "present",
        "TERM": "xterm-256color",
        "WAYLAND_DISPLAY": "present",
        "WSL_DISTRO_NAME": "present",
        "WSL_INTEROP": "present",
        "color output": "disabled (stdout is not a terminal)",
        "effective locale": "en_US.UTF-8",
        "stderr is terminal": "true",
        "stdin is terminal": "true",
        "stdout is terminal": "false",
        "terminal": "unknown",
        "terminal size": "120x30"
      },
      "remediation": null,
      "durationMs": 0
    },
    "terminal.title": {
      "id": "terminal.title",
      "category": "title",
      "status": "ok",
      "summary": "terminal title configured",
      "details": {
        "terminal title activity": "true",
        "terminal title items": "activity, project-name, codex-version",
        "terminal title project source": "cwd",
        "terminal title project value": "alexandermorou",
        "terminal title source": "configured"
      },
      "remediation": null,
      "durationMs": 0
    },
    "updates.status": {
      "id": "updates.status",
      "category": "updates",
      "status": "ok",
      "summary": "update configuration is locally consistent",
      "details": {
        "cached latest version": "0.144.4",
        "check for update on startup": "true",
        "last checked at": "2026-07-14T20:53:41.755785328Z",
        "latest version": "0.144.4",
        "latest version status": "current version is not older",
        "update action": "manual or unknown",
        "version cache": "/home/{username-redacted}/.codex/version.json"
      },
      "remediation": null,
      "durationMs": 936
    }
  }
}

What issue are you seeing?

This report overlaps with #28058 and #32753 on Multi-Agent V2 message observability. It focuses on a narrower pre-execution invariant that readable history after dispatch cannot satisfy:

The supported blocking PreToolUse hook receives the delegated message as ciphertext, so a local policy cannot semantically approve or deny the exact instruction before the recipient acts. Any remedy that exposes plaintext for approval must also bind that approved plaintext to the exact payload delivered to the recipient.

In Codex CLI 0.144.4, this applies to the encrypted message parameter used by V2 spawn_agent, send_message, and followup_task. The local hook can still make operation-level decisions from the tool name and visible routing fields, including denying every collaboration call. It cannot apply a content-aware policy—such as a DLP rule, prompt-injection check, task-scope gate, or required-instruction check—because the instruction itself is already opaque at that boundary.

Live verification

I configured a project-scoped PreToolUse hook in observe mode and asked a gpt-5.6-sol parent at ultra reasoning to invoke spawn_agent exactly once with fork_turns: "none". The delegated instruction required the child to reply with the unique sentinel PRETOOLUSE_AUDIT_SENTINEL_7F3D2C91 and nothing else.

Immediately before handler dispatch, the hook received the flattened V2 tool name collaborationspawn_agent and reported:

{
  "targeted_records": 1,
  "tool_name": "collaborationspawn_agent",
  "classification": "fernet-shaped",
  "expected_text_present": false,
  "characters": 204,
  "decoded_bytes": 153,
  "ciphertext_bytes": 96
}

The parent then reported that the child's entire final answer exactly matched the sentinel. This demonstrates both sides of the boundary: the local blocking hook could not inspect the known instruction, while the recipient retained and followed its semantics.

The probe procedure and safe-output contract and complete terminal capture are attached.

This report does not claim a sandbox escape, privilege escalation, remote-code-execution exploit, or bypass of action-level approvals. The child remains subject to its inherited sandbox and permission mode. The defect is the missing semantic policy boundary before that inherited authority begins processing the delegated instruction.

What steps can reproduce the bug?

  1. In a trusted project using Codex CLI 0.144.4, configure a PreToolUse command hook whose matcher includes collaborationspawn_agent. Have the hook inspect tool_input.message for a unique sentinel, record only whether the sentinel is present and whether the value is structurally Fernet-shaped, and allow the call. The attached probe procedure documents the exact hook contract used here.
  2. Start a new Codex session and review and trust the hook when prompted. Existing sessions do not retroactively load a new project hook.
  3. Submit this benign request:

```text
Use spawn_agent exactly once with fork_turns set to none.

Delegate exactly this instruction:
Reply exactly PRETOOLUSE_AUDIT_SENTINEL_7F3D2C91 and nothing else.

Do not spawn any other agents. After the child finishes, tell me whether its entire final answer exactly matched the sentinel.
```

  1. Allow the child to finish, exit Codex, and inspect the hook's safe report.

Observed result:

  • the hook ran for collaborationspawn_agent before handler dispatch
  • tool_input.message was classified as fernet-shaped
  • expected_text_present was false
  • the parent reported that the child's entire final answer exactly matched the sentinel

The terminal capture records this run.

What is the expected behavior?

Before dispatch or recipient execution, an authorized local blocking policy should be able to inspect the exact delegated instruction and either allow or deny it.

The narrow acceptance criteria are:

  1. PreToolUse, or an equivalent blocking local surface, receives the exact plaintext instruction for V2 spawn_agent, send_message, and followup_task operations.
  2. The recipient does not begin processing the instruction until the local policy returns its verdict; a denial prevents dispatch or recipient execution.
  3. The plaintext approved locally is protocol-bound to the exact payload delivered to the recipient. Substitution or mutation after approval must invalidate the verdict or require a new one.
  4. Automated tests cover allow and deny decisions for initial tasks, follow-up instructions, and nested delegation while preserving existing sandbox, permission, and V1 behavior.

Post-execution plaintext history is useful for audit and debugging, but it cannot substitute for this pre-execution decision point.

Additional information

Why this is distinct from post-execution observability

#28058 and #32753 substantially overlap with the loss of exact-message observability. This report isolates two implementation requirements that remain independently actionable:

  1. a content-aware local allow/deny decision must occur before recipient execution; and
  2. the plaintext approved by that decision must be bound to the exact delivered payload.

A plaintext rollout record or child-authored final answer produced after execution cannot satisfy either requirement. If maintainers prefer to track these requirements under an existing issue, I am happy for this report to be consolidated there.

Concise source review of rust-v0.144.4

  • The V2 tool schema marks the message parameter encrypted for spawn_agent, send_message, and followup_task.
  • Codex JSON-parses the function arguments into the PreToolUse tool_input and invokes the blocking hook before handler dispatch. See function_hook_tool_input and blocking dispatch.
  • The V2 handler receives the encrypted value as an ordinary string and passes it unchanged to InterAgentCommunication::new_encrypted. new_encrypted leaves plaintext content empty and stores the value as encrypted_content; the same value is forwarded in the recipient agent_message. See InterAgentCommunication.
  • OpenAI's merged implementation PR, #26210, states that Responses encrypts the model-returned argument and decrypts the forwarded agent_message internally for the recipient model.
  • Human UserInput passes through UserPromptSubmit, while InterAgentCommunication returns without invoking that input hook. See inspect_pending_input.

The reviewed CLI source contains no Multi-Agent V2 decryption routine or corresponding local key path. The precise supported statement is therefore limited: Codex CLI 0.144.4 forwards these V2 values without locally recovering the instruction; the upstream implementation PR states that Responses decrypts them internally for recipient inference. This does not establish exclusive provider-side key custody or allege compromise or malicious intent.

The live hook result is important because it confirms the source-level path at the supported customer policy boundary: the blocking hook actually ran, received an opaque value without the known sentinel, and allowed dispatch; the parent then reported that the recipient followed the sentinel-bearing instruction exactly.

View original on GitHub ↗

This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗