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
PR #1721 added JS support for custom builtins via Bash.addBuiltin() / customBuiltins. Callbacks are dispatched as Promise<String> over a NAPI threadsafe function.
If a script that invokes a custom builtin is run via executeSync(), the call silently deadlocks:
executeSync blocks the JS event loop on the synchronous NAPI call.
The custom builtin awaits a TSFN dispatch that requires the JS event loop to be free.
Nothing ever drains the queue, and the call hangs until the process is killed.
The README documents this caveat ("executeSync() will deadlock if your script invokes any custom builtin — use execute()") but there's no runtime check — users hit it before they read the docs.
Proposed fix
Mirror the existing OnOutputReentryScope / reject_on_output_reentry pattern from crates/bashkit-js/src/lib.rs:
Add in_sync_execute_depth: Arc<AtomicUsize> to SharedState.
Add a SyncExecuteScope RAII guard that increments on entry to execute_sync (on both Bash and BashTool) and decrements on drop.
Pass an Arc<AtomicUsize> clone into JsCustomBuiltinAdapter at add_builtin time.
In JsCustomBuiltinAdapter::execute, if the depth is non-zero, return ExecResult::err(..., 1) with a clear message instead of awaiting the TSFN.
Failure mode becomes: exit code 1 + stderr "custom builtins require execute() (async). executeSync() would deadlock because the JS event loop is blocked while the synchronous call is in flight" — surfaced as a normal script error, not a hang.
Optional follow-ups
Print a console warning at addBuiltin / customBuiltins time if executeSync was already used on this instance (encourages migration without breaking).
Detect cross-instance deadlock (instance A in executeSync, instance B's builtin fires) — process-wide flag on the JS thread rather than per-instance. Lower priority: rare in practice, requires more infrastructure.
Add a test in crates/bashkit-js/__test__/custom-builtins.spec.ts asserting the error result rather than a hang.
Context
PR #1721 added JS support for custom builtins via
Bash.addBuiltin()/customBuiltins. Callbacks are dispatched asPromise<String>over a NAPI threadsafe function.If a script that invokes a custom builtin is run via
executeSync(), the call silently deadlocks:executeSyncblocks the JS event loop on the synchronous NAPI call.The README documents this caveat ("
executeSync()will deadlock if your script invokes any custom builtin — useexecute()") but there's no runtime check — users hit it before they read the docs.Proposed fix
Mirror the existing
OnOutputReentryScope/reject_on_output_reentrypattern fromcrates/bashkit-js/src/lib.rs:in_sync_execute_depth: Arc<AtomicUsize>toSharedState.SyncExecuteScopeRAII guard that increments on entry toexecute_sync(on bothBashandBashTool) and decrements on drop.Arc<AtomicUsize>clone intoJsCustomBuiltinAdapteratadd_builtintime.JsCustomBuiltinAdapter::execute, if the depth is non-zero, returnExecResult::err(..., 1)with a clear message instead of awaiting the TSFN.Failure mode becomes: exit code 1 + stderr
"custom builtins require execute() (async). executeSync() would deadlock because the JS event loop is blocked while the synchronous call is in flight"— surfaced as a normal script error, not a hang.Optional follow-ups
addBuiltin/customBuiltinstime ifexecuteSyncwas already used on this instance (encourages migration without breaking).executeSync, instance B's builtin fires) — process-wide flag on the JS thread rather than per-instance. Lower priority: rare in practice, requires more infrastructure.crates/bashkit-js/__test__/custom-builtins.spec.tsasserting the error result rather than a hang.References
crates/bashkit-js/src/lib.rs—OnOutputReentryScope,reject_on_output_reentry(existing pattern to mirror)crates/bashkit-js/src/lib.rs—JsCustomBuiltinAdapter::execute(where the check goes)crates/bashkit-js/README.md— the documented caveat that this guardrail upgrades to a real error