[BUG] Bash tool dispatch with sandbox stalls 8-16s between permission decision and shell spawn; UI frozen during stall

Resolved 💬 3 comments Opened May 5, 2026 by jonare Closed Jun 3, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

Bash tool dispatch stalls 8-16s between permission decision and shell spawn; UI frozen during stall

Summary

This issue only reproduces when Claude Code's Bash sandbox is enabled (the
default on Linux, using bwrap). With the sandbox enabled, every Bash tool
call stalls ~8-16 seconds between tool_dispatch_start and Spawning shell
without login
, with no log output in the gap. The UI is frozen throughout —
the spinner animation and elapsed-time counter stop, and the terminal does
not respond to resize or Ctrl+C until Spawning shell is logged.

The stall does not depend on workspace size. Reproduced on two separate
sessions on the same workstation: one with ~30 git repos under cwd and 53
bwrap binds (8-16 s), and one with a single repo and 38 bwrap binds
(14-15 s). Cost per call is effectively constant.

The underlying sandbox binary (bwrap) and shell snapshot are fast (~170 ms
combined when measured directly), so the stall is in Claude Code's own Node
code that prepares each dispatch, blocking the main/render thread. Disabling
the sandbox for a single call (dangerouslyDisableSandbox: true) collapses
the gap from ~15 000 ms to ~80 ms (total dispatch ~445 ms), confirming the
sandbox-enabled dispatch-prep path is the culprit — not the Bash tool in
general, not bwrap itself.

Parallel Bash tool calls issued in a single message do not overlap: each
serializes on the dispatch queue and pays the full stall. Four concurrent
/bin/true calls took ~4 × 15 s, not 1 × 15 s.

Environment

  • Claude Code 2.1.128 (native install)
  • RHEL 9.7, kernel 5.14
  • bubblewrap 0.6.3
  • Node 22 (bundled with native install)
  • Model: claude-opus-4-7 via AWS Bedrock
  • Home on local XFS (not NFS)
  • Not inside tmux/zellij (TERM=xterm-256color)

Two cwd configurations tested on the same workstation:

  • Session A: workspace root with ~30 nested git repos → 53 bwrap binds (30 × .git/config, ~10 .idea/.vscode, ~10 sensitive-path masks, etc.)
  • Session B: single-repo cwd (one monorepo) → 38 bwrap binds (3 × .git/config)

Reproduction

Ask Claude Code to run trivial Bash commands (/bin/true, echo hi). The
stall is consistent across workspace sizes.

Session A: ~30 repos, 53 bwrap binds

| Call | sandbox | tool_dispatch_start → Spawning shell | total dispatch |
|-----------------------------------|---------|--------------------------------------|----------------|
| Bash #1 (sequential) | on | ~8 300 ms | 9 014 ms |
| Bash #2 (sequential) | on | ~8 500 ms | 8 728 ms |
| Bash #3 (4 requested in parallel) | on | 16 449 ms | 16 756 ms |
| Bash #4 (4 requested in parallel) | on | 15 353 ms | 15 694 ms |
| Bash #5 (4 requested in parallel) | on | 15 447 ms | 15 799 ms |
| Bash #6 (4 requested in parallel) | on | 15 775 ms | 16 142 ms |
| Bash (same workstation, same cwd) | off | ~80 ms | 445 ms |

Session B: 1 repo, 38 bwrap binds

| Call | sandbox | tool_dispatch_start → Spawning shell | total dispatch |
|-----------------------------------|---------|--------------------------------------|----------------|
| Bash #1 (sequential) | on | 15 362 ms | 15 619 ms |
| Bash #2 (sequential) | on | 14 114 ms | 14 356 ms |
| Bash #3 (sequential) | on | 15 175 ms | 15 504 ms |
| Bash #4 (requested in parallel) | on | 14 802 ms | 15 081 ms |
| Bash #5 (requested in parallel) | on | 14 818 ms | 15 060 ms |
| Bash #6 (requested in parallel) | on | 14 956 ms | 15 189 ms |

Observations:

  • Workspace size does not meaningfully change the cost. 53 binds vs 38

binds produced the same order-of-magnitude stall.

  • Parallel requests serialize. Tool calls issued in a single parallel

message had dispatch_start timestamps 15-18 s apart (no overlap). The
cost is paid once per call, not amortized across a batch.

  • Sandbox off collapses the cost. Same workstation, same cwd: ~80 ms gap

instead of 15 000+ ms. This confirms the sandbox-enabled dispatch-prep
path is the culprit — not the Bash tool in general, not bwrap itself.

Representative log excerpts from ~/.claude/debug/latest

