openclaw - ✅(Solved) Fix [Bug]: plugins uninstall can crash with exit 13 when confirmation stdin is closed [2 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#73562Fetched 2026-04-29 06:18:11
View on GitHub
Comments
1
Participants
2
Timeline
4
Reactions
0
Author
Timeline (top)
cross-referenced ×3commented ×1

openclaw plugins uninstall <id> awaits the confirmation prompt through promptYesNo(). When stdin is closed (for example < /dev/null) and --force is not used, Node can exit with Detected unsettled top-level await and code 13 instead of returning a clear CLI error.

Error Message

Error: plugins uninstall requires confirmation input. Re-run in an interactive TTY or pass --force.

Root Cause

Severity: crash / DX bug. No data corruption observed because the crash happens before config writes.

Fix Action

Fixed

PR fix notes

PR #73566: fix(cli): handle closed plugin uninstall prompt

Description (problem / solution / changelog)

Summary

  • Problem: openclaw plugins uninstall <id> could leave the confirmation prompt await unsettled when stdin closed before an answer, causing Node to exit 13 with Detected unsettled top-level await.
  • Why it matters: CI/scripts/detached shells get an internal Node crash instead of a clear CLI error; operators do not learn to use --force for non-interactive uninstall.
  • What changed: promptYesNo() now detects readline close/EOF before an answer and throws a typed prompt-closed error; plugins uninstall catches that case and exits 1 with an actionable message.
  • What did NOT change (scope boundary): Piped y / n answers and plugins uninstall --force remain supported; this does not change install records, uninstall planning, or plugin deletion semantics.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

  • Closes #73562
  • Related #73551
  • This PR fixes a bug or regression

Root Cause (if applicable)

  • Root cause: promptYesNo() awaited readline.question() without handling readline close / EOF, so closed stdin could leave the top-level CLI await unsettled.
  • Missing detection / guardrail: Existing prompt and uninstall tests covered normal answers and --force, but not EOF-before-answer.
  • Contributing context (if known): plugins uninstall already supports --force, but the interactive path did not fail cleanly when no confirmation input was possible.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file: src/cli/prompt.test.ts, src/cli/plugins-cli.uninstall.test.ts
  • Scenario the test should lock in: readline close before an answer rejects with PromptInputClosedError; plugins uninstall converts that specific prompt failure into exit 1 without config/index/file mutation.
  • Why this is the smallest reliable guardrail: The bug is in the prompt helper and uninstall command handler; focused tests cover both the shared primitive and command-level UX.
  • Existing test that already covers this (if any): None.
  • If no new test is added, why not: N/A

User-visible / Behavior Changes

openclaw plugins uninstall <id> < /dev/null now fails with a clear CLI error instead of surfacing Node's unsettled top-level-await warning. Users can rerun interactively, pipe y/n, or pass --force for non-interactive uninstall.

Diagram (if applicable)

Before:
closed stdin -> readline question never answers -> unsettled top-level await -> Node exit 13

After:
closed stdin -> prompt close detected -> plugins uninstall exits 1 with --force guidance

Security Impact (required)

  • New permissions/capabilities? (No)
  • Secrets/tokens handling changed? (No)
  • New/changed network calls? (No)
  • Command/tool execution surface changed? (No)
  • Data access scope changed? (No)
  • If any Yes, explain risk + mitigation: N/A

Repro + Verification

Environment

  • OS: Ubuntu/Linux
  • Runtime/container: Node 22.22.1, pnpm dev checkout
  • Model/provider: N/A
  • Integration/channel (if any): Plugins CLI
  • Relevant config (redacted): A managed plugin entry or install record for the uninstall target

Steps

  1. Ensure plugins uninstall <id> targets a managed plugin entry or install record.
  2. Run openclaw plugins uninstall <id> < /dev/null without --force.
  3. Observe the command result.

Expected

  • CLI exits 1 with an actionable confirmation-input error.
  • No config, install index, or plugin file mutation occurs.

Actual

  • Before this change, the prompt path could surface Node's Detected unsettled top-level await and exit 13.
  • After this change, the closed-input case is detected and handled before mutation.

Evidence

Attach at least one:

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Local verification:

  • pnpm test src/cli/prompt.test.ts src/cli/plugins-cli.uninstall.test.ts -- --reporter=verbose
  • pnpm check:changed -- --base upstream/main
  • git diff --check HEAD
  • Direct closed-stdin smoke against patched promptYesNo(): returns PromptInputClosedError with process exit 0 in the harness instead of Node exit 13.

Human Verification (required)

  • Verified scenarios: Prompt close before answer; uninstall prompt-close command path; existing --force uninstall path; normal prompt default/y/n behavior.
  • Edge cases checked: The fix avoids a blanket non-TTY ban, so piped answers remain supported by design.
  • What you did not verify: Full packaged pnpm openclaw plugins uninstall <id> < /dev/null against a real installed plugin, because the focused prompt and CLI seams directly cover the root cause.

Review Conversations

  • I replied to or resolved every bot review conversation I addressed in this PR.
  • I left unresolved only the conversations that still need reviewer or maintainer judgment.

Compatibility / Migration

  • Backward compatible? (Yes)
  • Config/env changes? (No)
  • Migration needed? (No)
  • If yes, exact upgrade steps: N/A

Risks and Mitigations

  • Risk: promptYesNo() is shared by a few CLI flows, so changing EOF handling can affect other confirmation prompts.
    • Mitigation: The new behavior only triggers when readline closes before an answer; normal answers, defaults, and global --yes remain covered by existing prompt tests.

Changed files

  • src/cli/plugins-cli-test-helpers.ts (modified, +7/-0)
  • src/cli/plugins-cli.ts (modified, +20/-2)
  • src/cli/plugins-cli.uninstall.test.ts (modified, +52/-0)
  • src/cli/prompt.test.ts (modified, +35/-3)
  • src/cli/prompt.ts (modified, +35/-2)

PR #73654: fix(cli): make crestodian exit non-zero on no-TTY (#73646)

Description (problem / solution / changelog)

What

Fixes #73646. pnpm openclaw crestodian < /dev/null (and node ./dist/index.js crestodian < /dev/null) currently:

  1. Detects no TTY
  2. Prints Crestodian needs an interactive TTY. Use --message for one command. to stderr
  3. Returns from runCrestodian cleanly — no thrown error, no process.exitCode set
  4. The CLI wrapper runCommandWithRuntime only sets runtime.exit(1) in its catch path, so the silent return falls through and Node exits 0

Shell scripts and CI flows reading $? see success and proceed as if Crestodian ran. The two in-tree sibling subcommands for the same condition (models auth login, secrets configure) both throw new Error("…") and exit non-zero. The bare-root crestodian path at cli/run-main.ts:399-404 also already sets process.exitCode = 1 directly. Crestodian-as-subcommand is the lone outlier.

Fix

Replace the silent runtime.error(...) + return in src/crestodian/crestodian.ts:93-96 with a throw new Error(...). The CLI wrapper's catch then runs runtime.error + exit(1) and the process exits non-zero. Behavior matches the bare-root path and the models auth login / secrets configure precedents.

if (!interactive || !inputIsTty || !outputIsTty) {
  throw new Error("Crestodian needs an interactive TTY. Use --message for one command.");
}

Verified locally

npx oxlint src/crestodian/crestodian.ts src/crestodian/crestodian.test.ts
# Found 0 warnings and 0 errors.

npx vitest run src/crestodian/crestodian.test.ts
# Tests  4 passed (4)

The new test pins the regression: runCrestodian({ input: { isTTY: false }, output: { isTTY: false } }) rejects with /needs an interactive TTY/ so the CLI wrapper's catch fires.

Pre-implement audit

  1. Existing-helper check. Error message + pattern reused verbatim from sibling subcommands (models auth login, secrets configure); setup-token uses the identical throw new Error("… requires an interactive TTY.") shape. ✅
  2. Shared-helper caller check. runCrestodian is called from 4 sites — cli/run-main.ts:423,433, cli/program/register.onboard.ts:182, cli/program/register.crestodian.ts:30 — all wrapped in runCommandWithRuntime (or runCommandWithRuntime-style catch in run-main.ts). All four catch paths surface the thrown error and exit non-zero. ✅
  3. Broader-fix rival scan. Zero rival PRs reference #73646. ✅

Note

#73562 (plugins uninstall < /dev/null exits 13) is a separate bug filed by the same reporter — wrong exit code there, but at least non-zero. Not addressed here.

lobster-biscuit: 73646-crestodian-no-tty-exit

Sign-Off:

  • I have read and agree to the OpenClaw Contributor License Agreement.

Changed files

  • CHANGELOG.md (modified, +1/-0)
  • src/crestodian/crestodian.test.ts (modified, +21/-0)
  • src/crestodian/crestodian.ts (modified, +6/-2)

Code Example

Error: plugins uninstall requires confirmation input. Re-run in an interactive TTY or pass --force.

---

echo "n" | openclaw plugins uninstall <id>
echo "y" | openclaw plugins uninstall <id>

---

openclaw plugins uninstall <id> --force

---

Uninstall plugin "<id>"? [y/N] Warning: Detected unsettled top-level await at file:///.../openclaw.mjs:233
  if (await tryImport("./dist/entry.js")) {
      ^

ELIFECYCLE Command failed with exit code 13.

---

timeout 10s node --input-type=module -e 'import readline from "node:readline/promises"; const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const answer = await rl.question("Question? "); console.log("answer", JSON.stringify(answer)); rl.close();' < /dev/null

---

exit=13
Question? Warning: Detected unsettled top-level await at file:///.../[eval1]:1
RAW_BUFFERClick to expand / collapse

Bug type

Crash (process/app exits or hangs)

Beta release blocker

No

Summary

openclaw plugins uninstall <id> awaits the confirmation prompt through promptYesNo(). When stdin is closed (for example < /dev/null) and --force is not used, Node can exit with Detected unsettled top-level await and code 13 instead of returning a clear CLI error.

Steps to reproduce

  1. Use OpenClaw 2026.4.27 on Node 22.
  2. Ensure the target plugin id is managed by plugin config or install records.
  3. Run pnpm openclaw plugins uninstall <id> < /dev/null.
  4. Observe the uninstall confirmation prompt starts.
  5. Observe Node exits with Detected unsettled top-level await and exit code 13.

Expected behavior

The command should fail cleanly with exit code 1 and an actionable message, for example:

Error: plugins uninstall requires confirmation input. Re-run in an interactive TTY or pass --force.

Piped answers should continue to work:

echo "n" | openclaw plugins uninstall <id>
echo "y" | openclaw plugins uninstall <id>

Non-interactive automation can also use:

openclaw plugins uninstall <id> --force

Actual behavior

With closed stdin, the prompt await can remain unsettled and Node exits with code 13:

Uninstall plugin "<id>"? [y/N] Warning: Detected unsettled top-level await at file:///.../openclaw.mjs:233
  if (await tryImport("./dist/entry.js")) {
      ^

ELIFECYCLE Command failed with exit code 13.

OpenClaw version

2026.4.27 source checkout

Operating system

Ubuntu/Linux

Install method

pnpm dev

Model

N/A

Provider / routing chain

N/A

Additional provider/model setup details

N/A. This is in the CLI prompt path before any provider/model call.

Logs, screenshots, and evidence

The OpenClaw code path is:

  • src/cli/plugins-cli.ts: plugins uninstall calls promptYesNo() when --force is not set.
  • src/cli/prompt.ts: promptYesNo() directly awaits readline.question() and does not handle readline close / EOF.

Minimal Node 22.22.1 reproduction of the dependency behavior:

timeout 10s node --input-type=module -e 'import readline from "node:readline/promises"; const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const answer = await rl.question("Question? "); console.log("answer", JSON.stringify(answer)); rl.close();' < /dev/null

Observed result:

exit=13
Question? Warning: Detected unsettled top-level await at file:///.../[eval1]:1

Impact and severity

Affected users/systems/channels: anyone running plugins uninstall from CI, scripts, detached shells, or other closed-stdin contexts without --force.

Severity: crash / DX bug. No data corruption observed because the crash happens before config writes.

Frequency: deterministic for the closed-stdin prompt case in the minimal Node/readline reproduction; OpenClaw reaches this prompt path when uninstalling a managed plugin without --force.

Consequence: automation receives exit 13 and an internal Node warning instead of a clear OpenClaw CLI error.

Additional information

Suggested fix: make promptYesNo() EOF-aware by racing readline.question() with readline close, while preserving piped y / n behavior. Then have plugins uninstall catch the closed-input case and exit 1 with a message that suggests --force.

Avoid a simple !process.stdin.isTTY gate because it would break currently working piped answers.

Suggested tests:

  • src/cli/prompt.test.ts: closed readline rejects or returns a distinct closed-input result instead of hanging.
  • src/cli/plugins-cli.uninstall.test.ts: closed confirmation exits 1, does not write config, and suggests --force.
  • Existing --force, cancel, and successful uninstall paths still pass.

Related: #73551 documents the separate stale-entry seed path that can make users try plugins uninstall as cleanup.

extent analysis

TL;DR

The issue can be fixed by making promptYesNo() EOF-aware and having plugins uninstall catch the closed-input case, exiting with a clear error message.

Guidance

  • Modify promptYesNo() to handle readline close / EOF events, allowing it to return a distinct result when input is closed.
  • Update plugins uninstall to catch the closed-input case and exit with code 1, providing a message that suggests using --force.
  • Ensure piped answers continue to work by preserving the current behavior of readline.question().
  • Add tests to verify the new behavior, including closed readline rejects, closed confirmation exits, and successful uninstall paths.

Example

// src/cli/prompt.ts
import readline from 'node:readline/promises';

async function promptYesNo(question) {
  const rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout,
  });

  const questionPromise = rl.question(question);
  const closePromise = new Promise((resolve) => {
    rl.on('close', () => resolve(false));
  });

  const result = await Promise.race([questionPromise, closePromise]);
  rl.close();

  if (result === false) {
    // Handle closed input
    throw new Error('Input closed');
  }

  return result.toLowerCase() === 'y';
}

Notes

The suggested fix requires careful handling of readline events to avoid breaking existing functionality. Additional tests should be added to ensure the new behavior works as expected.

Recommendation

Apply the workaround by modifying promptYesNo() to handle EOF events and updating plugins uninstall to catch closed-input cases, providing a clear error message. This approach addresses the root cause of the issue and provides a better user experience.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

FAQ

Expected behavior

The command should fail cleanly with exit code 1 and an actionable message, for example:

Error: plugins uninstall requires confirmation input. Re-run in an interactive TTY or pass --force.

Piped answers should continue to work:

echo "n" | openclaw plugins uninstall <id>
echo "y" | openclaw plugins uninstall <id>

Non-interactive automation can also use:

openclaw plugins uninstall <id> --force

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING