> SpecStream is the live web data engine for teams and agents. This is the Markdown copy of https://specstream.ai/docs/integrations/claude-computer-use. Every page, with what SpecStream does and costs: https://specstream.ai/llms.txt

# 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](https://specstream.ai/integrations/claude-computer-use.md)

## 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](https://specstream.ai/signup?utm_source=claude-computer-use).

1.  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

    ```bash
    pip install anthropic playwright httpx
    ```

2.  2

    ## 2. Create a SpecStream key and set both keys

    In the SpecStream workspace, open [Agents & API](https://specstream.ai/workspace/agents?tab=keys&utm_source=claude-computer-use), 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

    ```bash
    export SPECSTREAM_API_KEY="YOUR_SPECSTREAM_API_KEY"
    export ANTHROPIC_API_KEY="YOUR_ANTHROPIC_API_KEY"
    ```

3.  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

    ```python
    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

    ## 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

    ```bash
    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 version | Beta header | Models |
| --- | --- | --- |
| `computer_toolset_20260801` | None | Claude Fable 5.2, Mythos 5.2, Opus 5.5, Sonnet 5.5, Haiku 5.5 |
| `computer_20251124` | `computer-use-2025-11-24` | Claude 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_20250124` | `computer-use-2025-01-24` | Claude 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 returns | The script runs |
| --- | --- |
| `screenshot` | `page.screenshot(type="png")`, returned as a base64 image |
| `left_click`, `right_click`, `middle_click` | `page.mouse.click` at `coordinate` with that button |
| `double_click`, `triple_click` | `page.mouse.click` with `click_count` 2 or 3 |
| `left_click_drag` | `page.mouse.move` to `start_coordinate`, `down`, `move` to `coordinate`, `up` |
| `mouse_move`, `left_mouse_down`, `left_mouse_up` | `page.mouse.move`, `page.mouse.down`, `page.mouse.up` |
| `scroll` | `page.mouse.wheel`, 100 pixels for each unit of `scroll_amount` |
| `type` | `page.keyboard.type` |
| `key`, `hold_key` | `page.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` |
| `wait` | `time.sleep` for `duration` seconds |
| `cursor_position` | The 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 length | Browser 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 see | What to do |
| --- | --- |
| `401` from SpecStream | The key is missing or wrong. Check `SPECSTREAM_API_KEY`. |
| `402` from SpecStream | The balance or the key's spending limit does not hold the first minute. Add credit or raise the limit. |
| `403` from SpecStream | The key lacks the `finds:spend` scope. Create a key that has it. |
| `429` from SpecStream | The account holds as many open sessions as it may. Release one, or create again after the seconds in `Retry-After`. |
| `503` from SpecStream | No browser is free at the moment. Create again after the seconds in `Retry-After`. |
| `400` from Anthropic that names the tool type | The model does not take that tool version. Use a model and a version from the table above. |
| `invalid_request_error` from Anthropic | A `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](https://specstream.ai/integrations/claude-computer-use.md) covers what it does in Claude computer use on one page. Every other tool has its guide in [Connect your tools](https://specstream.ai/docs/integrations.md). You see the most a request can cost before it starts, and the matches are paid from your balance. [Billing](https://specstream.ai/docs/billing.md) has the details. For help, email [support@specstream.ai](mailto:support@specstream.ai).
