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), resolvePermissionRequestWithDetails → resolveReadOrPromptPermission:
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:
- Generate
gateway.cmd with < NUL (or spawn with stdio: ["ignore", ...]) so the TTY heuristic fails closed.
- 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).
- 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.
Summary
When the Gateway runs as a Windows service (Scheduled Task launched via the generated
gateway.vbs→ hiddencmdwindow →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 ay/Nreadline prompt written to a console no human can ever see. The configurednonInteractivePermissions: "deny"is never consulted, because it only applies on theisTTY === falsebranch.Environment
2026.7.1-2, Windows 11, node 24.18.0gateway.vbs/gateway.cmdpair generated by the installer, launched withWScript.Shell.Run "...gateway.cmd", 0, False)acpxclaude agent (@agentclientprotocol/[email protected]),permissionMode: "approve-reads",nonInteractivePermissions: "deny"Observed behavior
cat file && xxd file) emitssession/request_permission— and never gets an answer. The child CLI blocks indefinitely; the acp session records zero further activity.subagent run lost active execution context(subagent-error), leaving orphaned wrapper/CLI processes behind.Root cause
In the bundled runtime (
dist/runtime-*.js),resolvePermissionRequestWithDetails→resolveReadOrPromptPermission:with
canPromptForPermission()=process.stdin.isTTY && process.stderr.isTTY.A
cmdwindow launched hidden (WScript.Shell.Run ..., 0) still allocates a (hidden) console, so both handles are TTYs and the resolver goes down the interactive path:promptForPermissionwrites[permission] Allow ...? (y/N)to the invisible console and awaitsreadline.question()forever.Verified empirically:
{"stdinTTY":true,"stderrTTY":true};session/request_permissionrequest makes the identical task complete in ~27 s — the adapter and CLI are innocent.Workaround
Redirect the gateway's stdin in
gateway.cmd:stdin.isTTYbecomesfalse, the resolver takes the non-interactive branch, and the permission request is denied immediately pernonInteractivePermissions: "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 regeneratesgateway.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:
gateway.cmdwith< NUL(or spawn withstdio: ["ignore", ...]) so the TTY heuristic fails closed.OPENCLAW_SERVICE_MARKER/OPENCLAW_WINDOWS_TASK_NAMEenv vars are present).promptForPermissionpath so an unanswered prompt eventually falls back to thenonInteractivePermissionspolicy instead of wedging the worker until the ~31 min reap.