Skip to the page

Claude computer use

Run Claude's computer use tool on a SpecStream browser session. Claude reads a screenshot and returns clicks and keystrokes, and Playwright runs them in a real Chromium over the session's connect_url. A session costs $0.10 an hour, charged by the second, so a 5 minute task is under 1 cent of browser time. On Claude Opus 5.5 and the other current models the request needs no beta header.

SpecStream for Claude computer useAgent frameworks & builders

What you need

  • Python 3.10 or newer.
  • An Anthropic API key and a model that takes computer_toolset_20260801: Claude Fable 5.2, Mythos 5.2, Opus 5.5, Sonnet 5.5 or Haiku 5.5.
  • A SpecStream account. New accounts start with $10 of credit, no card. Create one.
  1. 1. Install the packages

    The Anthropic SDK calls Claude, Playwright drives the browser, and httpx calls SpecStream. Playwright connects to the session's browser, so there is no local browser to download and you can skip playwright install.

    Terminal
    pip install anthropic playwright httpx
  2. 2. Create a SpecStream key and set both keys

    In the SpecStream workspace, open Agents & API, choose API keys, then Create API key. A key that opens a browser needs the finds:spend scope and a spending limit above 0. The limit is the most the key can spend in total, so a small one is enough for a test.

    Set it next to your model key in the shell that runs the script:

    Terminal
    export SPECSTREAM_API_KEY="YOUR_SPECSTREAM_API_KEY"
    export ANTHROPIC_API_KEY="YOUR_ANTHROPIC_API_KEY"
  3. 3. Save the agent

    Save this file as agent.py. It opens a session, connects Playwright to it, and runs the loop: send the conversation to Claude, run every action Claude returns on the page, send the results back, and repeat until Claude answers in text.

    The screenshots show the page only, with no address bar, so the script gives Claude a small goto tool next to the computer toolset to open a URL.

    agent.py
    import base64
    import os
    import time
    import anthropic
    import httpx
    from playwright.sync_api import sync_playwright
    API = "https://api.specstream.ai/v1"
    HEADERS = {"Authorization": f"Bearer {os.environ['SPECSTREAM_API_KEY']}"}
    MODEL = "claude-opus-5-5"
    TASK = "Open https://news.ycombinator.com and tell me the title of the top story."
    TOOLS = [
    # The session's screenshots are already full size, so zoom is switched off.
    {"type": "computer_toolset_20260801", "configs": {"zoom": {"enabled": False}}},
    {
    "name": "goto",
    "description": "Open a URL in the browser. The screenshots show the page only, with no address bar.",
    "input_schema": {
    "type": "object",
    "properties": {"url": {"type": "string"}},
    "required": ["url"],
    },
    },
    ]
    KEYS = {
    "return": "Enter", "enter": "Enter", "kp_enter": "Enter", "escape": "Escape", "esc": "Escape",
    "tab": "Tab", "space": "Space", "backspace": "Backspace", "delete": "Delete",
    "home": "Home", "end": "End", "page_up": "PageUp", "prior": "PageUp",
    "page_down": "PageDown", "next": "PageDown", "up": "ArrowUp", "down": "ArrowDown",
    "left": "ArrowLeft", "right": "ArrowRight", "ctrl": "Control", "control": "Control",
    "alt": "Alt", "shift": "Shift", "super": "Meta", "cmd": "Meta", "meta": "Meta",
    }
    CLICKS = {
    "left_click": ("left", 1), "right_click": ("right", 1), "middle_click": ("middle", 1),
    "double_click": ("left", 2), "triple_click": ("left", 3),
    }
    SCROLL = {"up": (0, -100), "down": (0, 100), "left": (-100, 0), "right": (100, 0)}
    def keys(text):
    return [KEYS.get(part.lower(), part) for part in text.split("+")]
    def screenshot(page):
    data = base64.b64encode(page.screenshot(type="png")).decode()
    return [{"type": "image", "source": {"type": "base64", "media_type": "image/png", "data": data}}]
    def act(page, name, args, pos):
    """Run one member of the computer toolset on the page and return its result."""
    if name == "screenshot":
    return screenshot(page)
    if name == "cursor_position":
    return f"X={pos[0]}, Y={pos[1]}"
    if name == "type":
    page.keyboard.type(args["text"])
    elif name == "key":
    for _ in range(args.get("repeat", 1)):
    page.keyboard.press("+".join(keys(args["text"])))
    elif name == "hold_key":
    held = keys(args["text"])
    for key in held:
    page.keyboard.down(key)
    time.sleep(args["duration"])
    for key in reversed(held):
    page.keyboard.up(key)
    elif name == "wait":
    time.sleep(args["duration"])
    elif name == "left_mouse_down":
    page.mouse.down()
    elif name == "left_mouse_up":
    page.mouse.up()
    else:
    # The mouse members: text holds the modifier keys for this one action.
    held = keys(args["text"]) if args.get("text") else []
    for key in held:
    page.keyboard.down(key)
    try:
    if name == "left_click_drag":
    page.mouse.move(*args["start_coordinate"])
    page.mouse.down()
    page.mouse.move(*args["coordinate"], steps=10)
    page.mouse.up()
    pos[:] = args["coordinate"]
    else:
    if args.get("coordinate"):
    pos[:] = args["coordinate"]
    page.mouse.move(*pos)
    if name in CLICKS:
    button, count = CLICKS[name]
    page.mouse.click(*pos, button=button, click_count=count)
    elif name == "scroll":
    dx, dy = SCROLL[args["scroll_direction"]]
    amount = args.get("scroll_amount", 3)
    page.mouse.wheel(dx * amount, dy * amount)
    elif name != "mouse_move":
    raise ValueError(f"{name} is not implemented")
    finally:
    for key in reversed(held):
    page.keyboard.up(key)
    return "OK"
    def run_calls(page, response, pos):
    """Run every tool_use block in order and answer each one."""
    results, failed = [], False
    for block in response.content:
    if block.type != "tool_use":
    continue
    computer = getattr(block, "toolset_name", None) == "computer"
    result = {"type": "tool_result", "tool_use_id": block.id}
    if computer:
    result["toolset_name"] = "computer"
    if failed and computer:
    result["is_error"] = True
    result["content"] = "Not executed: an earlier computer action in this turn failed."
    else:
    try:
    if computer:
    result["content"] = act(page, block.name, block.input, pos)
    else:
    page.goto(block.input["url"])
    result["content"] = "OK"
    except Exception as error:
    failed = True
    result["is_error"] = True
    result["content"] = str(error)
    results.append(result)
    return results
    def main():
    session = httpx.post(
    f"{API}/browser/sessions", headers=HEADERS, json={"timeout_s": 600}, timeout=60
    )
    session.raise_for_status()
    session = session.json()["data"]
    print("Watch it:", session["live_url"])
    client = anthropic.Anthropic()
    messages = [{"role": "user", "content": TASK}]
    pos = [0, 0]
    try:
    with sync_playwright() as p:
    browser = p.chromium.connect_over_cdp(session["connect_url"])
    context = browser.contexts[0]
    page = context.pages[0] if context.pages else context.new_page()
    for _ in range(30):
    response = client.messages.create(
    model=MODEL, max_tokens=16000, tools=TOOLS, messages=messages
    )
    messages.append({"role": "assistant", "content": response.content})
    results = run_calls(page, response, pos)
    if not results:
    print("".join(b.text for b in response.content if b.type == "text"))
    break
    messages.append({"role": "user", "content": results})
    finally:
    ended = httpx.post(
    f"{API}/browser/sessions/{session['id']}/release", headers=HEADERS, timeout=60
    ).json()["data"]
    print(f"{ended['seconds']} seconds, ${ended['charged_usd']:.4f}")
    if __name__ == "__main__":
    main()
  4. 4. Run it

    The script prints the address where you can watch the browser, then Claude's answer, then the seconds the session ran and what it cost. Change TASK to your own task.

    Terminal
    python agent.py

