Tool-contract ambiguity: exec_command.cmd lets models over-quote shell operators

Resolved 💬 2 comments Opened May 3, 2026 by ritolika Closed May 3, 2026

Summary

The current exec_command and legacy shell tool surfaces blur the boundary between structured argv and shell scripts. This causes models (observed: GPT-5.5 high-reasoning) to over-defensively quote shell syntax, breaking pipelines and nested commands at runtime.

Two empirically observed failure modes

A. Shell operators emitted as quoted literal arguments

The model writes:

cmd: "rtk read package.json '|' sed -n 1,20p"

Wrapped as bash -lc <cmd>, the inner shell sees '|' as a literal argument to rtk (a shell pipe character would be unquoted). Result: rtk errors with an unrecognized argument; the pipeline never executes.

Class: |, &&, ;, >, < all emitted with surrounding single quotes when the model is being defensive.

B. Nested bash -c '...' blow-up with regex specials

The model writes:

cmd: "bash -c '... rg -n \"...`...$...\" ...' ..."

After the outer /bin/zsh -lc "..." wrapping plus the inner bash -c '...' quoting layer, backticks (` `) and dollars ($) don't survive both quote escapes. Result: bash: -c: line 0: unexpected EOF while looking for matching \``.

Root cause

Both failures share one root cause: exec_command.cmd is a String parameter (script-shaped), but the model treats it as if it were Vec<String> (argv-shaped) and defensively quotes shell metacharacters.

The transport layer (codex-rs/core/src/tools/handlers/unified_exec.rs) is doing the right thing — it wraps the script in [shell, -lc, cmd] without modification. The legacy shell tool's command: Vec<String> parameter passes through shlex_join which correctly shell-quotes each argument, including any operators the model emits as standalone array elements.

So both code paths are correct given their inputs. The issue is tool-contract ambiguity — the model can't tell that cmd should contain raw shell syntax.

Narrowest fix (workaround)

Update docstrings on exec_command.cmd and the legacy shell tool description to explicitly tell the model:

  • Shell operators are syntax inside cmd, not argv items — never quote them as literal tokens
  • Don't nest bash -c '...' inside the script; the double quote layer mangles backticks, $, and regex specials
  • For complex scripts, use a heredoc into bash -s or write a temp script and exec it

This is a workaround. It depends on the model honoring docstring guidance, which works for current GPT-5.5 but is fragile to future model training drift.

Proposed design-correct fix

Split the tool surface by intent at the schema level — make the wrong shape impossible, not just discouraged:

exec_argv { argv: string[] }
  → execvp() directly, no shell invocation
  → operators in argv are always literal arguments

exec_script { script: string }
  → bash -lc <script>, always invokes user shell
  → operators are syntax, no shell-quoting applied

The model can't confuse them — the schema picks the path. This closes the ambiguity class entirely.

Migration concerns

  • Existing model-behavior priors trained on the current exec_command / shell tools
  • Approval policy display changes (two tool-call shapes for the operator UI)
  • Hooks that intercept tool calls
  • App-server protocol envelope (whether the new tool names are wire-visible)
  • Test surface (every consumer of the current shell tool needs updating)
  • Compatibility with shell_command / unified exec — likely deprecate the array form

Verification path

For the narrowest workaround: cargo test -p codex-tools local_tool exercises the docstring assertions; manual smoke tests against a rebuilt binary confirm the model emits unquoted operators and skips bash -c '...' nesting.

For the design-correct fix: would need an evaluation set covering both failure modes, plus regression on the existing tool-call corpus.

Filed by

External observation against a custom codex build (v0.128.0, GPT-5.5 high reasoning, macOS arm64). Happy to provide additional repro context if useful.

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