Tool-contract ambiguity: exec_command.cmd lets models over-quote shell operators
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 -sor 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/shelltools - 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.
This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