What the script does

Each part of the loop follows Anthropic's computer use reference.

  • POST /v1/browser/sessions opens the browser. The answer's data holds the session's id, its connect_url and its live_url.
  • connect_url is a WebSocket that speaks the Chrome DevTools Protocol, so Playwright's connect_over_cdp takes it as it is. Its token belongs to that one session, so your SpecStream key never reaches the browser side.
  • Claude often returns several tool_use blocks in one turn, for example a click, some text and a screenshot. The script runs them in order and answers every one with a tool_result that carries "toolset_name": "computer".
  • A screenshot gets an image back. Every other action gets the text OK, and cursor_position gets the coordinates.
  • When an action fails, the script stops the batch, returns the error for that block with is_error, and answers each later block with the text Anthropic asks for: Not executed: an earlier computer action in this turn failed.
  • POST /v1/browser/sessions/{id}/release ends the session, also when the script fails, and returns the seconds it ran and charged_usd.

Tool versions and the beta header

Many guides still send the computer_20251124 tool with the computer-use-2025-11-24 beta header. On the Claude API the current models return a 400 error for that version. They take the toolset, which needs no beta header.

To run the script on an older model, call client.beta.messages.create with the model's tool version and its header in betas. The older versions return one tool named computer with the action in input.action, and they take display_width_px and display_height_px.

