Skip to content

[Bug]: exec tool treats subprocess nonzero exit codes as tool failures, masking stdout and exit-code semantics #97318

Description

@redasadki

Summary

The exec tool raises a tool failure at the framework level whenever the spawned subprocess exits with a nonzero code. This prevents the agent from receiving stdout, stderr, or the exit code as a normal return value. As a result, scripts that use exit codes semantically (a standard Unix convention — e.g. 0 = ok, 1 = condition X, 2 = condition Y) cannot be used as-is in any tool-driven workflow, including cron payloads.

Environment

  • OpenClaw 2026.6.10
  • Tool: exec
  • Surfaced through cron isolated agentTurn runs, but the underlying behavior is the exec tool itself.

Concrete failure mode

The cron job daily-cost-audit runs python3 daily-cost-audit.py. The script intentionally exits 1 when daily spend exceeds a threshold:

  • exit 0 → spend OK, no action
  • exit 1 → "alert needed", stdout contains the report to post

The cron prompt was written to branch on exit code:

Run python3 /path/to/daily-cost-audit.py.
Exit 0 → reply NO_REPLY.
Exit 1 → post the script's stdout to Slack as the alert.

What actually happens when the script exits 1:

  1. The exec tool call errors out at the framework level before returning to the agent
  2. The agent receives no stdout, no exit code, no return value
  3. cron action=runs shows lastError: ⚠️ 🛠️ \run python3 …` failed`
  4. consecutiveErrors increments
  5. The alert is never posted, even though the script succeeded at doing what it was asked to do

From the agent's perspective the tool simply failed and there is nothing to act on.

Why this is a problem

Exit codes carrying semantic meaning is one of the oldest and most widely used Unix conventions. Treating any nonzero exit as a tool failure:

  • Breaks idiomatic shell scripts (e.g. grep, which returns 1 on "no match", a successful no-result outcome)
  • Breaks any "alert needed" / "condition met" / "found N items" pattern
  • Hides stdout/stderr from the agent precisely when the agent most needs it
  • Is undocumented (we did not find this behavior described anywhere in the docs)

Reproduction

  1. Create /tmp/exit1.py:
    import sys
    print("threshold exceeded")
    sys.exit(1)
  2. From any agent (or cron isolated run) with toolsAllow: ["exec"], call exec with command: "python3 /tmp/exit1.py".
  3. Observe: the tool call surfaces as a failure. "threshold exceeded" is not returned to the agent's context.

Workaround

Wrap the call so the shell always exits 0 to the exec tool, encoding the real exit code in stdout:

out=$(python3 /path/to/script.py); rc=$?; echo "$out"; echo "---EXITCODE:$rc---"

The agent then parses the ---EXITCODE:N--- line. This works but is non-obvious, must be applied everywhere, and silently inverts a long-standing Unix convention.

Suggested fix

Any one of these would address the issue:

A. Treat subprocess exits in the range 0–127 as normal returns (the agent gets stdout/stderr + exit code). Only raise a tool failure for actual signals (128+, e.g. SIGKILL/137, SIGTERM/143) or exec itself being unable to spawn the process.

B. Add an exec tool option such as allowNonzeroExit: true (or acceptedExitCodes: [0,1,2]) that, when set, makes the tool return { stdout, stderr, exitCode } to the agent instead of raising.

C. At minimum, document the current behavior explicitly with the wrapper pattern above, so users do not lose hours debugging a script that "works in the shell but fails as a tool call."

Impact

Found via a daily-cost-audit cron that had been silently failing to post the daily cost-alert. The misdiagnosis cost ~45 minutes (the framework error message also made the cause hard to spot — filed separately).

Related

  • Issue on cron payload defaults (toolsAllow / lightContext) — being filed separately. The two interact in cron contexts.
  • Issue on exec tool failure message format — being filed separately.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Normal backlog priority with limited blast radius.clawsweeper:needs-maintainer-reviewClawSweeper marked this issue as needing maintainer review before automation.clawsweeper:needs-product-decisionClawSweeper marked this issue as needing a product or behavior decision.clawsweeper:no-new-fix-prClawSweeper does not recommend queueing a new automated fix PR for this issue.clawsweeper:source-reproClawSweeper found a high-confidence source-level issue reproduction.impact:message-lossChannel message delivery can be lost, duplicated, or misrouted.impact:session-stateSession, memory, transcript, context, or agent state can drift or corrupt.issue-rating: 🦞 diamond lobsterVery strong issue quality with high-confidence source-level or clear reproduction.

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions