Skip to content

opencode run exits 0 with empty stdout when a tool call is auto-rejected and the model produces no final message #36413

Description

@oldantique

Summary

In non-interactive mode (opencode run), if a tool call is auto-rejected by the
permission system and the model then produces no final assistant text, the process
exits with code 0 and prints nothing to stdout. There is no machine-detectable
signal on the two channels automation normally relies on (exit code and stdout), so
a run that was blocked and answered nothing is indistinguishable from a run that
legitimately produced an empty answer.

Environment

  • opencode 1.17.15
  • Linux (x86_64)
  • Provider: openai-compatible endpoint, DeepSeek (models deepseek/deepseek-v4-pro
    default, deepseek/deepseek-v4-flash)

Steps to reproduce

  1. Create a git repo so there is a clear project root:
    mkdir /tmp/oc-silent && cd /tmp/oc-silent && git init && echo "# test" > README.md
  2. From inside the repo, ask the model to read a path outside the project root
    (which triggers the external_directory permission auto-reject):
    opencode run "Read this EXACT absolute path and output ONLY its contents verbatim, nothing else: /etc/hostname"
  3. Inspect the exit code and stdout:
    echo "exit=$?"        # -> 0

Observed

  • Exit code: 0
  • stdout: 0 bytes (empty)
  • The model attempts the read, it is auto-rejected, and the model returns no final
    text, so nothing is printed.
  • Reproduced 5/5 times across deepseek-v4-flash and the default deepseek-v4-pro.

The only indications anything went wrong are:

  • stderr prints (interleaved with normal progress logging):
    > build · deepseek-v4-flash
    ! permission requested: external_directory (/etc/*); auto-rejecting
    ✗ Read /etc/hostname failed
    Error: The user rejected permission to use this specific tool call.
    
  • --format json includes a tool part with the rejection:
    {"type":"tool_use","part":{"tool":"read","state":{"status":"error",
     "input":{"filePath":"/etc/hostname"},
     "error":"The user rejected permission to use this specific tool call."}}}
    and the stream ends on step_finish (reason tool-calls) with no assistant
    text part.

Expected

At least one reliable, documented signal that the run did not complete normally.
Any of:

  • A non-zero exit code when a run ends with no assistant output because a tool call
    was rejected/errored, or
  • In plain-text mode, the rejection/error surfaced on stdout (or a documented
    stderr contract) rather than only as interleaved log lines, or
  • Documentation that automation must use --format json and check for tool parts
    with state.status == "error" / a missing final text part.

Why it matters

Headless automation drives opencode run and consumes exit code + stdout. Today a
run that was blocked by a permission rejection and produced no answer looks
identical to a successful empty answer: exit 0, empty stdout. There is no way to
tell "rejected and said nothing" from "legitimately returned nothing" without
opting into JSON output and parsing tool-part states, or scraping unstructured
stderr. The default contract silently swallows the failure.

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions