Claude Computer Use — tool_result malformed or action loop stuck
Computer Use is the Anthropic API primitive that turns Claude into an agent driving your desktop or a headless VM. It fails in ways that are unique to visual grounding — bad screenshot encoding, off-by-one coordinate systems, and tool_result blocks that do not match the shape Claude expects.
Quick fix (TL;DR)
computer_20250124 tool (or current version) with your display dimensions, (b) executing each returned tool_use action locally, (c) capturing a screenshot after every action, and (d) sending it back as a tool_result block containing an image. Fix by matching the tool version to your beta header, using base64 PNG (not JPEG), and putting a hard iteration cap on the loop.Real error messages you'll see
These are the exact strings returned by the Claude API service and its SDKs when this error occurs. Copy-paste-searching any of them should land on this page.
anthropic.BadRequestError: Error code: 400 - {'type': 'error', 'error': {'type': 'invalid_request_error', 'message': "Tool 'computer' requires the "
"'computer-use-2025-01-24' beta header."}}anthropic.BadRequestError: Error code: 400 - {'type': 'error', 'error': {'type': 'invalid_request_error', 'message': 'tool_result content image must have source.type=base64 and source.media_type=image/png.'}}[iter 12] action: {"action": "left_click", "coordinate": [640, 400]}
[iter 13] action: {"action": "left_click", "coordinate": [640, 400]}
[iter 14] action: {"action": "left_click", "coordinate": [640, 400]}
(model is clicking the same spot; screenshot is not changing)
Reference
Computer Use tool actions (as of 2025-01-24)
| Action | Params | Effect |
|---|---|---|
screenshot | none | Capture current display |
left_click | coordinate [x, y] | Mouse click at pixel |
right_click | coordinate [x, y] | Right click |
middle_click | coordinate [x, y] | Middle click |
double_click | coordinate [x, y] | Double click |
triple_click | coordinate [x, y] | Triple click (select line) |
left_click_drag | coordinate, start_coordinate | Drag from start to coordinate |
mouse_move | coordinate | Move without click |
type | text | Keyboard text input |
key | text (xdotool syntax) | Key press: Return, ctrl+a |
scroll | coordinate, scroll_direction, scroll_amount | Scroll wheel |
wait | duration (seconds) | Pause without action |
cursor_position | none | Read cursor coords |
tool_result shape after a screenshot
| Field | Value | Notes |
|---|---|---|
type | tool_result | Block type |
tool_use_id | string from the tool_use block | Must match exactly |
content | array with one image block | Cannot mix text and image |
content[0].type | image | |
content[0].source.type | base64 | URL sources not supported |
content[0].source.media_type | image/png | PNG only; JPEG fails |
content[0].source.data | Base64 PNG bytes | No data: prefix |
Root causes, ranked by frequency
Based on developer reports across Claude API forums, GitHub issues, and Anthropic community during 2025–2026.
- 26%Beta header missing or wrong version. Computer Use is behind a beta header (
computer-use-2025-01-24or current). Older versions have different tool names. - 18%Screenshot encoded as JPEG. Claude requires PNG. Common when using a screenshot library that defaults to JPEG.
- 14%Coordinate system mismatch. The declared
display_width_px/display_height_pxdo not match the actual screen; clicks miss targets. - 12%Missing screenshot after action. Loop executes the action but does not send a fresh screenshot back — Claude has no updated visual context.
- 10%No iteration cap on the action loop. Model gets stuck retrying the same action; loop runs until you kill it, burning cost.
- 8%tool_use_id not echoed in tool_result. Every tool_result must reference the id from its matching tool_use. Missing or mismatched id fails validation.
- 7%Screenshot too large. Screens above ~4K pixels get 400 or degrade the vision quality dramatically. Downscale before sending.
- 5%Concurrent actions on a single virtual desktop. Two Claude sessions competing for the same display produce chaotic state.
Fixes — copy-paste solutions
Correctly declare the tool with matching display dimensions
Declare the tool with your actual display width, height, and index. The coordinates Claude returns are in pixels relative to these dimensions — a mismatch means every click lands on the wrong spot.
import anthropic client = anthropic.Anthropic( default_headers={"anthropic-beta": "computer-use-2025-01-24"} # verify current beta ) # The tool declaration tells Claude the coordinate space it operates in COMPUTER_TOOL = { "type": "computer_20250124", # match the beta header version "name": "computer", "display_width_px": 1280, # MUST match your actual screen/VM "display_height_px": 800, "display_number": 1, # X display number (Linux VMs) } # Send the first message — Claude will respond with a tool_use (usually screenshot first) response = client.messages.create( model="claude-opus-4-7", max_tokens=1024, tools=[COMPUTER_TOOL], messages=[{ "role": "user", "content": "Open the file explorer and navigate to /home/user/reports.", }], ) # Inspect what the model asks for for block in response.content: if block.type == "tool_use": print(f"Model requests: {block.name}.{block.input.get('action')} @ {block.input}")
Implement a safe action loop with iteration cap and screenshot dedup
The loop reads Claude's tool_use, executes it on the local machine, screenshots, and sends the result back. Add: (1) iteration cap, (2) same-action-in-a-row detector, (3) screenshot dedup to catch stuck loops.
import base64 import hashlib import subprocess import anthropic from io import BytesIO from PIL import ImageGrab client = anthropic.Anthropic( default_headers={"anthropic-beta": "computer-use-2025-01-24"} ) MAX_ITERATIONS = 30 STUCK_THRESHOLD = 3 # same action + same screenshot 3 iterations = give up def screenshot_b64() -> tuple[str, str]: """Grab screen, return (base64_png, sha256_hash).""" img = ImageGrab.grab() buf = BytesIO() img.save(buf, format="PNG") data = buf.getvalue() return base64.b64encode(data).decode(), hashlib.sha256(data).hexdigest() def execute_action(action: dict) -> None: """Translate Claude's action to a local OS operation (Linux + xdotool).""" kind = action["action"] if kind == "screenshot": return # nothing to do — screenshot is captured after elif kind in ("left_click", "right_click", "middle_click", "double_click"): x, y = action["coordinate"] button = {"left_click": "1", "right_click": "3", "middle_click": "2"}.get(kind, "1") clicks = "2" if kind == "double_click" else "1" subprocess.run(["xdotool", "mousemove", str(x), str(y), "click", "--repeat", clicks, button], check=True) elif kind == "type": subprocess.run(["xdotool", "type", "--", action["text"]], check=True) elif kind == "key": subprocess.run(["xdotool", "key", action["text"]], check=True) # ... other actions omitted for brevity def run_agent(task: str) -> anthropic.types.Message: messages = [{"role": "user", "content": task}] prev_action_key = None prev_screenshot_hash = None stuck_count = 0 for iteration in range(MAX_ITERATIONS): response = client.messages.create( model="claude-opus-4-7", max_tokens=1024, tools=[{ "type": "computer_20250124", "name": "computer", "display_width_px": 1280, "display_height_px": 800, }], messages=messages, ) messages.append({"role": "assistant", "content": response.content}) if response.stop_reason != "tool_use": return response # Claude is done # Execute all tool_use blocks in the response tool_results = [] for block in response.content: if block.type != "tool_use": continue execute_action(block.input) b64, screenshot_hash = screenshot_b64() # Stuck detection action_key = f"{block.input.get('action')}:{block.input.get('coordinate')}" if action_key == prev_action_key and screenshot_hash == prev_screenshot_hash: stuck_count += 1 if stuck_count >= STUCK_THRESHOLD: raise RuntimeError(f"Stuck at iteration {iteration}: {action_key}") else: stuck_count = 0 prev_action_key = action_key prev_screenshot_hash = screenshot_hash tool_results.append({ "type": "tool_result", "tool_use_id": block.id, "content": [{ "type": "image", "source": {"type": "base64", "media_type": "image/png", "data": b64}, }], }) messages.append({"role": "user", "content": tool_results}) raise RuntimeError(f"Iteration cap ({MAX_ITERATIONS}) reached without completion.") result = run_agent("Open the terminal and run: ls -la /home") print(result.content[-1].text)
Downscale oversized screenshots before sending
Modern displays produce screenshots that are far larger than Claude's optimal vision input. Downscale to a reasonable target (e.g. 1280×800 or 1024×768 aspect-preserved) before base64-encoding.
from PIL import Image, ImageGrab from io import BytesIO import base64 TARGET_MAX_DIM = 1280 # matches your declared display width def screenshot_scaled_b64() -> tuple[str, tuple[int, int]]: """Grab, downscale to TARGET_MAX_DIM, return (base64, new_dims). IMPORTANT: When you downscale, the coordinates Claude returns are in the DOWNSCALED space. You must upscale them back before executing clicks on the real display. Alternatively, declare the tool with the downscaled dims. """ img = ImageGrab.grab() w, h = img.size scale = min(TARGET_MAX_DIM / max(w, h), 1.0) if scale < 1.0: img = img.resize((int(w * scale), int(h * scale)), Image.LANCZOS) buf = BytesIO() img.save(buf, format="PNG", optimize=True) return base64.b64encode(buf.getvalue()).decode(), img.size # Recommended pattern: declare the tool with the scaled dimensions, # so Claude returns coordinates in the same space you send screenshots in. b64, (scaled_w, scaled_h) = screenshot_scaled_b64() COMPUTER_TOOL = { "type": "computer_20250124", "name": "computer", "display_width_px": scaled_w, "display_height_px": scaled_h, } # When executing clicks, remember to translate scaled coordinates back to # real display coordinates: def to_real_coords(scaled_x: int, scaled_y: int, real_w: int, real_h: int, scaled_w: int, scaled_h: int) -> tuple[int, int]: return int(scaled_x * real_w / scaled_w), int(scaled_y * real_h / scaled_h)
Prevention checklist
Ship these seven safeguards once and this error stops appearing in your logs.
- Match the tool version to the beta header —
computer_20250124requirescomputer-use-2025-01-24. - Screenshot as PNG only; JPEG fails at request validation.
- Cap the action loop at 30-50 iterations; also add a "same action, same screenshot" stuck detector.
- Isolate Computer Use to a container or throwaway VM — never a workstation with sensitive access.
- Log every tool_use with its coordinates + screenshot hash for post-mortem when a task fails.
- Downscale screenshots to ~1280×800; run the target VM natively at that resolution when possible.
- For long-running agents, checkpoint the message history to disk so you can resume without re-running actions.
Frequently asked questions
Related errors & hubs
Get the weekly AI-error digest
New fixes, provider status recaps, and one deep tutorial — every Tuesday. 8,400+ engineers.