Tool versionBeta headerModels
computer_toolset_20260801NoneClaude Fable 5.2, Mythos 5.2, Opus 5.5, Sonnet 5.5, Haiku 5.5
computer_20251124computer-use-2025-11-24Claude Fable 5.1, Mythos 5.1, Fable 5, Mythos 5, Opus 5, Sonnet 5, Opus 4.8, Opus 4.7, Opus 4.6, Sonnet 4.6, Opus 4.5
computer_20250124computer-use-2025-01-24Claude Sonnet 4.5, Haiku 4.5

How each action reaches the browser

The toolset has 17 member tools. The script runs 16 of them with Playwright and switches zoom off in configs, which also removes about 410 input tokens from each request.

Claude returnsThe script runs
screenshotpage.screenshot(type="png"), returned as a base64 image
left_click, right_click, middle_clickpage.mouse.click at coordinate with that button
double_click, triple_clickpage.mouse.click with click_count 2 or 3
left_click_dragpage.mouse.move to start_coordinate, down, move to coordinate, up
mouse_move, left_mouse_down, left_mouse_uppage.mouse.move, page.mouse.down, page.mouse.up
scrollpage.mouse.wheel, 100 pixels for each unit of scroll_amount
typepage.keyboard.type
key, hold_keypage.keyboard.press, or down and up around the wait, with names such as Return and ctrl+s changed to Playwright's Enter and Control+s
waittime.sleep for duration seconds
cursor_positionThe last position the script moved the mouse to, as text

Screenshots and coordinates

Claude gives every coordinate in the pixels of the screenshot you return. The session's screen is 1280 by 720 unless you send another viewport, and Playwright's screenshot is that screen, so the script passes Claude's coordinates to the mouse unchanged.

The models that take the toolset accept an image of up to 2,576 pixels on the long edge. The toolset does not shrink a larger one for you: the API rejects it. If you send a viewport above that, resize each screenshot before you return it and scale Claude's coordinates back up.

What a session costs

Browser time is $0.10 an hour, charged by the second. The script drives the session over connect_url, so the clicks and keystrokes add nothing: you pay for the time alone. The $1 per 1,000 actions applies only to steps you send to POST /v1/browser/sessions/{id}/actions.

Claude's tokens are billed by Anthropic. The toolset adds about 4,500 input tokens to a request, and each screenshot is billed as image input.

Session lengthBrowser time
1 minute$0.0017
5 minutes$0.0083
10 minutes$0.0167
1 hour$0.10

Keep a run inside limits

The model acts on real pages, so bound the run in the session and in the script.

  • timeout_s is the most seconds the session runs, from 60 to 3,600. Leave it out and the session ends after 300. The script sends 600.
  • allowed_domains limits the hosts the browser may open, such as ["example.com", "*.example.com"]. A page outside them is refused.
  • viewport sets the screen, 1280 by 720 unless you send another, for example {"width": 1280, "height": 800}.
  • The loop stops after 30 turns. Raise or lower the number in range(30).
  • The key's spending limit bounds what all its sessions cost together.
  • live_url is a page that shows the browser's screen as it changes. The script prints it at the start, so you can watch the run.
  • Text on a page is data. Anthropic's guide lists the precautions for computer use on the open web, such as keeping account logins away from the model and asking a person to confirm actions with real consequences.

If a run fails

The status of the call tells you which side refused.

What you seeWhat to do
401 from SpecStreamThe key is missing or wrong. Check SPECSTREAM_API_KEY.
402 from SpecStreamThe balance or the key's spending limit does not hold the first minute. Add credit or raise the limit.
403 from SpecStreamThe key lacks the finds:spend scope. Create a key that has it.
429 from SpecStreamThe account holds as many open sessions as it may. Release one, or create again after the seconds in Retry-After.
503 from SpecStreamNo browser is free at the moment. Create again after the seconds in Retry-After.
400 from Anthropic that names the tool typeThe model does not take that tool version. Use a model and a version from the table above.
invalid_request_error from AnthropicA tool_use block has no tool_result, a result lacks toolset_name, or the toolset entry carries a field of an older version such as display_width_px.

More

SpecStream for Claude computer use covers what it does in Claude computer use on one page. Every other tool has its guide in Connect your tools. You see the most a request can cost before it starts, and the matches are paid from your balance. Billing has the details. For help, email [email protected].