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
- 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
- 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"
- Inspect the exit code and stdout:
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
Summary
In non-interactive mode (
opencode run), if a tool call is auto-rejected by thepermission system and the model then produces no final assistant text, the process
exits with code
0and prints nothing to stdout. There is no machine-detectablesignal 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
deepseek/deepseek-v4-prodefault,
deepseek/deepseek-v4-flash)Steps to reproduce
(which triggers the
external_directorypermission auto-reject):opencode run "Read this EXACT absolute path and output ONLY its contents verbatim, nothing else: /etc/hostname"Observed
0text, so nothing is printed.
deepseek-v4-flashand the defaultdeepseek-v4-pro.The only indications anything went wrong are:
--format jsonincludes 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."}}}step_finish(reasontool-calls) with no assistanttext part.
Expected
At least one reliable, documented signal that the run did not complete normally.
Any of:
was rejected/errored, or
stderr contract) rather than only as interleaved log lines, or
--format jsonand check for tool partswith
state.status == "error"/ a missing final text part.Why it matters
Headless automation drives
opencode runand consumes exit code + stdout. Today arun 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
opencode runproduces no stdout output while running (samesilent-failure family: missing output / no error surfaced; this issue is
specifically about the exit-code + stdout contract on tool-call rejection).