Skip to main content
RapidDev - Software Development Agency
build-ai-agent

How to Build a Computer Use Agent

A computer-use agent controls an entire desktop by taking screenshots and issuing clicks and keystrokes at pixel coordinates, which lets it drive software with no API and no web version. This guide builds one with Claude's computer_toolset_20260801 that enters vendor invoices into a 20-year-old desktop accounting app inside a sandboxed container, with a human-approval gate before it saves anything. Budget 60-90 minutes and a sandbox environment to test in.

What you'll learn

  • Wire up Claude's computer_toolset_20260801 tool against a real virtual display, not a mocked screenshot
  • Translate each member action (screenshot, click, type, key, scroll) into real input events with pyautogui and mss
  • Build the tool_result response format the API expects, including the sequential stop-on-failure rule for batched actions
  • Gate the one truly destructive action, saving, behind a human-approval prompt the agent can't skip
  • Run the whole thing inside a disposable, network-restricted sandbox instead of your own desktop
Book a free consultation
4.9Clutch rating ⭐
1,000+Happy partners
20+Countries served
200+Team members
Advanced11 min read60-90 minPython 3.10+, anthropic SDK, a sandboxed Linux container or VM with a virtual display, Claude Opus or Sonnet 5.5Last updated November 2026RapidDev Engineering Team
TL;DR

A computer-use agent controls an entire desktop by taking screenshots and issuing clicks and keystrokes at pixel coordinates, which lets it drive software with no API and no web version. This guide builds one with Claude's computer_toolset_20260801 that enters vendor invoices into a 20-year-old desktop accounting app inside a sandboxed container, with a human-approval gate before it saves anything. Budget 60-90 minutes and a sandbox environment to test in.

Quick facts about this guide
FactValue
ToolClaude computer use
DifficultyAdvanced
Time required60-90 min
CompatibilityPython 3.10+, anthropic SDK, a sandboxed Linux container or VM with a virtual display, Claude Opus or Sonnet 5.5
Last updatedNovember 2026

What you'll build

You'll build an agent that enters vendor invoices into a 20-year-old desktop accounting app: no API, no web version, just a Windows-era GUI with fields, tabs, and a Save button. That's the exact situation computer use exists for, and it's a different problem than a browser agent, which only ever works inside a browser tab reading a DOM.

A computer-use agent instead takes a screenshot of the entire screen, sends it to Claude, and gets back pixel-coordinate actions: click here, type this, press that key. There's no DOM, no accessibility tree, no structured page content, just an image and coordinates, which means it works against any application at all, including ones built before APIs existed, at the cost of being slower and far more token-hungry per action than a browser agent reading structured page content. Use computer use only when there's genuinely no DOM, no API, and no other way in; reach for a browser agent for anything with a URL.

The twist in this guide, same as the system prompt and SDK guides, is a human-approval gate. The agent can click around and fill in fields freely, but it stops and asks before it presses the key combination that actually commits the invoice. Everything before that point is reversible by just not saving; the save itself is the one action that isn't.

Prerequisites

  • An Anthropic API key with access to a Claude 5.5 model (Opus, Sonnet, or Haiku 5.5)
  • A disposable sandbox, a Docker container or VM with Xvfb and the target desktop app installed; never point this at your primary machine
  • pip install anthropic pyautogui mss pillow
  • Comfort running a headless Linux container with a virtual display

Step-by-step guide

1

Start a virtual display inside the sandbox

Install Xvfb inside your container or VM and start it on a virtual display number, then point the DISPLAY environment variable at it so every GUI app, including the legacy accounting software, renders into that virtual framebuffer instead of a physical screen. This is the foundation everything else sits on: Claude never touches your real desktop, only this disposable one.

typescript
1apt-get install -y xvfb x11-utils
2Xvfb :99 -screen 0 1024x768x24 &
3export DISPLAY=:99
4pip install anthropic pyautogui mss pillow
5export ANTHROPIC_API_KEY="sk-ant-..."

Expected result: echo $DISPLAY prints :99, and launching any X11 app inside the container doesn't error with 'cannot open display.'

2

Capture real screenshots with mss

Write a function that grabs the current virtual display and returns it as base64-encoded PNG bytes, the exact format a screenshot tool_result needs. mss is fast enough to call on every single step of the loop without adding noticeable latency, which matters since a computer-use agent takes a fresh screenshot after nearly every action.

