Ghosthand skill
Operate the Windows desktop the way a human does — eyes (screenshots) + hands
(mouse/keyboard input via user32.dll).
When to use
Invoke this skill when the user wants the agent to control the desktop:
- "open ", "click on …", "type … into "
- automating a flow that has no API (taskbar, file explorer, third-party apps)
- demos of GUI automation
Don't use it for tasks that are easier via direct shell commands (e.g. running
notepad.exe via Start-Process is fine; you only need this skill when the
user wants the path-via-clicks behavior).
Files
Ghosthand.ps1— public functions (see header inside the file)watcher.ps1— background screenshot daemon
Always source the module before use:
. "$HOME\.copilot\skills\ghosthand\Ghosthand.ps1"
Core principles (READ BEFORE EACH TASK)
1. Behave like a human, not a script
- Mouse: use
Click-MouseHuman/Move-MouseHumanby default. They produce a curved, eased path with pixel jitter — the user sees the cursor travel. PlainClick-Mouseteleports and looks robotic; reserve it only for invisible/off-screen prep. - Typing: use
Send-Text(the default). It paces at ~70 WPM, varies per-key timing, occasionally hits adjacent QWERTY keys and corrects with backspace. UseSend-Keysonly for special keys ({ENTER},{TAB}, etc.) andSend-Rawonly when you specifically need an instant paste-like dump (rare). - Tune knobs only when needed:
-WpmAvg,-TypoProb,-PauseProb. Defaults already feel natural — don't crankTypoProbabove ~0.05.
2. Batch actions, verify at milestones
Do not do action → screenshot → view → action → screenshot → view. That's
slow, robotic, and noisy. Instead:
- Plan the next 2-6 actions based on the current screenshot.
- Execute them in a single PowerShell call using the helpers above.
- Only screenshot + view at meaningful milestones (e.g., after the app finishes opening, after a save dialog appears).
Example — opening Notepad and typing a sentence is one action batch, followed by one verification screenshot.
3. See-while-acting via the watcher
For long action sequences, start the screen watcher once at the beginning:
. "$HOME\.copilot\skills\ghosthand\Ghosthand.ps1"
Start-ScreenWatcher # writes state\current.png every ~250ms
Then issue actions in PowerShell calls and, in the same tool-call turn,
view the file ~/.copilot/skills/ghosthand/state/current.png. The image
the agent sees is at most ~250ms stale — effectively concurrent vision +
action. No manual Capture-Screen step needed.
Stop the watcher when done: Stop-ScreenWatcher.
4. Parallelism guidance for the agent
- Issue PowerShell action sequences and
view current.pngin parallel (in the same tool-call response) whenever both are independent. - Do NOT serialize a screenshot capture, then view, then next action — let the watcher handle capture in the background.
- Remember: actions take real human-paced time (typing 30 chars ≈ 5s). During that time, no extra agent work is needed; just await completion.
5. Coordinate hygiene
- The module sets per-monitor DPI awareness, so screenshot pixel coords
match
SetCursorPoscoords. However the rendered screenshot you see may be displayed at a different scale by the viewer — do NOT eyeball pixel coords from a rendered screenshot. Instead, use the UIA locator (Find-UIElement/Click-UIElement) to get exact screen-space rectangles by name. Reserve manual coordinates for cases where UIA cannot enumerate the target. - Always re-locate after a window has been moved/closed/resized; never reuse stale coordinates across long pauses.
- Multi-monitor: only the primary screen is captured by default.
6. Things that block screen capture
Windows blocks screenshots on the secure desktop. If the screen looks black or you see the lock screen, abort gracefully and tell the user.
Behavioral realism
Send-Text uses bigram-aware log-normal keystroke timings by default
(common bigrams "th","he","in" run fast; same-finger / shift / digit /
punctuation slower). Pass -NoBigramTiming to revert to plain log-normal
jitter.
Move-MouseHuman adds 8–13 Hz sinusoidal tremor along the path
(matches real-muscle frequency) and a 50–150 ms sub-pixel correction
phase at the end. Disable individually with -NoTremor /
-NoSubpixelCorrection.
Click-MouseHuman performs a -PreHoverMs micro-jitter dwell at the
destination before mousedown (default ~80–220 ms log-normal). Hesitate
and hold timings are also log-normal now.
Scroll-MouseHuman scrolls in 1–3 bursts with reading pauses between and
an ~18 % chance of a small reverse "re-read" backtrack. Disable with
-NoBacktrack.
Add-ReadingDelay -Text "..." pauses by content length / ~240 wpm
reading speed (with log-normal jitter). Call after a screen change before
acting.
Internal helpers exposed for scripts:
_LogNormalMs -MedianMs -Sigma -MinMs (Box-Muller sampler) and
_TypingDelayMs -Prev -Curr -BaseMs (bigram-aware delay).
Realism techniques applied
| Concern | Technique |
|---|---|
| Mouse trajectory shape (Bézier vs straight line) | Tremor + overshoot + sub-pixel correction |
| Click-position-too-perfect-center | -PaddingPercent (random in bbox) |
| Inter-keystroke timing variance | Bigram + log-normal cadence |
| Reading-delay-after-page-change | Add-ReadingDelay |
| Linear scroll patterns | Bursty scroll w/ backtrack |
event.isTrusted check |
OS-level injection sets this true |
navigator.webdriver flag |
N/A — no WebDriver / CDP attachment |
What is out of scope
- Network-layer signals (TLS / HTTP-2 fingerprint, IP reputation, account history) — not addressable from an OS-level input layer
- CAPTCHAs — designed for this; the skill detects and surfaces them but never attempts to solve
- Kernel-level anti-cheat — synthetic input sets a flag visible to kernel drivers. Don't use this skill for games.
- Enterprise device-fingerprint stacks with persistent IDs across sessions
Speed: agent execution patterns (READ THIS — biggest realism win)
The fundamental cost is not action execution; it is the LLM "think" time between agent tool calls (~10–20 s per call). Optimizing for speed therefore means minimizing the number of agent decision points, not spawning more agents.
Why fewer agents is faster (counterintuitive but real)
It's tempting to "parallelise" by spawning a Brain agent that plans and a Doer subagent that executes, with the two coordinating via a SQL queue. Empirically this is slower, not faster:
| Measurement | Result |
|---|---|
| Bundle's own PS execution | 2.0 s |
| Total time from queue → done (bundle 1) | 55 s |
| Tool calls the Doer makes per bundle | 3 (sql + ps + sql) |
| LLM-thinking overhead per bundle | ~53 s |
Spawning a subagent multiplies the per-action LLM tax rather than amortising it. Two agents thinking sequentially is worse than one. The "Brain plans while Doer executes" overlap exists but is swamped by per-decision LLM latency on both sides.
What is actually parallel and does help
| Mechanism | Real parallelism? | When to use |
|---|---|---|
Start-ScreenWatcher (separate OS process) |
✅ yes | Always — vision is free |
Invoke-ActionBatch in a single PS call |
✅ yes | Any flow with ≥3 UI actions |
| Whole-flow PowerShell script in one tool call | ✅ yes | Multi-step deterministic flows |
| Multiple agents (Eyes/Brain/Doer/Hands) | ❌ no — adds LLM tax | Almost never |
Recommended patterns (in order of preference)
1. Whole-flow script (BEST for known flows)
For a multi-step task whose shape is known up-front, write the entire flow as one PowerShell script and run it in one tool call:
. "$HOME\.copilot\skills\ghosthand\Ghosthand.ps1"
Start-ScreenWatcher
# everything below is one agent decision
$br = Open-Url 'https://example.com/dashboard'
$null = Wait-ForUrlContains -Pattern '/login' -ProcessId $br.ProcessId
Click-UIElement -NameLike 'Continue with Google' -PaddingPercent 50 | Out-Null
$null = Wait-ForUrlContains -Pattern 'accounts.google.com' -ProcessId $br.ProcessId
Click-UIElement -NameLike 'your.email@example.com' | Out-Null
$null = Wait-ForUrlContains -Pattern '/home/overview' -ProcessId $br.ProcessId -TimeoutMs 15000
Click-UIElement -Name 'project-name' -Kind Hyperlink | Out-Null
# ... etc, all in one shell call
Stop-ScreenWatcher
15 actions → 1 LLM decision → ~30–45 s wall clock (mostly real human-paced input timing). This is the canonical fast path.
2. Invoke-ActionBatch for ref-driven flows
When you have a UIA snapshot with refs, drive the whole flow through
Invoke-ActionBatch with wait / snapshot actions interleaved:
Get-UIASnapshot -ProcessId $calc.ProcessId | Out-Null
Invoke-ActionBatch -Actions @(
@{type='invoke'; ref='w1e25'},
@{type='wait'; ms=200},
@{type='invoke'; ref='w1e45'},
@{type='invoke'; ref='w1e33'},
@{type='invoke'; ref='w1e46'},
@{type='invoke'; ref='w1e36'},
@{type='wait'; ms=300},
@{type='snapshot'; pid=$calc.ProcessId}
)
3. Subagent for LONG flows (only when context-clearing matters)
If a task is long enough that the conversation context becomes the
bottleneck (≥30 actions, lots of intermediate output), delegate the
entire flow to a general-purpose subagent with the whole-flow script
written by the main agent already inside the prompt. The subagent's
job is to run the script, capture its output, and return a summary —
not to make per-action decisions.
4. The multi-agent SQL queue (DO NOT use for speed)
The infrastructure exists (pc_action_queue, pc_scene_state,
pc_plan_state tables; Invoke-ActionBundle, New-ActionBundle,
Get-SceneDigest helpers) and works correctly. It's useful for
fail-tolerant multi-agent flows (e.g., a long-running background
"PC butler" that resumes from queue state after a crash), but it is
not faster than option 1. The architectural design is documented
here for completeness; do not reach for it as a latency optimization.
Concrete latency budget (revised, honest)
| Pattern | Wall clock for 15-step CF task |
|---|---|
| Sequential main-agent loop, screenshot every step | ~5–8 min |
| Multi-agent (Brain+Doer) with per-bundle agent decisions | ~10+ min (worse) |
Single agent, Invoke-ActionBatch of refs |
~60–90 s |
| Single agent, whole flow as one PS script (BEST) | ~30–45 s |
The speed limit is set by physical typing time + a single LLM-decision overhead. Anything more than ~1 LLM decision per "block of work" is wasted budget.
Function reference
Vision
Capture-Screen [-Path] # save PNG and return its path
Start-ScreenWatcher [-IntervalMs] # background continuous capture
Stop-ScreenWatcher # stop the daemon
Mouse — instant (use sparingly)
Move-Mouse X Y
Click-Mouse X Y [-Button left|right] [-Times N]
Mouse — humanized (DEFAULT)
Get-MousePos # {X,Y}
Move-MouseHuman X Y # eased Bezier travel
Click-MouseHuman X Y | -Element $obj
[-Button] [-Times] [-Rapid]
# -Element : prefer UIA ClickablePoint, fall back to
# randomised point in element's bounding rect
DoubleClick-MouseHuman X Y [-Button]
RightClick-MouseHuman X Y
Hover-MouseHuman X Y [-DurationMs]
Drag-MouseHuman -FromX -FromY -ToX -ToY [-HoldMs] # try/finally — release guaranteed
Scroll-MouseHuman [-X -Y] -Notches N [-Horizontal] # +ve = up/right
Keyboard — text
Send-Text "string" # humanized typing w/ typos (DEFAULT)
Send-Keys "{ENTER}" # SendKeys passthrough for special keys
Send-Raw "string" # paste-like, no humanization (rare)
Keyboard — chords & raw keys
Send-Hotkey "ctrl+shift+s" # OR -Keys @('Ctrl','Shift','S')
# OR -Keys 'ctrl+Home' (combo strings flattened)
# supports ctrl/shift/alt/win, letters, digits,
# F1-F12, arrows, home/end/pageup/pagedown,
# tab/enter/esc/space/backspace/delete/insert/apps
Press-VK 0xNN # press a virtual-key code
Press-Win # tap Windows key
Hold-Key 0xNN # press down (no auto-release)
Release-Key 0xNN # release a held key
Common in-app shortcuts (one-liners)
Copy / Paste / Cut / Select-All / Save-File / Undo / Redo
Find-In-App / Print-File / New-Tab / Close-Tab
System / window shortcuts
Alt-Tab / Alt-F4 / Show-Desktop / Open-Explorer / Task-View
(Win+L is intentionally NOT exposed — it would lock the user out.)
Safety
Reset-InputState # release all modifiers + mouse buttons
# call this if a chord/drag was interrupted
Window management (operate on HWND from Find-Window)
Get-Windows [-IncludeUntitled] # all visible top-level windows
Find-Window -Title "*..." [-ClassName ...] [-ProcessName ...] [-ProcessId N] [-Exact] [-Index]
Find-Windows ... # plural, returns all matches
Wait-ForWindow ... # poll Find-Window until found / timeout
Focus-Window -Handle # returns {Success, Reason} — best-effort
Wait-ForForegroundWindow -Handle # confirm focus actually landed
Minimize-Window / Maximize-Window / Restore-Window -Handle
Close-Window -Handle # polite WM_CLOSE — app may prompt to save
Move-Window -Handle -X -Y
Resize-Window -Handle -Width -Height
UIA — find/inspect/wait on UI elements
Find-UIElement -Name "..." [-NameLike] [-NameRegex] [-NameAny @("a","b")]
[-ClassName] [-AutomationId] [-ProcessId]
[-Kind Button|MenuItem|Edit|Window|CheckBox|RadioButton|ComboBox|
Hyperlink|TabItem|TreeItem|ListItem|Text|Any]
[-Index N] [-TimeoutMs N]
[-AutoScroll Top|Bottom|Both]
# returns {Name, AutomationId, ClassName, ControlType,
# ProcessId, NativeHandle,
# X, Y, Left, Top, Width, Height,
# HasClickablePoint, Element}
# NameLike = glob substring (greedy)
# NameRegex = case-insensitive regex (stricter)
# NameAny = exact match against any of the supplied names
# AutoScroll Top : send Home once, then rescan
# AutoScroll Bottom: send End once, then rescan
# AutoScroll Both : Home -> End -> Home (only when needed)
# X/Y prefer the UIA ClickablePoint when available
Find-UIElements ... # plural — same matchers
Click-UIElement -Name "..." | -Element $obj | -Ref "w1e5"
[-Button] [-Times] [-Instant]
[-PaddingPercent N] # 0=any pt in bbox, 100=exact center, default 60
[-HesitateMs N] [-HoldMs N]
[-EnsureVisible] # scroll into screen viewport first
Invoke-UIElement -Name "..." | -Ref "..." | -Element $obj [-NoFallback]
# programmatic UIA Invoke/Toggle/Select
# works on hidden/unfocused windows
Wait-ForUIElement ... [-TimeoutMs N] [-AutoScroll Top|Bottom|Both]
[-OnPoll {…}]
Wait-ForUIElementGone ... [-OnPoll {…}]
# OnPoll runs each iteration. Use sparingly:
# most modern web UIs push updates without
# reload, so passive polling (no -OnPoll)
# is normally what you want.
Wait-PageInteractive [-ProcessId N] [-TimeoutMs 4000]
[-Kind Hyperlink|Button|Edit|Text|Any]
[-MinElements 1]
# use after Focus-Window / tab switches
# before querying the UIA tree, instead of
# arbitrary Start-Sleep waits.
Get-UIElementText -Element $el # tries ValuePattern, TextPattern, then Name
Set-UIElementText -Element $el -Text "..." [-NoFallback]
# ValuePattern w/ typing fallback (Ctrl+A → Send-Text)
UIA snapshot tree (RECOMMENDED for agent use)
Get-UIASnapshot [-Title "..."] [-ProcessId N] [-Handle $h] [-MaxDepth 8]
# returns indented text tree with refs
# like "- button \"Equals\" (id=...) [ref=w1e36]"
# refs are valid until next snapshot call
Get-UIElementByRef -Ref "w1e36" # resolve ref to element object
Batch actions
Invoke-ActionBatch -Actions @(
@{type='click'; ref='w1e36'},
@{type='invoke'; ref='w1e36'}, # UIA Invoke (no physical click)
@{type='doubleclick'; name='File'},
@{type='rightclick'; x=100; y=200},
@{type='type'; text='hello'; wpm=70},
@{type='hotkey'; combo='ctrl+s'},
@{type='key'; keys='{ENTER}'},
@{type='wait'; ms=200},
@{type='wait'; name='Save dialog'; ms=5000},
@{type='snapshot'; pid=1234},
@{type='move'; ref='w1e36'},
@{type='scroll'; notches=-3}
)
# returns @[{Action, Status, Result}, ...]
Idle behavior
Start-IdleMouseMoves [-DurationMs N] # random small moves to look "alive"
Reading delay (post-screen-change pause)
Add-ReadingDelay [-Text "..."] [-WpmReading 240] [-MinMs 350] [-MaxMs 4000]
Realism helpers
_LogNormalMs -MedianMs N [-Sigma 0.3] [-MinMs 1] # Box-Muller log-normal
_TypingDelayMs -Prev <char> -Curr <char> -BaseMs N # bigram-aware delay
Multi-agent coordination (see "Speed" section — DO NOT use for latency,
only for fail-tolerant flows)
New-ActionBundle -Description "..." -Ps "..."
[-Preconditions "..."] [-Postconditions "..."] [-PostTimeoutMs N]
# build a JSON bundle for the queue
Invoke-ActionBundle -BundleJson "..." # run a bundle: precond → ps → postcond
# returns {ok,error,result,precond,postcond}
Get-SceneDigest # quick JSON: hash, active window, url
# use as a change-detector for any
# downstream perception consumer
Browser layer (for AI-assistant browsing — drives a real Chrome/Edge/Firefox
window via OS keyboard, NOT WebDriver/CDP, so navigator.webdriver stays false)
Get-BrowserWindow # current Chrome/Edge/Firefox/Brave window
Wait-ForBrowserWindow [-TimeoutMs N]
Open-Url -Url "https://..." [-NewTab] [-NewWindow] [-WaitLoadMs N]
Wait-PageLoad [-Handle $h] [-TimeoutMs N] [-StableMs 900]
Wait-ForUrlContains -Pattern "/path" [-ProcessId N] [-TimeoutMs N]
# waits for address bar to contain pattern
Get-PageText [-ProcessId N] [-MaxChars 12000] # UIA text extraction
Click-LinkByText -Text "..." [-Partial] # exact (or glob with -Partial)
Click-LinkByText -Regex "..." # case-insensitive regex
Click-LinkByText -NameAny @("A","B") # exact match against any
[-Invoke] [-ProcessId N]
Fill-FieldByName -Name "Search" -Text "..." [-Partial] [-Clear] [-ProcessId N]
Find-CaptchaPresentV2 [-ProcessId N] # refined detector (preferred)
Find-CaptchaPresent [-ProcessId N] # legacy, may false-positive on brand names
Find-DropdownByLabel -Label "Destination" [-ProcessId N] [-MaxYDistance 200]
# for React-Select-style custom dropdowns
# not exposed as UIA ComboBox
Get-BrowserUrl [-ProcessId N] # read current address bar
Get-BrowserTabs [-ProcessId N] # list tabs ({Index,Title,IsActive,X,Y,Width,Element})
Switch-BrowserTab -Index N | -TitleLike "..." # mouse-click on the tab strip (DEFAULT)
[-UseHotkey] # opt into Ctrl+1..8 / Ctrl+9 instead
Close-BrowserTab [-Index N | -TitleLike "..." | -Current]
Realism / timing
Get-HumanDelay -MedianMs N [-Sigma 0.3] [-MinMs 1] # log-normal sample (no sleep)
Add-ReadingDelay [-Text "..."] [-WpmReading 240] # sleeps for a "reading" pause
Clipboard (PowerShell builtins)
Get-Clipboard
Set-Clipboard -Value "..."
Pitfalls and caveats
Don't reload pages that auto-update. Most modern web UIs (CI status, PR checks, dashboards, chat) push state changes via long-poll or WebSocket; firing F5 from
Wait-ForUIElement -OnPollthrashes the page, resets scroll, and is visible to the user. Use passive polling (no-OnPoll) for these, and reserve-OnPollfor cases where the caller genuinely needs to drive a control between checks.Don't
Send-Hotkey 'End'reflexively to find an element. That jumps to the bottom of the document and is jarring. Prefer-AutoScroll Top(the typical case — header chrome lives at the top) and only-AutoScroll Bottomwhen you specifically need a footer- anchored element.-AutoScroll Bothshould be a last resort.Snapshot refs are tied to a single
Get-UIASnapshotcall. Each snapshot resets the ref counter and rebuilds the map. If you take a fresh snapshot, all old refs are invalidated; re-read the new tree to get the new refs. Don't cache refs across snapshots.Invoke-UIElementis more reliable thanClick-UIElementfor UIA-friendly buttons — it doesn't require the window to be focused or visible, and ignores cursor obstructions. Prefer it for deterministic workflows (button presses, menu items). Use real clicks only when you actually want to mimic a human, or when the control doesn't expose InvokePattern.Don't eyeball pixel coords from a rendered screenshot. The viewer rescales the image. Use
Get-UIASnapshot+-Ref, orFind-UIElement/Find-Windowfor exact screen-space coordinates.Scroll target depends on OS setting "Scroll inactive windows when I hover over them". If results feel wrong, focus the window first via
Focus-Window.Focus-Windowis best-effort. Foreground locks, elevation boundaries, virtual desktops, and minimized targets can prevent it. Always check.Successand useWait-ForForegroundWindowto confirm.Click-UIElement -Name "OK"is ambiguous when multiple "OK" buttons exist. Add-AutomationId,-ProcessId, or-Index.Stuck input recovery. If a script aborts mid-chord/drag, the modifier or mouse button can stay logically down. Run
Reset-InputStateto clear it.Mixed-DPI multi-monitor is not extensively tested. The watcher captures the primary monitor only; coords are physical pixels under per-monitor-V2 awareness.
Safety boundaries
- Never type passwords, OTPs, or other secrets into focused windows on behalf of the user without a clear, explicit instruction.
- Don't screenshot or read text from password fields. Skip them.
- UAC / secure desktop / lock screen are invisible to capture and uncontrollable via injected input. If a screenshot looks black or the user is at the lock screen, abort and tell the user.
- Win+L is deliberately not implemented (it would lock the user out of their own machine and end the session abruptly).
Close-WindowusesWM_CLOSE(graceful). Do not switch toTerminateProcessto force-close — apps would lose unsaved work.
Recipes
Open an app via the Start menu (mouse-driven, locator-based)
. "$HOME\.copilot\skills\ghosthand\Ghosthand.ps1"
Click-UIElement -Name "Search" # taskbar Search box (UIA-located, no guess)
Start-Sleep -Milliseconds 700
Send-Text "notepad"
Start-Sleep -Milliseconds 600
Send-Keys "{ENTER}"
Open Start via Win key (faster, less mouse movement)
Press-Win
Start-Sleep -Milliseconds 700
Send-Text "calc"
Send-Keys "{ENTER}"
Type a paragraph into a focused editor
Click-MouseHuman -X 800 -Y 400 # focus the editor area
Send-Text "Dear team, please review the attached design document."
Send-Keys "{ENTER}{ENTER}"
Send-Text "I'd appreciate feedback by Friday. Thanks!"
Right-click + click a context menu item (with watcher)
Start-ScreenWatcher
Click-MouseHuman -X 1200 -Y 500 -Button right
# ... in the SAME tool-call turn, agent reads state\current.png and finds
# "Properties" item, then issues:
Click-MouseHuman -X 1245 -Y 612
Stop-ScreenWatcher
Snapshot + batch (preferred for deterministic workflows)
$calc = Find-Window -Title 'Calculator'
Get-UIASnapshot -ProcessId $calc.ProcessId | Set-Content "$env:TEMP\snap.txt"
# (agent reads snap.txt, picks refs)
Invoke-ActionBatch -Actions @(
@{type='invoke'; ref='w1e25'}, # Clear
@{type='invoke'; ref='w1e45'}, # Seven
@{type='invoke'; ref='w1e33'}, # Multiply by
@{type='invoke'; ref='w1e46'}, # Eight
@{type='invoke'; ref='w1e36'}, # Equals
@{type='wait'; ms=200}
)
$display = Get-UIElementByRef -Ref 'w1e13'
Get-UIElementText -Element $display # -> "Display is 56"
Idle "alive" behavior between actions
Click-UIElement -Ref 'w1e35' # Plus
Start-IdleMouseMoves -DurationMs 1500 # mouse drifts naturally
Click-UIElement -Ref 'w1e36' # Equals
Browse the web (AI-assistant recipe)
. "$HOME\.copilot\skills\ghosthand\Ghosthand.ps1"
# 1. Navigate. Open-Url focuses an existing browser if any, else launches default.
$br = Open-Url 'https://duckduckgo.com'
# 2. Always check for CAPTCHAs / "checking your browser" before acting
$cap = Find-CaptchaPresent -ProcessId $br.ProcessId
if ($cap.Detected) {
Write-Warning "Challenge detected: $($cap.Sign). Pausing for human."
return
}
# 3. Reading-delay so the page-load -> first-action timing looks human
Add-ReadingDelay -Text 'Privacy, simplified.'
# 4. Find the search input (semantic, not pixel-based)
Fill-FieldByName -Name 'Search privately' -Text 'climate change effects' -ProcessId $br.ProcessId | Out-Null
Send-Keys '{ENTER}'
# 5. Wait for results, then read the page
Wait-PageLoad -Handle $br.Handle -TimeoutMs 12000 | Out-Null
Add-ReadingDelay -MinMs 1200
$pageText = Get-PageText -ProcessId $br.ProcessId -MaxChars 4000
# 6. Click a specific result link by visible text
Click-LinkByText -Text 'NASA' -Partial -ProcessId $br.ProcessId
Wait-PageLoad -Handle $br.Handle | Out-Null
Add-ReadingDelay -Text $pageText.Substring(0, 800)
# 7. Scroll to read more (bursty + reading pauses + chance of backtrack)
Scroll-MouseHuman -Notches -8
# 8. New tab + another search
$br2 = Open-Url 'https://wikipedia.org' -NewTab
Key principles for the browse loop
- Always
Find-CaptchaPresentbefore each new action. If detected, pause and surface to the user — the skill will not auto-solve them. Open-Urltypes the URL via Ctrl+L with bigram-aware rhythm, rather than instantly navigating via shell. This is what real users do.- Use
Get-PageTextinstead of OCR for normal sites. Chrome/Edge expose page text via UIA. Chrome auto-enables accessibility on the first UIA query (one-time ~500ms delay). Add-ReadingDelaybetween every page-change and action. Real users need 1-3s to perceive new content before clicking.- Drive a normal Chrome/Edge/Firefox — NOT Selenium / Playwright /
Puppeteer. Those tools set
navigator.webdriver=true, which is trivially detectable. OS-level injection into a normal browser leavesnavigator.webdriverfalse. Click-LinkByText -Partialfinds links by visible text (not pixel position) — robust to layout shifts and DPI changes.- Form fields: prefer
Fill-FieldByName(UIA accessible name likearia-labelorplaceholder) over coordinate clicks.
Cleanup
Always Stop-ScreenWatcher when done with a task that started one — the
daemon is a detached process and survives this session.