You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
cli: "api get" truncates stdout when it is a pipe #52925
opencode api get returns complete output when stdout is a file, but truncated output when stdout is a pipe. The result is invalid JSON, so the usual opencode api get ... | jq pattern fails on larger responses.
Reproducible on every attempt with a ~580 KB response:
opencode api get "/api/session/<id>/message?limit=100"> file.json wc -c #1 593218 bytes, valid JSON, 2/2 runs
opencode api get "/api/session/<id>/message?limit=100"| wc -c # varies per run: 598985 / 409600 / 262144 bytes
The truncation points land on exact multiples of 4096 (262144 = 256 KiB, 327680 = 320 KiB, 409600 = 400 KiB), which looks like the process exiting before stdout has been drained rather than a transport error. Exit code is 0 in all cases.
Smaller responses often come through intact, which is why it looks intermittent: `/openapi.json" (256 KB) succeeded via redirect on every run but failed when piped.
Impact: scripts that pipe opencode api output into jq, `python" or any other consumer break silently on large payloads, with exit code 0. Workaround: redirect to a file first.
Expected: stdout should be fully flushed before exit, regardless of whether it is a TTY, a pipe, or a file.
Plugins
none
OpenCode version
2.0.22 (channel: latest)
Steps to reproduce
Pick a session with a large transcript (a few hundred KB of JSON), e.g. /api/session/<id>/message?limit=100.
opencode api get "/api/session/<id>/message?limit=100" > /tmp/a.json then validate it with python3 - c "import json;json.load(open('/tmp/a.json'))" -> no error, exit 0, complete file.
opencode api get "/api/session/<id>/message?limit=100" | python3 - c "import json,sys;json.load(sys.stdin)" -> json.decoder.JSONDecodeError: Unterminated string.
opencode api get "/api/session/<id>/message?limit=100" | wc -c -> a different, smaller number on each run (262144, 327680, 409600), always less than the redirected file size.
Reproduced with the current v2 CLI entrypoint on macOS / Bun 1.4.2, using api get /synthetic --server http://127.0.0.1:PORT against a local HTTP fixture (no session data). The same response is 600,095 bytes including the newline:
All three CLI processes exited 0. The tested files match current v2: handlers/api.ts calls process.stdout.write(...) without awaiting completion, and src/index.ts:136 explicitly calls process.exit(...) on success. This gives a session-independent reproduction for testing the stdout-drain fix. The older #46912 targets dev export/session-list/db handlers, so it does not cover this V2 handler.
I reproduced this on the V2 CLI and opened a fix: #52955. api get didn't wait for its stdout write to finish before the entrypoint called process.exit(), so Bun cut off large piped output.
Occurrence report — still reproducible on the current stable release (v2.1.4)
Adding measured evidence from another affected consumer, since #52925 (and its fix PR #52955) are still open and the defect is present in the latest stable build.
Environment
OpenCode: v2.1.4 (channel: latest, published 2026-10-23) — the newest stable release
OS: Linux (Arch-based), x86_64
Install: npm CLI (@opencode/cli) on PATH
Plugins: none
Observed behavior
opencode api get returns a truncated body when stdout is a pipe, and exits 0. The truncation always lands on a 4096 multiple and always ends mid-JSON, so the consumer sees invalid JSON with no error signal:
opencode api get "/api/model?location%5Bdirectory%5D=<dir>" | wc -c
# 262144 | 327680 | 389996 (varies per run; 389996 = the complete body)
opencode api get "/api/model?location%5Bdirectory%5D=<dir>" > out.json
wc -c < out.json
# 389996 (complete, every run)
The complete body for this request is ~390 KB, i.e. above the 256 KiB pipe-buffer size, which matches the mechanism described in #57816.
Additional data points that may help triage
It is not a version regression. Every release I could test truncates, so the fix cannot be a matter of pinning or downgrading a runtime version. Eight runs per version, same request, stdout piped to wc -c:
version
complete
truncated
2.0.32
5/8
3/8
2.1.1
4/8
4/8
2.1.2
4/8
4/8
2.1.3
3/8
5/8
2.1.4
7/8
1/8
The rate is not stable across versions, which makes a single-sample version matrix actively misleading — a first pass here made 2.1.1 look clean and 2.1.2 look broken, and that was just luck. Worth calling out for anyone bisecting.
The failure rate depends on how the consumer reads the pipe. With the same v2.1.4 binary and the same request, opencode api get ... | wc -c returned the complete body in 7 of 8 runs, while a consumer that reads the child's stdout through an io.ReadAll loop over an exec-created pipe (what gentle-ai does) was truncated in 6 of 6 runs. So "retry until it works" is not a dependable mitigation for a programmatic consumer either.
Small payloads are never truncated. Two endpoints well under the buffer size were intact 10/10 runs each through a pipe (/config, ~7 KB; /api/provider, ~1.2 KB). Only bodies above ~256 KiB are affected, which is why the symptom looks intermittent in mixed workloads.
The cut size is not fixed. 262144 and 327680 were both observed, so this is a flush race rather than a constant in the source.
Downstream impact.gentle-ai consumes this exact endpoint to discover the model catalog for its agent-assignment screen, reading the child's stdout through a pipe. With a truncated body its catalog parse fails and the TUI shows:
Could not parse models from OpenCode.
OpenCode returned an unexpected model catalog format. Verify your OpenCode version.
Because #52955 is not merged, opencode api get still corrupts piped output in v2.1.4, the release published today. Scripts that pipe the output into jq, python, or any consumer keep breaking silently with exit 0. Redirecting to a file first is the only reliable workaround.
AI assistance disclosure: the measurements, reproduction and analysis in this comment were produced with AI assistance (MiniMax-M3 via OpenCode). The reporter independently reviewed every command and result reproduced above; all numbers come from runs performed on the machine described here. No OpenCode source code was modified.
Reproduced on installed OpenCode v2.1.3 on 24 October 2026, Darwin 27.0.0 arm64. This adds evidence to #52925 and #52955.
A Python HTTP server bound only to 127.0.0.1 returned synthetic JSON. No user sessions, provider inventory, or inference were involved. HOME, XDG directories, and OpenCode configuration were isolated.
The response was {"data":" + 1,500,000 ASCII x characters + "}\n, exactly 1,500,012 bytes. Each run used opencode api --server http://127.0.0.1:<port> GET /fixture.
stdout destination
Captured bytes
JSON
CLI exit
stderr
Regular file
1,500,012
Valid, exact response
0
Empty
Python subprocess.PIPE, run 1
294,912
Unterminated string
0
Empty
Python subprocess.PIPE, run 2
327,680
Unterminated string
0
Empty
Slow pipe, 4,096-byte reads with 5 ms delay
65,536
Unterminated string
0
Empty
Reader closes after 4,096 bytes
4,096
Intentionally incomplete
0
Empty
The file SHA256 matched the response: 5640acc268f4a597d833576aa519b33d843b95fe5aca8e9f65100ae0706741f0.
The installed executable SHA256 was 1638ccfd0936dc10b15df34268b541a219822f4735d564359420d426c03383f1.
The server received only /api/info and /fixture. All observed child processes stopped, and the loopback endpoint closed. Process polling does not exclude unobserved short-lived descendants.
These measurements confirm the PIPE/file contrast on v2.1.3. They do not establish a fixed truncation limit. I did not test PR head 64446144b78a67ee18ed9d72cb8c6c7994e4cb20, so these results do not certify its fix or EPIPE handling.
Dependency-free reproduction
The script covers the regular-file and PIPE cases. The slow-reader and reader-close cases used a separate harness.
Run this Python 3 script with opencode installed. It creates a temporary isolated profile and an owned loopback endpoint.
Adding a production-impact data point and one measurement that refines the characterisation. Linux x86_64 (6.14 kernel), opencode v2.0.26, bash 5.
Impact: we run opencode api get /api/session/<id>/message?limit=50 from a systemd timer every 10 minutes to decide whether to wake a parked session. Once that session's message payload grew past ~1.3 MB, command-substitution capture (OUT="$(opencode api get …)") returned truncated JSON with exit 0 on every run. The script correctly rejected the invalid JSON and fail-closed to "do nothing" — so the wake stopped silently for three days (488 consecutive failed ticks) with no error anywhere, because the CLI reported success.
Measurement: it is not a fixed 256 KiB cap; the cut point is the consumer's drain timing.
$ opencode api get "$URL" > out.json; wc -c out.json # 1317886, valid JSON — every run
$ for i in 1 2 3; do printf %s "$(opencode api get "$URL")" | wc -c; done
217088
339968
249856
$ opencode api get "$URL" | (sleep 2; wc -c) # 65536 — exactly one pipe buffer
$ opencode api get "$URL" | wc -c # 1317886 — fast consumer keeps up
Payloads under 64 KiB are unaffected, which is why this surfaces late and intermittently. Consistent with the un-awaited write before process.exit already identified; #52955 looks like the right fix. The silent exit-0 is the dangerous part — for unattended callers, a non-zero exit on an incomplete write would be a meaningful improvement even before the flush lands.
Thanks for the detailed report. I reproduced this with a local server that returns a ~600 KB JSON body, calling opencode api get <path> --server <url>:
Redirected to a file, the output is always complete (604902 bytes).
Piped to wc -c, the size changes from run to run (604902, 262144, 589824).
Piped to a reader that waits a second before reading, it is cut off at 65536 bytes every time. Piped into Python's json.load, it fails with Unterminated string while opencode still exits with 0.
This still happens on the latest dev (45b53fe). The likely cause is that the api command writes the response to stdout and the CLI then calls process.exit before a piped stdout has been fully written. The script below reproduces it. Until it's fixed, redirecting to a file first (as you described) is a reliable workaround.
Reproduction script (repro.sh)
#!/usr/bin/env bash# Reproduces anomalyco/opencode#52925: `opencode api get` truncates stdout when it is a pipe.# Usage: ./repro.sh (uses `opencode` from PATH)# OC_DEV=/path/to/opencode/checkout ./repro.sh (runs the CLI from source)set -u
cd"$(dirname "$0")"if [ -n"${OC_DEV:-}" ];thenoc() { bun --conditions=browser --preload="$OC_DEV/packages/tui/node_modules/@opentui/solid/scripts/preload.js""$OC_DEV/packages/cli/src/index.ts""$@"; }
elseoc() { opencode "$@"; }
fi# Mock server: returns a ~600 KB JSON body for every path (stands in for a large session transcript).
cat > /tmp/big-server.ts <<'TS'const big = JSON.stringify({ items: Array.from({ length: 6000 }, (_, i) => ({ i, text: "x".repeat(80) })) })Bun.serve({ port: 4999, fetch: () => new Response(big, { headers: { "content-type": "application/json" } }) })TS
bun /tmp/big-server.ts & SERVER=$!;trap'kill $SERVER' EXIT; sleep 1
URL=http://127.0.0.1:4999
echo"redirect to file:"; oc api get /api/x --server $URL2>/dev/null > /tmp/api-out.json; wc -c < /tmp/api-out.json
echo"pipe, fast reader:";foriin 1 2 3;do oc api get /api/x --server $URL2>/dev/null | wc -c;doneecho"pipe, slow reader (sleep 1 before reading):";foriin 1 2 3;do oc api get /api/x --server $URL2>/dev/null | (sleep 1; wc -c);doneecho"pipe into python json parser, exit status of opencode:"
oc api get /api/x --server $URL2>/dev/null | (sleep 1; python3 -c 'import json,sys; json.load(sys.stdin); print("valid JSON")') ;echo"opencode exit=${PIPESTATUS[0]}"
Still reproduces on 2.2.0 (channel latest, npm @opencode/cli, Darwin 25.6.0 arm64). #52955 is still open.
This reproduction needs no session data and fails every time. It uses /openapi.json (318,081 bytes) and a reader that waits one second before draining the pipe:
opencode api get /openapi.json > spec.json; wc -c < spec.json # 318081 (3/3 runs)
opencode api get /openapi.json | wc -c # 318081 (5/5 runs)
opencode api get /openapi.json | (sleep 1; wc -c) # 65536 (3/3 runs)
opencode api get /openapi.json | jq .> /dev/null # jq: parse error at column 163840
Exit status is 0 every time. At this size a fast reader usually drains the pipe before the CLI exits, which is why earlier reports look intermittent. The sleeping reader removes that race.
The same root cause also hits opencode session export on 2.x (2.2.3 and 2.2.5, Linux x64). The export is written with one process.stdout.write(...) followed by process.exit(), so when stdout is a pipe the output stops at a multiple of 64 KiB and the exit code is still 0. Redirected to a file it's always complete. This looks like #29330 again, closed in the V1 triage.
Over a pipe (--server against a stub serving one session), the export was truncated in: ~105 KB 0/10 runs, 210 KB 1/10, 250–630 KB 7–8/10, ≥1 MB 10/10. Redirected to a file, 30/30 runs were complete (up to 3.1 MB). Anything that reads the export through a pipe (| jq, Python's subprocess.run(capture_output=True)) silently gets invalid JSON.
Plain Bun 1.4.4 behaves the same, so waiting for stdout to drain before exiting should fix it:
// bun exit-after-write.js | (sleep 1; wc -c) -> 65536// bun exit-after-write.js > out.txt -> 1048577 bytesprocess.stdout.write("y".repeat(1048576)+"\n");process.exit(0);
Repro against opencode (stub server + script)
fake_server.py serves one session with an export of about 1.2 MB:
to a file: 1270882 bytes
131072 bytes to a pipe, opencode exit 0
131072 bytes to a pipe, opencode exit 0
458752 bytes to a pipe, opencode exit 0
262144 bytes to a pipe, opencode exit 0
1270882 bytes to a pipe, opencode exit 0
65536 bytes to a pipe read 1 s late, opencode exit 0
Same symptom on a different command: opencode models can also exit 0 after writing only part of its output when stdout is a pipe.
Environment: version 1.18.34, Linux, one shared opencode.db of about 2.4 GB, about 20 concurrent opencode processes.
Measurement: opencode models was started 192 times in parallel from Node child_process with piped stdout.
About 5% of runs returned only 3-4 KB of a roughly 13 KB list, cut off mid-line, with exit code 0 and empty stderr.
Some other runs failed with Unexpected error: database is locked.
The same command with stdout redirected to a file never truncated in 600 runs.
This matches the diagnosis here (the process exits before stdout is drained). Expected: flush stdout before exit, or exit non-zero. For callers that parse the list, a silent partial list with exit 0 is hard to distinguish from a valid short list.
Activity
github-actions commentedon Oct 3, 2026
This issue might be a duplicate of existing issues. Please check:
opencode exportproduces truncated JSON when piped for large sessions.opencode debug skillreturns incomplete JSON when output is buffered/piped.r266-tech commentedon Oct 3, 2026
Reproduced with the current
v2CLI entrypoint on macOS / Bun 1.4.2, usingapi get /synthetic --server http://127.0.0.1:PORTagainst a local HTTP fixture (no session data). The same response is 600,095 bytes including the newline:All three CLI processes exited 0. The tested files match current
v2:handlers/api.tscallsprocess.stdout.write(...)without awaiting completion, andsrc/index.ts:136explicitly callsprocess.exit(...)on success. This gives a session-independent reproduction for testing the stdout-drain fix. The older #46912 targetsdevexport/session-list/db handlers, so it does not cover this V2 handler.Shalin-Shah-2002 commentedon Oct 3, 2026
I reproduced this on the V2 CLI and opened a fix: #52955.
api getdidn't wait for its stdout write to finish before the entrypoint calledprocess.exit(), so Bun cut off large piped output.conectaxp commentedon Oct 23, 2026
Occurrence report — still reproducible on the current stable release (v2.1.4)
Adding measured evidence from another affected consumer, since #52925 (and its fix PR #52955) are still open and the defect is present in the latest stable build.
Environment
@opencode/cli) onPATHObserved behavior
opencode api getreturns a truncated body when stdout is a pipe, and exits 0. The truncation always lands on a 4096 multiple and always ends mid-JSON, so the consumer sees invalid JSON with no error signal:The complete body for this request is ~390 KB, i.e. above the 256 KiB pipe-buffer size, which matches the mechanism described in #57816.
Additional data points that may help triage
It is not a version regression. Every release I could test truncates, so the fix cannot be a matter of pinning or downgrading a runtime version. Eight runs per version, same request, stdout piped to
wc -c:The rate is not stable across versions, which makes a single-sample version matrix actively misleading — a first pass here made 2.1.1 look clean and 2.1.2 look broken, and that was just luck. Worth calling out for anyone bisecting.
The failure rate depends on how the consumer reads the pipe. With the same v2.1.4 binary and the same request,
opencode api get ... | wc -creturned the complete body in 7 of 8 runs, while a consumer that reads the child's stdout through anio.ReadAllloop over an exec-created pipe (whatgentle-aidoes) was truncated in 6 of 6 runs. So "retry until it works" is not a dependable mitigation for a programmatic consumer either.Small payloads are never truncated. Two endpoints well under the buffer size were intact 10/10 runs each through a pipe (
/config, ~7 KB;/api/provider, ~1.2 KB). Only bodies above ~256 KiB are affected, which is why the symptom looks intermittent in mixed workloads.The cut size is not fixed. 262144 and 327680 were both observed, so this is a flush race rather than a constant in the source.
Downstream impact.
gentle-aiconsumes this exact endpoint to discover the model catalog for its agent-assignment screen, reading the child's stdout through a pipe. With a truncated body its catalog parse fails and the TUI shows:The message points at the OpenCode version, but the version is not the variable — the transport is. Redirecting that one call to a temp file makes the discovery succeed 6/6 runs (0/6 before), which is consistent with opencode api truncates large output when stdout is a pipe (un-awaited write before process.exit) #57816's proposed cause and fix(cli): flush api output before exit #52955's fix.
Impact on the current stable release
Because #52955 is not merged,
opencode api getstill corrupts piped output in v2.1.4, the release published today. Scripts that pipe the output intojq,python, or any consumer keep breaking silently with exit 0. Redirecting to a file first is the only reliable workaround.AI assistance disclosure: the measurements, reproduction and analysis in this comment were produced with AI assistance (MiniMax-M3 via OpenCode). The reporter independently reviewed every command and result reproduced above; all numbers come from runs performed on the machine described here. No OpenCode source code was modified.
CBEPX commentedon Oct 24, 2026
Reproduced on installed OpenCode v2.1.3 on 24 October 2026, Darwin 27.0.0 arm64. This adds evidence to #52925 and #52955.
A Python HTTP server bound only to
127.0.0.1returned synthetic JSON. No user sessions, provider inventory, or inference were involved. HOME, XDG directories, and OpenCode configuration were isolated.The response was
{"data":"+ 1,500,000 ASCIIxcharacters +"}\n, exactly 1,500,012 bytes. Each run usedopencode api --server http://127.0.0.1:<port> GET /fixture.subprocess.PIPE, run 1subprocess.PIPE, run 2The file SHA256 matched the response:
5640acc268f4a597d833576aa519b33d843b95fe5aca8e9f65100ae0706741f0.The installed executable SHA256 was
1638ccfd0936dc10b15df34268b541a219822f4735d564359420d426c03383f1.The server received only
/api/infoand/fixture. All observed child processes stopped, and the loopback endpoint closed. Process polling does not exclude unobserved short-lived descendants.These measurements confirm the PIPE/file contrast on v2.1.3. They do not establish a fixed truncation limit. I did not test PR head
64446144b78a67ee18ed9d72cb8c6c7994e4cb20, so these results do not certify its fix or EPIPE handling.Dependency-free reproduction
The script covers the regular-file and PIPE cases. The slow-reader and reader-close cases used a separate harness.
Run this Python 3 script with
opencodeinstalled. It creates a temporary isolated profile and an owned loopback endpoint.DBenYaakov commentedon Oct 26, 2026
Adding a production-impact data point and one measurement that refines the characterisation. Linux x86_64 (6.14 kernel), opencode v2.0.26, bash 5.
Impact: we run
opencode api get /api/session/<id>/message?limit=50from a systemd timer every 10 minutes to decide whether to wake a parked session. Once that session's message payload grew past ~1.3 MB, command-substitution capture (OUT="$(opencode api get …)") returned truncated JSON with exit 0 on every run. The script correctly rejected the invalid JSON and fail-closed to "do nothing" — so the wake stopped silently for three days (488 consecutive failed ticks) with no error anywhere, because the CLI reported success.Measurement: it is not a fixed 256 KiB cap; the cut point is the consumer's drain timing.
Payloads under 64 KiB are unaffected, which is why this surfaces late and intermittently. Consistent with the un-awaited write before
process.exitalready identified; #52955 looks like the right fix. The silent exit-0 is the dangerous part — for unattended callers, a non-zero exit on an incomplete write would be a meaningful improvement even before the flush lands.opencodejr commentedon Oct 27, 2026
Thanks for the detailed report. I reproduced this with a local server that returns a ~600 KB JSON body, calling
opencode api get <path> --server <url>:wc -c, the size changes from run to run (604902, 262144, 589824).json.load, it fails withUnterminated stringwhileopencodestill exits with 0.This still happens on the latest
dev(45b53fe). The likely cause is that theapicommand writes the response to stdout and the CLI then callsprocess.exitbefore a piped stdout has been fully written. The script below reproduces it. Until it's fixed, redirecting to a file first (as you described) is a reliable workaround.Reproduction script (repro.sh)
Run it:
bash repro.shjeffweiss commentedon Oct 28, 2026
Still reproduces on 2.2.0 (channel latest, npm
@opencode/cli, Darwin 25.6.0 arm64). #52955 is still open.This reproduction needs no session data and fails every time. It uses
/openapi.json(318,081 bytes) and a reader that waits one second before draining the pipe:Exit status is 0 every time. At this size a fast reader usually drains the pipe before the CLI exits, which is why earlier reports look intermittent. The sleeping reader removes that race.
dbech commentedon Nov 1, 2026
The same root cause also hits
opencode session exporton 2.x (2.2.3 and 2.2.5, Linux x64). The export is written with oneprocess.stdout.write(...)followed byprocess.exit(), so when stdout is a pipe the output stops at a multiple of 64 KiB and the exit code is still 0. Redirected to a file it's always complete. This looks like #29330 again, closed in the V1 triage.Over a pipe (
--serveragainst a stub serving one session), the export was truncated in: ~105 KB 0/10 runs, 210 KB 1/10, 250–630 KB 7–8/10, ≥1 MB 10/10. Redirected to a file, 30/30 runs were complete (up to 3.1 MB). Anything that reads the export through a pipe (| jq, Python'ssubprocess.run(capture_output=True)) silently gets invalid JSON.Plain Bun 1.4.4 behaves the same, so waiting for stdout to drain before exiting should fix it:
Repro against opencode (stub server + script)
fake_server.pyserves one session with an export of about 1.2 MB:repro.sh:Output on 2.2.5:
Written by Claude Opus 5.5 (Anthropic).
naeemakhtar110 commentedon Nov 2, 2026
Same symptom on a different command:
opencode modelscan also exit 0 after writing only part of its output when stdout is a pipe.Environment: version 1.18.34, Linux, one shared opencode.db of about 2.4 GB, about 20 concurrent opencode processes.
Measurement:
opencode modelswas started 192 times in parallel from Nodechild_processwith piped stdout.Unexpected error: database is locked.This matches the diagnosis here (the process exits before stdout is drained). Expected: flush stdout before exit, or exit non-zero. For callers that parse the list, a silent partial list with exit 0 is hard to distinguish from a valid short list.