typescript
1import mss, io, base64
2from PIL import Image
3
4def capture_screenshot_base64() -> str:
5 with mss.mss() as sct:
6 shot = sct.grab(sct.monitors[1])
7 img = Image.frombytes("RGB", shot.size, shot.bgra, "raw", "BGRX")
8 buf = io.BytesIO()
9 img.save(buf, format="PNG")
10 return base64.standard_b64encode(buf.getvalue()).decode()

Expected result: Calling the function returns a long base64 string that decodes back into a real PNG of the virtual display's current contents.

3

Translate each member action into real input events

The computer_toolset_20260801 tool sends you member actions like left_click, type, key, and scroll with coordinates and text, not instructions for how to perform them. Map each one to pyautogui calls: click at a coordinate, type text with a short interval so the legacy app's input handler doesn't drop keystrokes, and send key combinations as hotkeys.

typescript
1import pyautogui
2
3def perform_action(name: str, input_data: dict) -> str:
4 if name == "left_click":
5 coord = input_data.get("coordinate")
6 pyautogui.click(x=coord[0], y=coord[1]) if coord else pyautogui.click()
7 return "OK"
8 if name == "type":
9 pyautogui.typewrite(input_data["text"], interval=0.01)
10 return "OK"
11 if name == "key":
12 pyautogui.hotkey(*input_data["text"].lower().split("+"))
13 return "OK"
14 if name == "scroll":
15 amt = input_data.get("scroll_amount", 3)
16 sign = 1 if input_data.get("scroll_direction") == "up" else -1
17 pyautogui.scroll(sign * amt)
18 return "OK"
19 raise ValueError(f"Unhandled action: {name}")

Expected result: Each member action the model issues produces a real, visible effect on the virtual display, confirmed by diffing before/after screenshots.

4

Build the tool_result response and gate the save action

Every tool_result for a computer action needs toolset_name set to 'computer', an image content block for screenshot or zoom and text for everything else, and is_error set when an action fails. Add one more rule on top: if the action is a key press matching your save shortcut, stop and ask a human before it runs. Everything up to that point in the invoice entry is harmless to redo; the save is not.

typescript
1def requires_approval(name: str, input_data: dict) -> bool:
2 return name == "key" and input_data.get("text", "").lower() == "ctrl+s"
3
4def handle_action(name: str, input_data: dict):
5 if name in ("screenshot", "zoom"):
6 return [{"type": "image", "source": {"type": "base64",
7 "media_type": "image/png", "data": capture_screenshot_base64()}}]
8 return perform_action(name, input_data)

Expected result: A handler that performs every action except the save keystroke immediately, and routes that one keystroke through an approval check first.

5

Run the agent loop with the approval gate wired in

Call messages.create with the computer_toolset_20260801 tool on every turn, process each tool_use block in order (stopping the batch at the first failure, as the toolset requires), and feed the results back as a tool_result message. When requires_approval fires, print what's about to happen and block on a real approval before letting the keystroke through.

typescript
1def process_tool_calls(response):
2 results, failed = [], False
3 for block in response.content:
4 if block.type != "tool_use" or getattr(block, "toolset_name", None) != "computer":
5 continue
6 result = {"type": "tool_result", "tool_use_id": block.id, "toolset_name": "computer"}
7 if failed:
8 result["content"], result["is_error"] = "Not executed: an earlier action failed.", True
9 elif requires_approval(block.name, block.input):
10 print("[APPROVAL NEEDED] Claude wants to save this invoice (Ctrl+S).")
11 if input("Approve? (y/n): ").strip().lower() == "y":
12 result["content"] = handle_action(block.name, block.input)
13 else:
14 result["content"], result["is_error"] = "Save blocked by operator.", True
15 failed = True
16 else:
17 result["content"] = handle_action(block.name, block.input)
18 results.append(result)
19 return results

Expected result: The agent fills in invoice fields with no prompts, then stops and prints [APPROVAL NEEDED] the moment it reaches for Ctrl+S.

6

Test on one invoice with a before/after screenshot log

Run the full loop against a single test invoice and save a screenshot before and after the approval decision, not just the final state. When someone asks six months from now why a particular invoice has the wrong total, that pair of screenshots is the audit trail that answers it, long after chat logs would have rotated out.

typescript
1import time
2
3def log_screenshot(label: str):
4 with open(f"audit_{label}_{int(time.time())}.png", "wb") as f:
5 f.write(base64.standard_b64decode(capture_screenshot_base64()))

Expected result: Two PNG files per invoice run, one from right before the save approval and one from right after, saved alongside your other logs.

7

Harden it: network isolation and a hard step cap

