Observe-Act is VibeCody’s computer-use loop: it takes a screenshot of your desktop, asks a vision model what to do next, performs the actions on your real mouse and keyboard, checks whether the screen changed the way the model said it would, and repeats. It is the same pattern as Anthropic Computer Use and OpenClaw, driven by whichever provider and model you select — not by a fixed vendor.
It drives your actual machine. There is no sandbox here. Read Safety modes before running one autonomously, and start in Restricted, which observes and records what it would do without touching anything.
Where it lives
VibeCoder → System Monitor → Observe-Act, with four tabs:
| Tab | What it does |
|---|---|
| Setup | Preflight status, task, vision model, safety mode, step and interval budgets, Start / Pause / Stop |
| Monitor | Live status, the latest screenshot, the model’s current reasoning, and an event log |
| History | Every step: reasoning, actions run, actions not run, and the verification verdict |
| Safety | The rails — action budget, rate limit, forbidden key combos, forbidden screen regions |
Everything the panel does goes to the VibeCLI daemon over /observe/*. The
daemon owns the session, because there is one screen per machine: a registry
per shell would give three shells three different answers to “is a session
running”.
Prerequisites
The loop shells out to the platform’s automation tools. Setup runs a preflight and names anything missing, with a distinct message per cause — a missing binary and a denied permission need different fixes.
| Platform | Tools | Also required |
|---|---|---|
| macOS | osascript, screencapture, cliclick (brew install cliclick) |
Screen Recording and Accessibility permission for the process running the daemon |
| Linux (X11) | xdotool, wmctrl, scrot, xdpyinfo |
— |
| Windows | powershell |
— |
On macOS, screencapture exits 0 when Screen Recording is denied and
writes no file. The loop checks that the file exists rather than trusting the
exit code, and says which permission is missing — the one failure mode that
otherwise looks exactly like success.
Safety modes
| Mode | What runs |
|---|---|
| Restricted | Nothing. The model observes and proposes; every proposal is recorded and shown struck through in History. This is the mode to watch a task in before letting it act. |
| Cautious (default) | Everything except destructive actions, which stop and ask. Unanswered for five minutes, the request is treated as a refusal — an operator who walked away has not approved anything. |
| Autonomous | Everything, without asking. Stop is the only gate. |
“Destructive” is classified in observe_act::is_destructive: key combos
containing delete/backspace, ctrl+w/q/x, alt+f4; typed text containing
rm , del , sudo , shutdown, reboot; and any drag.
The rails
Configured on the Safety tab, enforced in the daemon before any action is performed:
- Max actions per step — an over-long batch is refused whole, not truncated to the limit. Ceiling of 20 whatever the request asks for.
- Rate limit — minimum milliseconds between consecutive actions.
- Max consecutive failures — the session fails rather than burning its whole step budget.
- Forbidden key combos — never issued.
alt+f4andctrl+alt+delby default. - Forbidden screen regions — rectangles no click, drag or mouse move may target, in the display’s own units.
- Hard caps that are not configurable: a
waitis clamped to 30 s, atypeto 4096 characters, ascrollto 50 increments. Each of these would otherwise let one model turn park the session with nothing on screen saying why.
Coordinate spaces
Three of them, and conflating any two puts the click somewhere the model never looked:
| Space | What it is |
|---|---|
| Image | The pixels of the screenshot actually sent to the model, downscaled to fit the vision API’s limits. The model answers in this space, because it is the only one it can see. |
| Capture | The screenshot file’s own pixels. On a Retina display screencapture writes the backing store, so this is 2× the logical size. |
| Logical | The points cliclick and xdotool take. Where an action must land. |
The daemon measures image width and logical width and divides. The backing
scale factor is never guessed — the usual source for it (system_profiler
printing the word “Retina”) is absent on a scaled resolution and wrong on a
second display, and a factor-of-two error there puts every click in the wrong
quadrant.
Coordinates are mapped before the safety rails run, so a forbidden region is checked against the pixel the click will actually reach.
Verification
When Verify after action is on, each step costs a second screenshot and a second model call: the model states in advance what the screen should look like, and is then shown the result and asked whether it matches.
A step has three verification states, not two:
| State | Meaning |
|---|---|
| Verified | Checked, and it matched. |
| Failed | Checked, and it did not. Counts toward the consecutive-failure limit. |
| Unverified | Not checked — verification is off, the model gave no expected change, the verification screenshot failed, or its verdict could not be parsed. |
Unverified is not a failure. It leaves the failure streak exactly where it
was, and it is excluded from the verified rate — a run with verification off
reads n/a, never 0%.
The model
The vision model is chosen in the panel, seeded from the toolbar’s provider.
Both provider and model are required on the API; there is no fallback
to the daemon’s boot provider, because a loop that silently used another vendor
would be sending screenshots of your desktop to a service you did not pick.
The daemon reports whether the provider advertises vision support but does
not enforce it: AIProvider::supports_vision defaults to false and several
providers that do accept images never override it, so refusing on it would
reject working configurations. If every step comes back blind, that warning is
why.
Screenshots are downscaled to the configured cap (1280×720 by default) and re-encoded as JPEG at quality 80. A raw Retina PNG is roughly 12 MB base64-encoded — past Anthropic’s 5 MB per-image limit before the request is even built.
HTTP API
All authed (Authorization: Bearer <token>); the SSE stream also accepts
?token= because EventSource cannot set a header.
| Route | Purpose |
|---|---|
GET /observe/preflight |
Platform, missing tools, logical screen size, ready |
GET / PUT /observe/config |
The saved loop + safety configuration |
GET /observe/sessions |
Every session, newest first, without steps |
POST /observe/sessions |
Start one — { task, provider, model, config?, safety? } |
GET /observe/sessions/{id} |
The full record, steps included |
POST /observe/sessions/{id}/pause |
Pause after the current step |
POST /observe/sessions/{id}/resume |
Resume |
POST /observe/sessions/{id}/abort |
Stop at the next action boundary |
POST /observe/sessions/{id}/approve |
{ approval_id, approve } |
GET /observe/sessions/{id}/events |
SSE — snapshot, then one step per loop event |
GET /observe/sessions/{id}/screenshot |
The most recent capture, as PNG |
Starting a session while one is still running returns 409. Two loops moving the same mouse would each verify against the other’s half-finished work.
curl -sX POST localhost:7878/observe/sessions \
-H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"task":"open the Downloads folder","provider":"claude","model":"claude-sonnet-4-6"}'
Where the record lives
~/.vibecli/observe_act/<session-id>/ — one PNG per step (plus a
-verify.png where verification ran) and a session.json written after every
step.
A session that was running when the daemon stopped reloads as aborted. The process driving it is gone, so it is not running, and showing it as running would leave a phantom in the history that no stop button could clear.
Limits
- One screen. Multi-display setups are captured and driven as the primary display reports itself; a second monitor is outside the coordinate space.
- No accessibility tree. Everything is coordinate-based, from pixels. There is no element lookup, so a model that misreads a small label clicks the wrong thing — which is what Restricted mode and verification are for.
- X11 only on Linux.
xdotooldoes not drive a Wayland compositor. - No CLI surface yet. The loop is reachable from the panel and the HTTP
API;
vibeclihas no/observesubcommand. - VibeCoder only, deliberately. The loop drives the machine the daemon runs
on, so a phone or a watch could only ever watch and stop one — worth having,
not built. VibeDesk and VibeAIChat do not carry the panel either. The
/observe/*routes are open to any authenticated client that wants to.