Claude Computer Use — screenshot, tool_result, and action loop errors (2026) — Fix Guide (2026) | AI Error Hub
Home Providers Claude Computer Use errors
Claude Computer Use · Tool Result Severity: Medium HTTP 400

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.

By Ahmed R. · Senior AI Infrastructure Engineer Published 2026-07-26 Verified 2026-07-26

Quick fix (TL;DR)

Resolution: Computer Use requires (a) declaring the 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.

400 — tool version mismatch
anthropic.BadRequestError: Error code: 400 - {'type': 'error', 'error': {'type': 'invalid_request_error', 'message': "Tool 'computer' requires the "
"'computer-use-2025-01-24' beta header."}}
400 — malformed tool_result image
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.'}}
Action loop stuck — same action repeated
[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)

ActionParamsEffect
screenshotnoneCapture current display
left_clickcoordinate [x, y]Mouse click at pixel
right_clickcoordinate [x, y]Right click
middle_clickcoordinate [x, y]Middle click
double_clickcoordinate [x, y]Double click
triple_clickcoordinate [x, y]Triple click (select line)
left_click_dragcoordinate, start_coordinateDrag from start to coordinate
mouse_movecoordinateMove without click
typetextKeyboard text input
keytext (xdotool syntax)Key press: Return, ctrl+a
scrollcoordinate, scroll_direction, scroll_amountScroll wheel
waitduration (seconds)Pause without action
cursor_positionnoneRead cursor coords

tool_result shape after a screenshot

FieldValueNotes
typetool_resultBlock type
tool_use_idstring from the tool_use blockMust match exactly
contentarray with one image blockCannot mix text and image
content[0].typeimage
content[0].source.typebase64URL sources not supported
content[0].source.media_typeimage/pngPNG only; JPEG fails
content[0].source.dataBase64 PNG bytesNo 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-24 or 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_px do 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

Fix #1

Correctly declare the tool with matching display dimensions

The <code>computer</code> tool declaration is what tells Claude the coordinate space.

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.

computer_use_setup.py
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}")
For containerized setups (Docker + Xvfb + your VNC), pick display dimensions you can actually render. 1280×800 is the recommended sweet spot — big enough for text, small enough for fast screenshots.
Fix #2

Implement a safe action loop with iteration cap and screenshot dedup

Stops runaway loops and burnt cost.

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.

safe_action_loop.py
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)
For production, isolate Computer Use in a container or dedicated VM — the agent can execute arbitrary keyboard and mouse actions. Never point it at a workstation with production credentials or unlocked sessions.
Fix #3

Downscale oversized screenshots before sending

Screenshots above ~4K pixels waste tokens and degrade vision quality.

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.

downscale_screenshot.py
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)
Simpler alternative: run the target VM at 1280×800 directly, so no scaling is needed. This avoids the coordinate-translation trap entirely.

Prevention checklist

Ship these seven safeguards once and this error stops appearing in your logs.

  • Match the tool version to the beta header — computer_20250124 requires computer-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

Anthropic's reference implementation is Linux with xdotool + xvfb. It also works on macOS and Windows if you provide equivalent screenshot and input primitives. The tool schema is OS-agnostic; only your local action executor is OS-specific.
Claude reads screenshots as images — its vision model handles OCR implicitly. You do not need to run a separate OCR step. For very small or unusual fonts, results vary; increase display DPI and font size for reliability.
Each screenshot is ~1500-2500 input tokens depending on resolution. A 20-iteration task costs roughly 30-50K input tokens plus output tokens for the model's planning. On Opus that is ~$1-3 per task; on Sonnet ~30-60¢.
Yes — the system prompt and initial task context cache normally. Screenshots do not cache because each one is unique. The savings show up on multi-turn tasks with long stable system prompts.
That is a real risk. Best practices: (1) run in an isolated VM with no production access, (2) never log into sensitive accounts from the Computer Use environment, (3) whitelist allowed applications, (4) manual review for high-stakes tasks. Computer Use is powerful and requires deliberate sandboxing.

Get the weekly AI-error digest

New fixes, provider status recaps, and one deep tutorial — every Tuesday. 8,400+ engineers.