Run the sandbox with no outbound network access except to Anthropic's API, since a computer-use agent that can see and type into anything on screen is a bad thing to also give internet access to. Cap max_iterations so a confused agent clicking in circles fails loudly after a fixed number of steps instead of running (and billing) indefinitely.

typescript
1MAX_ITERATIONS = 20
2# In your container's network policy: allow only api.anthropic.com, deny all other egress.

Expected result: A sandbox that can reach Claude's API and nothing else, and an agent loop that gives up with a clear error after 20 steps instead of looping silently.

Complete working example

invoice_entry_agent.py
1"""Computer-use agent: enters a vendor invoice into a legacy desktop app,
2stopping for human approval before it saves. Run inside a disposable sandbox only.
3pip install anthropic pyautogui mss pillow; Xvfb running; ANTHROPIC_API_KEY set.
4"""
5import base64
6import io
7import time
8
9import anthropic
10import mss
11import pyautogui
12from PIL import Image
13
14MAX_ITERATIONS = 20
15
16client = anthropic.Anthropic()
17
18
19def capture_screenshot_base64() -> str:
20 with mss.mss() as sct:
21 shot = sct.grab(sct.monitors[1])
22 img = Image.frombytes("RGB", shot.size, shot.bgra, "raw", "BGRX")
23 buf = io.BytesIO()
24 img.save(buf, format="PNG")
25 return base64.standard_b64encode(buf.getvalue()).decode()
26
27
28def log_screenshot(label: str) -> None:
29 with open(f"audit_{label}_{int(time.time())}.png", "wb") as f:
30 f.write(base64.standard_b64decode(capture_screenshot_base64()))
31
32
33def perform_action(name: str, input_data: dict) -> str:
34 if name == "left_click":
35 coord = input_data.get("coordinate")
36 pyautogui.click(x=coord[0], y=coord[1]) if coord else pyautogui.click()
37 return "OK"
38 if name == "type":
39 pyautogui.typewrite(input_data["text"], interval=0.01)
40 return "OK"
41 if name == "key":
42 pyautogui.hotkey(*input_data["text"].lower().split("+"))
43 return "OK"
44 if name == "scroll":
45 amt = input_data.get("scroll_amount", 3)
46 sign = 1 if input_data.get("scroll_direction") == "up" else -1
47 pyautogui.scroll(sign * amt)
48 return "OK"
49 raise ValueError(f"Unhandled action: {name}")
50
51
52def handle_action(name: str, input_data: dict):
53 if name in ("screenshot", "zoom"):
54 return [{"type": "image", "source": {"type": "base64",
55 "media_type": "image/png", "data": capture_screenshot_base64()}}]
56 return perform_action(name, input_data)
57
58
59def requires_approval(name: str, input_data: dict) -> bool:
60 return name == "key" and input_data.get("text", "").lower() == "ctrl+s"
61
62
63def process_tool_calls(response) -> list[dict]:
64 results, failed = [], False
65 for block in response.content:
66 if block.type != "tool_use" or getattr(block, "toolset_name", None) != "computer":
67 continue
68 result = {"type": "tool_result", "tool_use_id": block.id, "toolset_name": "computer"}
69 if failed:
70 result["content"], result["is_error"] = "Not executed: an earlier action failed.", True
71 elif requires_approval(block.name, block.input):
72 log_screenshot("before_save")
73 print("\n[APPROVAL NEEDED] Claude wants to save this invoice (Ctrl+S).")
74 if input("Approve? (y/n): ").strip().lower() == "y":
75 result["content"] = handle_action(block.name, block.input)
76 log_screenshot("after_save")
77 else:
78 result["content"], result["is_error"] = "Save blocked by operator.", True
79 failed = True
80 else:
81 try:
82 result["content"] = handle_action(block.name, block.input)
83 except Exception as e:
84 result["content"], result["is_error"] = f"Error: {e}", True
85 failed = True
86 results.append(result)
87 return results
88
89
90def agent_loop(task: str) -> None:
91 messages = [{"role": "user", "content": task}]
92 for step in range(MAX_ITERATIONS):
93 response = client.messages.create(
94 model="claude-sonnet-5-5",
95 max_tokens=2048,
96 tools=[{"type": "computer_toolset_20260801"}],
97 messages=messages,
98 )
99 messages.append({"role": "assistant", "content": response.content})
100 tool_results = process_tool_calls(response)
101 if not tool_results:
102 print("Task complete.")
103 return
104 messages.append({"role": "user", "content": tool_results})
105 print(f"Stopped after {MAX_ITERATIONS} steps without completing.")
106
107
108if __name__ == "__main__":
109 agent_loop(
110 "Open the Invoices tab in the accounting app, enter a new invoice for "
111 "vendor 'Acme Supplies', amount $482.50, dated today, then save it."
112 )
113

