Skip to content

ACP workers hang forever on permission prompts when Gateway runs as a hidden-console Windows service (stdin.isTTY=true) #112173

Description

@Yuyorman

Summary

When the Gateway runs as a Windows service (Scheduled Task launched via the generated gateway.vbs → hidden cmd window → node), ACP claude worker sessions silently hang forever on the first tool call that is not auto-approved. The bundled acpx permission resolver decides it can prompt interactively — because the hidden console still gives the node process real console handles (process.stdin.isTTY === true) — and blocks on a y/N readline prompt written to a console no human can ever see. The configured nonInteractivePermissions: "deny" is never consulted, because it only applies on the isTTY === false branch.

Environment

  • openclaw 2026.7.1-2, Windows 11, node 24.18.0
  • Gateway installed as Windows Scheduled Task (the standard gateway.vbs / gateway.cmd pair generated by the installer, launched with WScript.Shell.Run "...gateway.cmd", 0, False)
  • ACP session: acpx claude agent (@agentclientprotocol/[email protected]), permissionMode: "approve-reads", nonInteractivePermissions: "deny"

Observed behavior

  • The Claude Code engine starts fine and streams updates; read/search-kind tool calls are auto-approved and complete.
  • The first non-read tool call (e.g. Bash cat file && xxd file) emits session/request_permission — and never gets an answer. The child CLI blocks indefinitely; the acp session records zero further activity.
  • ~31 minutes later the run is reaped with subagent run lost active execution context (subagent-error), leaving orphaned wrapper/CLI processes behind.
  • Reproduced twice back-to-back with identical timing (1,872,917 ms and 1,911,005 ms elapsed).

Root cause

In the bundled runtime (dist/runtime-*.js), resolvePermissionRequestWithDetailsresolveReadOrPromptPermission:

async function resolveReadOrPromptPermission(params, nonInteractivePolicy, allowOption, rejectOption) {
    if (isAutoApprovedReadKind(inferToolKind(params)) && allowOption) return { response: selected(allowOption.optionId) };
    if (!canPromptForPermission()) return resolveNonInteractivePermission(nonInteractivePolicy, rejectOption);
    return resolveInteractivePromptResult(params, allowOption, rejectOption);  // <-- taken in the service gateway
}

with canPromptForPermission() = process.stdin.isTTY && process.stderr.isTTY.

A cmd window launched hidden (WScript.Shell.Run ..., 0) still allocates a (hidden) console, so both handles are TTYs and the resolver goes down the interactive path: promptForPermission writes [permission] Allow ...? (y/N) to the invisible console and awaits readline.question() forever.

Verified empirically:

  • a node process launched exactly like the gateway (vbs → hidden cmd) reports {"stdinTTY":true,"stderrTTY":true};
  • driving the same claude-agent-acp adapter directly over ndjson and answering the session/request_permission request makes the identical task complete in ~27 s — the adapter and CLI are innocent.

Workaround

Redirect the gateway's stdin in gateway.cmd:

"...\node.exe" ...\openclaw\dist\index.js gateway --port 18789 < NUL

stdin.isTTY becomes false, the resolver takes the non-interactive branch, and the permission request is denied immediately per nonInteractivePermissions: "deny" (the worker then falls back gracefully, e.g. to the auto-approved Read tool). Verified live: the same probe that previously hung for 31 minutes now completes in under 2 minutes with the non-read call cleanly rejected. Downside: the installer regenerates gateway.cmd, so the patch must be re-applied after upgrades.

Suggested fix

A service-managed gateway should never consider itself interactive. Options, in increasing order of scope:

  1. Generate gateway.cmd with < NUL (or spawn with stdio: ["ignore", ...]) so the TTY heuristic fails closed.
  2. Have the gateway itself force the non-interactive permission path when it knows it is service-managed (e.g. when OPENCLAW_SERVICE_MARKER / OPENCLAW_WINDOWS_TASK_NAME env vars are present).
  3. Add a timeout to the interactive promptForPermission path so an unanswered prompt eventually falls back to the nonInteractivePermissions policy instead of wedging the worker until the ~31 min reap.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1High-priority user-facing bug, regression, or broken workflow.clawsweeper:needs-live-reproClawSweeper needs live local, crabbox, or manual validation to confirm this issue.clawsweeper:needs-maintainer-reviewClawSweeper marked this issue as needing maintainer review before automation.clawsweeper:no-new-fix-prClawSweeper does not recommend queueing a new automated fix PR for this issue.impact:otherThis issue has meaningful maintainer-visible impact outside the owned taxonomy.issue-rating: 🐚 platinum hermitGood issue quality with a plausible reproduction path needing some confirmation.

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions