Claude Computer Use Bash Tool Timeout — Long Command Fix (2026) | AI Error Hub
Anthropic Claude Computer Use Severity: Medium Client-side execution

Claude Computer Use — Bash Tool Timeout

Claude called the bash tool for a long-running command (npm install, pytest, build) and your executor timed out. Fix: increase timeout for known-slow commands, stream stdout to detect hangs, or run in background and poll status.

Error surface: Client tool execution Category: Computer Use Tool: bash_20250124 Last tested: Nov 15, 2026
Quick Fix (TL;DR)

Use subprocess.Popen with a streaming reader and a heartbeat check instead of blocking subprocess.run(timeout=N). For truly long tasks (installs, builds, migrations), start with nohup in the background, return the PID, and let Claude poll status with subsequent bash calls.

The Error Symptoms

Common failure modes
subprocess.TimeoutExpired: Command 'npm install' timed out after 30s
  → Real install needs 2-5 minutes; default timeout too short

Command hangs indefinitely — no output, no exit
  → Waiting for stdin (interactive prompt) with no TTY

TimeoutError in asyncio.wait_for(...)
  → Async wrapper enforced its own timeout

Claude keeps retrying the same command
  → tool_result contained "timeout" but no output; Claude assumes broken

Zombie processes left after timeout
  → Didn't kill process tree; child processes orphaned

Output truncated at ~10KB
  → Buffer limit; long output lost

What Actually Causes This Error

38%
Default timeout too short for real tasks30s default fails on npm install, pip install, cargo build, pytest suites.
25%
Blocking read of full stdout/stderrsubprocess.run(capture_output=True) waits for process end. Long output = long wait.
18%
Interactive prompts (no TTY)Commands asking "y/N?" hang forever without stdin. Kill on hang.
10%
Killed process leaves orphan childrenKilling bash doesn't kill npm; needs process group kill.
9%
Not returning partial output on timeoutClaude needs to see progress to decide next action, even on timeout.

Fixes That Work (Tested Nov 2026)

1Streaming Bash Executor with Adaptive Timeout

Python · streaming subprocess with heartbeat
import subprocess, os, signal, time, threading def bash_streaming(command, hard_timeout=600, idle_timeout=60): """Run bash command with streaming output and idle detection. hard_timeout: absolute wall-clock cap idle_timeout: kill if no output for this long""" proc = subprocess.Popen( command, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, # merge streams preexec_fn=os.setsid, # process group for clean kill bufsize=1, universal_newlines=True ) output = [] last_read = time.time() start = time.time() while True: # Check timeouts if time.time() - start > hard_timeout: os.killpg(os.getpgid(proc.pid), signal.SIGTERM) return { "exit_code": -1, "output": "".join(output), "status": "hard_timeout", "elapsed": round(time.time() - start, 1) } if time.time() - last_read > idle_timeout: os.killpg(os.getpgid(proc.pid), signal.SIGTERM) return { "exit_code": -1, "output": "".join(output), "status": "idle_timeout", "note": "No output for 60s — process may be waiting for input" } # Non-blocking read line = proc.stdout.readline() if line: output.append(line) last_read = time.time() elif proc.poll() is not None: # Process done, drain remaining output.append(proc.stdout.read()) break else: time.sleep(0.05) return { "exit_code": proc.returncode, "output": "".join(output)[-10000:], # cap tool_result size "status": "completed", "elapsed": round(time.time() - start, 1) } # Use it result = bash_streaming("npm install", hard_timeout=600, idle_timeout=120)

2Command-Specific Timeout Presets

Python · smart defaults by command pattern
import re COMMAND_TIMEOUTS = [ # (regex, hard_timeout, idle_timeout, note) (r'\b(npm|yarn|pnpm)\sinstall\b', 600, 180, "deps install"), (r'\bpip\sinstall\b', 300, 120, "pip install"), (r'\bcargo\sbuild\b', 900, 300, "rust build"), (r'\bdocker\sbuild\b', 1800, 300, "docker build"), (r'\bpytest\b', 300, 120, "test suite"), (r'\bjest\b', 300, 120, "jest tests"), (r'\bpython\s.*\.py\b', 300, 60, "python script"), (r'\bwget|curl\b', 120, 30, "download"), (r'\bgit\sclone\b', 600, 60, "git clone"), ] DEFAULT = (30, 15, "quick") def timeouts_for(command): for pattern, hard, idle, note in COMMAND_TIMEOUTS: if re.search(pattern, command): return hard, idle, note return DEFAULT # Use in handler def handle_bash_tool(tool_use): command = tool_use.input["command"] hard, idle, note = timeouts_for(command) result = bash_streaming(command, hard_timeout=hard, idle_timeout=idle) return { "type": "tool_result", "tool_use_id": tool_use.id, "content": f"[{note}, {result['elapsed']}s, exit={result['exit_code']}]\n{result['output']}", "is_error": result["exit_code"] != 0 }

3Background Long Tasks with Polling

Python · nohup background pattern
import uuid, os BG_DIR = "/tmp/agent_bg" os.makedirs(BG_DIR, exist_ok=True) def start_background(command): """Run command in background; return handle for polling.""" job_id = uuid.uuid4().hex[:8] log_path = f"{BG_DIR}/{job_id}.log" pid_path = f"{BG_DIR}/{job_id}.pid" # Detach fully with setsid full = f"nohup bash -c '{command}' > {log_path} 2>&1 & echo $! > {pid_path}" subprocess.run(full, shell=True, check=True) with open(pid_path) as f: pid = int(f.read().strip()) return {"job_id": job_id, "pid": pid, "log": log_path} def check_background(job_id): """Poll background job status.""" log_path = f"{BG_DIR}/{job_id}.log" pid_path = f"{BG_DIR}/{job_id}.pid" with open(pid_path) as f: pid = int(f.read().strip()) # Check if PID is alive try: os.kill(pid, 0) alive = True except ProcessLookupError: alive = False with open(log_path) as f: output = f.read()[-5000:] # last 5KB return {"alive": alive, "output": output} # In agent: expose these as tools Claude can call # - "start_background(command)" returns job_id # - "check_background(job_id)" returns current status
Why background + polling works better for long tasks

Claude's context handles polling naturally: "I started the build; let me check progress." Poll every N screenshots. This mimics how a human works: kick off a build, do other things, check periodically. Much better than blocking the whole conversation on a 20-minute Docker build.

Preventing This Error Going Forward

  • Never use blocking subprocess.run(timeout=30) for real workloads. Streams + Popen only.
  • Kill process groups, not single processes. Use preexec_fn=os.setsid + os.killpg.
  • Always return partial output on timeout. Empty result confuses Claude.
  • Detect interactive hangs. Use </dev/null or -y flags where possible.
  • Cap output at 10KB. Long build logs waste tokens; keep tail.
  • For 5+ minute tasks, use background pattern. Poll status via subsequent tool calls.
SK
Tested by Sana K.
Researcher · AI Error Hub
Last verified: Nov 15, 2026 · Beta: computer-use-2025-01-24

Frequently Asked Questions

You can expose a timeout parameter Claude can override, but sensible defaults per command pattern (install/build/test) are safer. Claude may not know that docker build can take 20 minutes.

Don't run them from bash tool — they need a TTY. Use the computer tool for GUI editors, or filter them out ("this command requires interactive terminal, not supported"). Guide Claude to use non-interactive equivalents (less --quit, top -bn1).

Yes if you expose a kill_background(job_id) tool. Implement with os.killpg(pid, signal.SIGTERM). Give Claude 10 seconds to complete cleanup, then SIGKILL.

Binary in stdout breaks the tool_result JSON. Redirect binary output to files, then return path/size/first-bytes summary. Never send raw binary to Claude.

Simpler for quick wraps: timeout 60 npm test. But you lose streaming output and don't detect idle hangs. Use it for simple cases; the Python approach for anything sophisticated.