Common mistakes

Why it's a problem: Running a computer-use agent against your actual desktop instead of a sandbox

How to avoid: Always run inside a disposable container or VM with a virtual display. The agent can click and type anywhere on screen; a sandbox limits the blast radius of a mistake to something you can throw away.

Why it's a problem: Reaching for computer use when the target actually has a web UI or API

How to avoid: Check for a DOM or API first. A browser agent or a direct API call is faster, cheaper, and more reliable than screenshot-and-click for anything with a URL.

Why it's a problem: Trying to infer which click is 'the destructive one' from coordinates alone

How to avoid: Gate on an explicit, named action, like a specific save keystroke, instead of guessing intent from pixel positions. Coordinates shift; the save shortcut doesn't.

Why it's a problem: Letting the sandbox reach the open internet

How to avoid: Restrict egress to only the API endpoint you need. A desktop-controlling agent with open internet access is a bigger risk than the legacy app it's supposed to operate.

Why it's a problem: Not capping max_iterations and letting a confused agent loop indefinitely

How to avoid: Set a hard step limit. Each step is a full screenshot round-trip; an uncapped loop fails by draining your budget, not by erroring cleanly.

Best practices

  • Run every computer-use agent inside a sandbox with no outbound network access beyond the model API.
  • Match your virtual display resolution to what you tell Claude, so returned coordinates land where you expect.
  • Gate explicit, named destructive actions (a save shortcut, a submit keystroke) behind human approval rather than trying to infer intent from clicks.
  • Log a screenshot immediately before and after any approved action, not just the final state, for a real audit trail.
  • Cap max_iterations and watch for repeated identical screenshots, a sign the agent is stuck clicking the same spot.
  • Reserve computer use for targets with no DOM or API at all; use a browser agent for anything with a URL.

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

I have this computer-use agent (below) that enters invoices into a legacy desktop app and gates the Ctrl+S save behind human approval. Add a second gate that also requires approval before the agent clicks any button whose on-screen text (read from a zoom screenshot) contains the word 'Delete'. [paste complete_code]

Build AI Agents Prompt

In Claude Code, open invoice_entry_agent.py and ask: 'Add a dry-run mode that logs every action it would take without calling perform_action, so I can review a full invoice entry before letting it touch the real app.'

Frequently asked questions

What's the real difference between a computer-use agent and a browser agent?

A browser agent reads the DOM and accessibility tree inside a browser tab and acts on structured page content. A computer-use agent has none of that; it only sees a screenshot of the whole screen and clicks pixel coordinates, which is slower and more expensive per action but works against any application, including ones with no DOM or API at all.

Does computer_toolset_20260801 need a beta header?

No. It's a stable tool type as of late 2026, used the same way as any other tool definition in the Messages API, with no beta header required.

Why gate the save action specifically instead of every click?

Nearly every action before the save is harmless to redo: a misplaced click or a typo in a field costs you a retry, not a consequence. The save is the one action that commits the result, so it's the one worth a human looking at first.

Can I run a computer-use agent without a sandbox, just on my own laptop?

You can, but you shouldn't for anything beyond a quick manual test. The agent can click and type anywhere the virtual pointer and keyboard can reach, so a sandboxed, disposable environment is the difference between a mistake you can throw away and one you can't.

How much does a computer-use agent cost to run compared to a browser agent?

More, step for step. Each request adds roughly 4,500 input tokens of overhead for the toolset plus the tokens for the screenshot image itself, versus a browser agent reading much smaller structured text. Budget accordingly for any task that takes many steps.

Can someone build this kind of desktop-automation agent for me?

Yes. RapidDev builds production computer-use and browser agents, including sandboxing and approval gates like the one in this guide. A free 30-minute consultation is the fastest way to scope your specific legacy-app integration.

RapidDev

Talk to an Expert

Our team has built 1,000+ apps. Get personalized help with your project.

Book a free consultation
Matt Graham

Written by

Matt Graham · CEO & Founder, RapidDev

1,000+ client projects delivered. Columbia University & Harvard Business School alumnus, U.S. Navy veteran. About the author →

Learning is great. Shipping is faster with help.

Our engineers have built 1,000+ apps on your platform and the tools around it. If your project needs to be live sooner than your learning curve allows — book a free consultation.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.