Skip to content

fix(persona-kit): spawn opencode through a shell exec (#330) - #331

Merged
khaliqgant merged 2 commits into
mainfrom
fix/330-opencode-spawn
Sep 1, 2026
Merged

fix(persona-kit): spawn opencode through a shell exec (#330)#331
khaliqgant merged 2 commits into
mainfrom
fix/330-opencode-spawn

Conversation

@khaliqgant

@khaliqgant khaliqgant commented Sep 1, 2026

Copy link
Copy Markdown
Member

Fixes #330.

The bug

Spawned directly by the runtime, opencode run fails in ~1.2s and writes nothing:

Error: {
  "name": "UnknownError",
  "data": { "message": "Unexpected server error. Check server logs for details.", "ref": "err_bcc367a8" }
}

Its own log file (~/.local/share/opencode/log/opencode.log) is created but stays 0 bytes.

With one shell between Node and the binary — same binary, args, cwd, env, stdin, model and credential — the identical invocation runs normally.

The change

-  bin: interactive.bin,
-  args,
+  bin: '/bin/sh',
+  args: ['-c', 'exec "$0" "$@"', interactive.bin, ...args],

exec replaces the shell, so the process count is unchanged, the caller still waits on the real opencode process, and its exit code and output are reported verbatim. Only the opencode branch is routed this way — claude, codex and grok spawn directly and are untouched.

Verification

End to end on daily-ship: the opaque failure without the shell, real digests posted to Slack with it — 49s and 54s, first attempt, no retries. Reproduced across many runs in both directions.

  • persona-kit — 43/43 pass
  • runtime (cloud-defaults, harness-process) — 10/10 pass

This is empirical, and deliberately narrow

I could not determine why opencode behaves differently under a direct execve, and the commit says so. What I can say is what it isn't — each ruled out by direct experiment in a live sandbox, not by reasoning:

Hypothesis Result
PATH Runner's PATH already resolves opencode
Prompt size / content 119KB synthetic and a real 72KB prompt both fine
Provider flakiness 6/6 consecutive clean runs
NODE_OPTIONS (throttle-retry hook) Succeeds with it set
HOME / XDG_DATA_HOME Correct in the child; wrong values fail differently
USER / LOGNAME, cwd, FD limits Succeeds down to ulimit -n 64
stdin write race stdin delayed 3s is still read correctly
Inherited SIGPIPE disposition Deliberately re-ignoring it (trap "" PIPE) still succeeded
Process group (timeout's setpgid) bash exec with no timeout also succeeded

The last two were the strongest a-priori candidates — Node sets SIGPIPE to SIG_IGN and ignored dispositions survive execve — and both are disproven. The shell alone is what matters.

Why land it anyway

Every opencode-harness persona is currently broken by this, and the failure names neither the cause nor the component. On daily-ship it cost days: three unrelated faults (a stale opencode in the sandbox image, a credential written where opencode never reads, and a model the workspace key doesn't authorize) all surfaced as this same message.

daily-ship currently ships its own PATH-shim workaround (AgentWorkforce/daily#37); once this lands that shim can be deleted.

The upstream question — why opencode needs a shell — is worth reporting to opencode, and #330 stays open for it.

Related

  • AgentWorkforce/daily#35 — added harness stderr to the degraded log; without it this class of failure is invisible
  • Also worth fixing: foldHarnessFailureOutput folds stderr into output only on a non-zero exit, so an exit-0 failure discards the cause entirely

🤖 Generated with Claude Code

https://claude.ai/code/session_01RNKZhUhEazLwGSxCG2w8KT

Review in cubic

Spawned directly by the runtime, `opencode run` fails in ~1.2s with an
opaque error and writes nothing:

  Error: {"name":"UnknownError","data":{"message":"Unexpected server
          error. Check server logs for details.","ref":"err_bcc367a8"}}

Its own log file (~/.local/share/opencode/log/opencode.log) is created
but stays 0 bytes. With one shell between Node and the binary — same
binary, args, cwd, env, stdin, model and credential — the identical
invocation runs normally.

Verified end to end on daily-ship: the opaque failure without the shell,
real digests posted to Slack with it (49s and 54s, first attempt).

This is empirically derived, and deliberately narrow rather than
"wrap it and hope". Ruled out by direct experiment in a live sandbox:

  - PATH (the runner's already resolves opencode)
  - prompt size and content (119KB synthetic and a real 72KB both fine)
  - provider flakiness (6/6 consecutive clean runs)
  - NODE_OPTIONS (the throttle-retry hook)
  - HOME / XDG_DATA_HOME, USER / LOGNAME, cwd, FD limits (`ulimit -n 64`)
  - stdin write race (stdin delayed 3s is still read correctly)
  - inherited SIGPIPE disposition — re-ignoring it deliberately
    (`trap "" PIPE`) still SUCCEEDED, so signal inheritance is not it
  - the process group — `timeout`'s setpgid is not needed either; bash
    exec with no timeout also succeeded

The shell alone is what matters. The underlying reason opencode behaves
differently under a direct execve is still unknown and worth an upstream
report; this makes the harness usable meanwhile.

`exec` replaces the shell, so the process count is unchanged, the caller
still waits on the real opencode process, and its exit code and output
are reported verbatim. Only the opencode branch is routed this way —
claude, codex and grok spawn directly and are untouched.

Refs #330

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RNKZhUhEazLwGSxCG2w8KT
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 1, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-09-01T07:36:54.796629Z bef56f1 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@coderabbitai

coderabbitai Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

Next included review available in 47 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: da2829f6-8cde-41ac-b9c4-5cb2fb5c3035

📥 Commits

Reviewing files that changed from the base of the PR and between bef56f1 and 1ab335c.

📒 Files selected for processing (2)
  • packages/persona-kit/src/interactive-spec.test.ts
  • packages/persona-kit/src/interactive-spec.ts

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Team

Run ID: 545c5731-9583-4c8e-865e-be872ba2f53d

📥 Commits

Reviewing files that changed from the base of the PR and between 975f1a6 and bef56f1.

📒 Files selected for processing (2)
  • packages/persona-kit/src/interactive-spec.test.ts
  • packages/persona-kit/src/interactive-spec.ts

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The non-interactive opencode harness now launches through /bin/sh -c 'exec "$0" "$@"'. The test expects the shell command structure and preserves validation of the existing run arguments. Other harnesses remain unchanged.

Changes

opencode shell launch

Layer / File(s) Summary
Shell launch and validation
packages/persona-kit/src/interactive-spec.ts, packages/persona-kit/src/interactive-spec.test.ts
The opencode launch uses /bin/sh with exec and forwards the command arguments. The test validates the shell binary, command prefix, opencode command, and existing run options.

Estimated code review effort: 2 (Simple) | ~10 minutes

Merge Risk: ⚪ Minimal · up to bef56

This narrowly changes how opencode is launched while preserving argument forwarding and exit-code behavior; the other providers remain unchanged, and no actionable merge-blocking risk remains after normal checks.

Sequence Diagram(s)

sequenceDiagram
  participant Runtime
  participant Shell as /bin/sh
  participant Opencode
  Runtime->>Shell: spawn -c exec "$0" "$@"
  Shell->>Opencode: exec opencode with run arguments
  Opencode-->>Runtime: return exit status
Loading

Suggested reviewers: willwashburn

Poem

A rabbit watched the shell command run
Through /bin/sh it hopped to the sun
exec passed each arg in line
Opencode answered right on time
The tests checked every sign

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main change: spawning opencode through a shell exec. It is concise and specific.
Description check ✅ Passed The description directly explains the opencode failure, the shell-wrapper fix, preserved behavior, scope, and verification results.
Linked Issues check ✅ Passed The changes satisfy issue #330 by routing non-interactive opencode launches through /bin/sh -c with exec while preserving the binary, arguments, environment, working directory, stdin, output, and exit…
Out of Scope Changes check ✅ Passed The code changes are limited to the opencode non-interactive launch path and its test. No unrelated implementation changes are present.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 1 functions across 2 files.
Full details: Linked Issues check

Explanation

The changes satisfy issue #330 by routing non-interactive opencode launches through /bin/sh -c with exec while preserving the binary, arguments, environment, working directory, stdin, output, and exit status. Other harnesses remain unchanged.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/330-opencode-spawn

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: bef56f1462

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +765 to +766
bin: '/bin/sh',
args: ['-c', 'exec "$0" "$@"', interactive.bin, ...args],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep the opencode spawn portable on Windows

On win32, every non-interactive opencode invocation now asks Node to spawn the literal POSIX path /bin/sh, which is absent from a standard Windows installation, so the command fails with ENOENT before reaching opencode. This affects consumers such as the CLI persona improver, which passes spec.bin directly to spawnNonInteractiveAndCapture, and the repository otherwise contains explicit Windows execution support. Please make the wrapper platform-aware or retain direct opencode spawning on platforms where this shell is unavailable.

Useful? React with 👍 / 👎.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 2 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/persona-kit/src/interactive-spec.ts Outdated
Review caught that hardcoding `/bin/sh` breaks win32: that path does not
exist on a standard Windows install, so every non-interactive opencode
spawn would ENOENT before reaching the binary. That reaches real
consumers — the CLI persona improver passes `spec.bin` straight to
spawnNonInteractiveAndCapture — and the repo supports Windows elsewhere
(persona-kit/detect.ts, cli/persona-install.ts, cli/invoke/prepare-target.ts).

Gate the wrapper on `process.platform !== 'win32'`, the same split the
sandbox exec path already uses (runtime/src/cloud-defaults.ts:185).

Windows keeps the direct spawn deliberately, not as a fallback of
convenience: the failure this works around is observed in the Linux cloud
sandboxes, and `cmd.exe` has no exec-replacement semantics, so wrapping
there would add a process rather than replace one and break exit-code and
signal passthrough.

The spec test now derives the expected shape from the platform, so it
asserts the wrapper on POSIX and the direct spawn on win32 instead of
hardcoding one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@khaliqgant

Copy link
Copy Markdown
Member Author

P1: Keep the opencode spawn portable on Windows — on win32, every non-interactive opencode invocation now asks Node to spawn the literal POSIX path /bin/sh ... so the command fails with ENOENT before reaching opencode.

Good catch — this was a real break, not a theoretical one. Fixed in 1ab335cb.

The CLI persona improver (cli-impl.ts:4502) passes spec.bin straight to spawnNonInteractiveAndCapture, and the repo does support Windows elsewhere — persona-kit/detect.ts:27, cli/persona-install.ts:469, cli/invoke/prepare-target.ts:115. Hardcoding /bin/sh would have ENOENT'd there.

const execThroughShell = process.platform !== 'win32';
return {
  bin: execThroughShell ? '/bin/sh' : interactive.bin,
  args: execThroughShell
    ? ['-c', 'exec "$0" "$@"', interactive.bin, ...args]
    : args,

Same platform split runtime/src/cloud-defaults.ts:185 already uses for sandbox exec.

Windows keeps the direct spawn deliberately rather than getting a cmd.exe equivalent: the failure being worked around is observed in the Linux cloud sandboxes, and cmd.exe has no exec-replacement semantics — wrapping there would add a process instead of replacing one, breaking exit-code and signal passthrough. A workaround that doesn't apply shouldn't be emulated on a platform that doesn't need it.

The spec test now derives the expected shape from process.platform, so it asserts the wrapper on POSIX and the direct spawn on win32 rather than hardcoding one. persona-kit 43/43 pass.

@khaliqgant
khaliqgant merged commit 68a7a34 into main Sep 1, 2026
4 checks passed
@khaliqgant
khaliqgant deleted the fix/330-opencode-spawn branch September 1, 2026 07:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

opencode harness fails in ~1.2s when spawned by the runtime, but works when wrapped in bash

1 participant