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.
| Fact | Value |
|---|---|
| Tool | Claude computer use |
| Difficulty | Advanced |
| Time required | 60-90 min |
| Compatibility | Python 3.10+, anthropic SDK, a sandboxed Linux container or VM with a virtual display, Claude Opus or Sonnet 5.5 |
| Last updated | November 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
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.
1apt-get install -y xvfb x11-utils2Xvfb :99 -screen 0 1024x768x24 &3export DISPLAY=:994pip install anthropic pyautogui mss pillow5export 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.'
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.
1import mss, io, base642from PIL import Image34def 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.
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.
1import pyautogui23def 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 -117 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.
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.
1def requires_approval(name: str, input_data: dict) -> bool:2 return name == "key" and input_data.get("text", "").lower() == "ctrl+s"34def 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.
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.
1def process_tool_calls(response):2 results, failed = [], False3 for block in response.content:4 if block.type != "tool_use" or getattr(block, "toolset_name", None) != "computer":5 continue6 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.", True9 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.", True15 failed = True16 else:17 result["content"] = handle_action(block.name, block.input)18 results.append(result)19 return resultsExpected result: The agent fills in invoice fields with no prompts, then stops and prints [APPROVAL NEEDED] the moment it reaches for Ctrl+S.
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.
1import time23def 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.
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.
1MAX_ITERATIONS = 202# 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
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 base646import io7import time89import anthropic10import mss11import pyautogui12from PIL import Image1314MAX_ITERATIONS = 201516client = anthropic.Anthropic()171819def 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()262728def 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()))313233def 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 -147 pyautogui.scroll(sign * amt)48 return "OK"49 raise ValueError(f"Unhandled action: {name}")505152def 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)575859def requires_approval(name: str, input_data: dict) -> bool:60 return name == "key" and input_data.get("text", "").lower() == "ctrl+s"616263def process_tool_calls(response) -> list[dict]:64 results, failed = [], False65 for block in response.content:66 if block.type != "tool_use" or getattr(block, "toolset_name", None) != "computer":67 continue68 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.", True71 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.", True79 failed = True80 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}", True85 failed = True86 results.append(result)87 return results888990def 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 return104 messages.append({"role": "user", "content": tool_results})105 print(f"Stopped after {MAX_ITERATIONS} steps without completing.")106107108if __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 )113Common 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.
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]
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.
Talk to an Expert
Our team has built 1,000+ apps. Get personalized help with your project.
Book a free consultation