Sequential case (Bash #1):

T+00.000 [INFO]  [Stall] tool_dispatch_start tool=Bash permissionDecisionMs=3
T+00.002 [DEBUG] Creating shell snapshot for bash (/bin/bash)
T+00.008 [DEBUG] Creating snapshot at: <snapshot-path>
T+00.504 [DEBUG] Shell snapshot created successfully (38360 bytes)
T+00.505 [DEBUG] No session environment scripts found
                 ↑
                 ~8 330 ms gap, no log lines
                 ↓
T+08.836 [DEBUG] Spawning shell without login (-l flag skipped)
T+09.014 [WARN]  [Stall] tool_dispatch_end tool=Bash outcome=error durationMs=9014

Concurrent case (Bash #3 — snapshot already cached):

T+00.000 [INFO]  [Stall] tool_dispatch_start tool=Bash permissionDecisionMs=7
                 ↑
                 16 449 ms gap, no log lines
                 ↓
T+16.449 [DEBUG] Spawning shell without login (-l flag skipped)
T+16.756 [INFO]  [Stall] tool_dispatch_end tool=Bash outcome=ok durationMs=16756

Other subsystems (FileIndex, MCP, API streams, file-permission cache writes)
keep logging in other windows while this gap occurs, so it is not general log
buffering — it is unlogged code on the hot path.

UI symptom: frozen render during stall

Throughout the 8-16 second window, the spinner animation and elapsed-time
counter both freeze. The UI thread appears blocked, not just slow — there is
no "thinking" animation, the timer stops incrementing, and the terminal does
not respond to resize or Ctrl+C until Spawning shell is logged. Consistent
with a synchronous code path on the main/render thread that never yields to
the event loop.

Independent benchmarks (ruling out the environment)

Measured on the same host, outside the stall window:

| Step | Time |
|------------------------------------------------------------------|--------|
| bwrap --unshare-net --unshare-pid + 30 --ro-bind + /bin/true | 13 ms |
| bwrap ... + socat proxy setup + /bin/true | 13 ms |
| Sourcing the shell snapshot (562 lines, 38 KB) | 152 ms |
| find <workspace> -maxdepth 6 \( -name .git -o -name .idea -o -name .vscode \) | 76 ms |
| /bin/true | ~4 ms |

Total explainable cost: ~170 ms (once snapshot is cached, which it is by call
#2). The missing 8-16 seconds are not attributable to bwrap, kernel
namespaces, shell snapshot sourcing, or filesystem walks.

Sandbox invocation details

For context, the bwrap command assembled per Bash call contains:

  • --unshare-net --unshare-pid --new-session --die-with-parent
  • Session A: 53 --bind/--ro-bind operations (30 × <repo>/.git/config,

~10 .idea/.vscode directory binds, ~10 sensitive-path masks e.g.
/dev/null~/.ssh/id_ed25519, plus standard workspace binds: cwd,
/tmp, .npm/_logs, .claude/debug).

  • Session B: 38 --bind/--ro-bind operations (3 × .git/config only;

otherwise same structure).

  • Env-var setup for HTTP/SOCKS proxies bridged via socat.

In Session A, several of the .git/config bind entries are exact duplicates
in the argv — suggesting deduplication is missing even within the current
per-call assembly. Not a performance concern at this scale, but it is a hint
that the bind list is rebuilt from scratch each call.

Hypotheses (please verify)

Since cost is ~constant across workspace sizes, the bottleneck is likely a
fixed-size per-call step rather than anything that scales with cwd:

  1. Permission rule compilation re-materialized per call. Both sessions

load similar settings (35 deny + 26 ask rules + allow rules). If these
are recompiled (e.g. regex build, glob-to-path resolution) per dispatch,
the cost would be roughly constant across workspaces.

  1. Fixed-size set of synchronous syscalls on the main thread. The

dispatch-prep path may stat/readdir a fixed candidate set (home dotfiles,
sensitive-path targets to mask, known config locations) per call. Would
explain both the ~constant duration and the UI freeze.

  1. Per-call initialization unrelated to the filesystem — e.g. re-reading

the settings.json hierarchy and re-resolving per-tool permission state.
Consistent with tool calls being strictly serialized on the dispatch queue.

Request

  • Add tracing for the tool_dispatch_start → Spawning shell interval so

the expensive step is visible in debug logs. Right now it is entirely
opaque.

  • Cache sandbox policy / bind list per session. The first call may

legitimately need to enumerate the workspace; subsequent calls should not
repeat the work. Invalidate on config/cwd change.

  • Move dispatch-prep off the render thread, or yield periodically (e.g.

setImmediate between syscalls), so the UI remains responsive even when
prep is slow.

Impact

Every Bash tool call costs ~14-16 s before the actual command runs
(regardless of workspace size), and the UI is unresponsive during the
entire stall. Iterative shell work (grep, git, test runs) becomes painful,
and users reasonably assume the CLI has hung. Batching calls in a single
parallel message does not help because they serialize on the dispatch queue.

What Should Happen?

No repeating stalls

Error Messages/Logs

Steps to Reproduce

  1. Use Claude Code 2.1.128 native install on Linux, with the default Bash

sandbox enabled (bwrap present on PATH, sandbox not disabled in settings).

  1. Start Claude Code with debug logging enabled: claude --debug (or

CLAUDE_CODE_DEBUG=1 claude). This makes [Stall] lines show up in
~/.claude/debug/<session>.txt (symlinked from ~/.claude/debug/latest).

  1. From any project directory, ask Claude to run a trivial Bash command, e.g.:

> Run /bin/true.

  1. Observe: the spinner and elapsed-time counter freeze for ~8-16 s before

the command output appears.

  1. Ask Claude to run four trivial Bash commands in a single parallel tool

batch, e.g.:
> Run /bin/true four times in parallel in one message.

  1. Observe: the four calls serialize; each pays the full stall, total wall

time ≈ 4 × 15 s.

  1. Confirm the stall is in the sandbox path by asking Claude to run the same

command with the sandbox disabled (dangerouslyDisableSandbox: true).
The stall collapses to ~80 ms.

  1. Inspect ~/.claude/debug/latest and grep for Stall:

``
grep -E "\[Stall\]|Spawning shell without login" ~/.claude/debug/latest
`
Each Bash call shows a multi-second gap between
tool_dispatch_start and
Spawning shell without login (-l flag skipped)` with no log lines in
between.

Claude Model

Opus

Is this a regression?

I don't know

Last Working Version

_No response_

Claude Code Version

2.1.128 (Claude Code)

Platform

AWS Bedrock

Operating System

Other Linux

Terminal/Shell

Other

Additional Information

_No response_

View original on GitHub ↗

This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