GCU Browser Automation

SkillWeb & browsing

This skill lets your AI control a real Chrome browser to click, type, and take screenshots on web pages. It works from the terminal and attaches to the Chrome you already have running instead of opening a new browser. Once added, your AI also learns the rules for working with your browser safely.

Available today. Use it from your connected AI after setup.

After adding the skill, open Chrome and ask your AI to run a hive-browser command from the terminal. Keep in mind this skill must be added before any hive-browser command will work.

Then ask your AI: use the GCU Browser Automation skill

What your AI can do with it

  • Click buttons and links on web pages
  • Type into forms and fields
  • Take screenshots of web pages
  • Drive Chrome by running commands from the terminal
  • Attach to the Chrome you already have open without closing or restarting it

What this skill tells your AI

The instructions your AI receives, as published by aden-hive/hive in core/framework/skills/_default_skills/browser-automation/SKILL.md and read by ahel’s review.

All GCU browser automation drives a real Chrome instance through the Beeline extension and Chrome DevTools Protocol (CDP). You drive it from the terminal: every command is hive-browser <command> ... --json, run through terminal_exec. Always pass --json so the result is machine-readable. That means clicks, keystrokes, and screenshots are processed by the actual browser's native hit testing, focus, and layout engines — not a synthetic event layer. Understanding this unlocks strategies that make hard sites easy.

Browser lifecycle & recovery — read this before "fixing" anything

The bridge attaches to the user's already-running Chrome via the extension. The browser is not yours: it holds the user's logged-in sessions and other agents' work, and the runtime — not you — owns the connection to it. Consequences:

  • There is nothing to "start." If no browser is connected, hive-browser setup --json tells you and gives the user install steps. Launching Chrome yourself (any google-chrome/chromium command, --remote-debugging-port, --user-data-dir, headless flags) is forbidden and blocked — it opens a browser the bridge can't see, often under the wrong profile.
  • There is no situation where killing the browser helps. Never use terminal tools to kill/pkill/killall Chrome, the bridge, or gcu processes. These commands are blocked, and attempting them breaks every agent sharing the connection. The same applies to the Hive desktop app and bridge_host.
  • A timeout is NOT a stuck browser. All hive-browser commands share one transport across all agents; one slow call (a heavy hive-browser evaluate on a big page) can make YOUR calls time out while the browser is perfectly healthy. The timeout message tells you whether the server passed its liveness check and whether recovery is already running — believe it.
  • Allowed recovery, in order: (1) wait ~30s and retry ONCE, with a smaller/simpler request — e.g. split a page-wide hive-browser evaluate sweep into chunked queries; (2) close YOUR OWN tabs with hive-browser tab close <T> --json and reopen; (3) report the failure (report_to_parent or your reporting channel) and move on to work that doesn't need the browser. Escalate to the user; never to the process table.
  • Keep heavy hive-browser evaluate scripts cheap: avoid innerText over thousands of nodes (each read forces layout). Prefer textContent, scope the selector, and paginate the sweep.

Targeting a specific Chrome profile / account

If your machine has more than one Chrome profile connected (different logged-in accounts), say which one to act in — if you omit it, the bridge falls back to the first-connected profile, which may be the wrong account. Pass --browser-profile <label> to hive-browser open (and hive-browser navigate / hive-browser script):

  • See every connected profile in hive-browser status --json / hive-browser setup --json — both return a connected_profiles list of {label, is_default, starred} for ALL connected Chrome profiles (not just the one you're using). That's how you discover the labels. (hive-browser status reflects ALL connections; don't conclude "only one profile" from the single profile label of your own context.)
  • The label is a connected profile label — the name shown in that profile's Hive extension side panel (or its auto 3-word id). If your task assigns you a profile (e.g. "your profile is acct-jpn"), pass exactly that.
  • Every command's JSON echoes the profile it actually used. Check it: if it doesn't match what you intended, you opened the wrong account. Stop and fix the label (don't proceed).
  • A label that isn't connected fails fast with the list of connected labels — bind to one of those.
  • With one profile connected (or one starred default), you can omit it. With several connected and no star, omitting it uses the first-connected profile — fine for single-account work, risky for multi-account, so pass the label when the account matters.

Working on LinkedIn?

For ANY LinkedIn flow, load hive.linkedin-core first — it owns the auth check, rate limits, stop protocol, reply-text policy, and DOM gotchas every LinkedIn script depends on. Then load the capability skill for the task: hive.linkedin-discovery (scans / People search), hive.linkedin-messaging (lk_send_to_message_url, reply, inbox), hive.linkedin-connect (lk_send_invite, post comments), or hive.linkedin-sales-navigator (premium search + InMail).

Coordinates

Every hive-browser interact action that takes a --coordinate — and every command that returns one — operates in fractions of the viewport (0..1 for both axes). Read a target's proportional position off hive-browser screenshot — "this button is about 35% from the left and 20% from the top" → pass --coordinate 0.35,0.20. Rect-returning commands (hive-browser page shadow-query and the rect inside focused_element) also return fractions. The CLI converts to CSS pixels internally before dispatching to Chrome.

hive-browser screenshot --json                                      → image + cssWidth/cssHeight in meta
hive-browser interact --action left_click --coordinate x,y --json   → x, y are fractions 0..1
hive-browser interact --action hover --coordinate x,y --json        → fractions
hive-browser interact --action key --coordinate x,y --text k --json → fractions
hive-browser page shadow-query "<selector>" --json → rect           → rect.cx / rect.cy are fractions

Exception for zoomed elements: pages that use zoom or transform: scale() on a container (LinkedIn's #interop-outlet, some embedded iframes) render in a scaled local coordinate space. getBoundingClientRect there may not match CDP's hit space. Prefer hive-browser page shadow-query (which handles the math and returns fractions) or visually pick coordinates from a screenshot. Avoid raw hive-browser evaluate + getBoundingClientRect() for coord lookup — that returns CSS px and will be wrong when fed to a --coordinate.

Screenshot + coordinates is shadow-agnostic — prefer it on shadow-heavy sites

Start with hive-browser page snapshot when you need to inspect the page structure or find ordinary controls. If the snapshot does not show the thing you need, shows stale or misleading refs, or cannot prove where a visible target is, take hive-browser screenshot and use the screenshot + coordinate path. This is especially useful on sites that use Shadow DOM heavily.

Why:

  • CDP hit testing walks shadow roots natively. hive-browser interact --action left_click --coordinate x,y --json routes through Chrome's native hit tester, which traverses open shadow roots automatically. You don't need to know the shadow structure.
  • Keyboard dispatch follows focus into shadow roots. After a click focuses an input (even one three shadow levels deep), hive-browser interact --action key ... --json with no --selector dispatches keys to document.activeElement's computed focus target.
  • Screenshots render the real layout regardless of DOM implementation.

Whereas wait_for_selector and a selector-targeted left_click / type all use document.querySelector under the hood, which stops at shadow boundaries. They cannot see elements inside shadow roots. For shadow-DOM inputs, use a type action with no selector after focusing via a coordinate click.

Recommended workflow on shadow-heavy sites

  1. hive-browser screenshot --json → JPEG. The image is attached to your context automatically on your next turn — do NOT attach_file / read the saved_to path; that is redundant and wasteful. The result JSON carries a saved_to path (read it ONLY if, in a later turn, no image actually appeared); meta includes cssWidth/cssHeight for reference.
  2. Identify the target visually → estimate its proportional position [fx, fy] where each is in 0..1.
  3. hive-browser interact --action left_click --coordinate fx,fy --json → the CLI converts to CSS px and dispatches; CDP native hit testing focuses the element. The result includes focused_element: {tag, id, role, contenteditable, rect, inFrame?, ...} — use it to verify you actually focused what you intended. rect is in fractions (same space as your input). When focus is inside a same-origin iframe, the descriptor reports the inner element and adds inFrame: [...] breadcrumbs.
  4. hive-browser interact --action type --text "..." --json with no --selector → inserts text into document.activeElement (traverses into same-origin iframes automatically). Shadow roots, iframes, Lexical, Draft.js, ProseMirror all just work. Pass --selector instead when you have a reliable CSS selector for a light-DOM element.
  5. Verify via hive-browser screenshot OR hive-browser evaluate reading a known-reachable marker (e.g. check that the Send button's aria-disabled flipped to false).

The click→type loop (canonical pattern)

  1. Run hive-browser interact --action left_click --coordinate x,y --json to click the target element.
  2. Check the focused_element field in the result — it tells you what actually received focus (tag, id, role, contenteditable, rect).
  3. If the focused element is editable, run hive-browser interact --action type --text "..." --json to insert text. Verify the text took effect — prefer checking the underlying .value / innerText via hive-browser evaluate or confirming the submit button enabled. A screenshot alone can mislead: narrow input boxes visually clip long text, so only a portion may appear on screen even though the full string was accepted.
  4. If it is NOT editable, your click landed on the wrong thing — refine coordinates and retry. Do NOT reach for hive-browser evaluate + execCommand('insertText') or shadow-root traversals. The problem is the click target, not the typing method.

A --selector-based left_click also returns focused_element, so the same check works whether you clicked by selector or coordinate.

Empirically verified (2026-04-11)

Tested against https://www.reddit.com/r/programming/ whose search input lives at:

document > reddit-search-large [shadow]
         > faceplate-search-input#search-input [shadow]
         > input[name="q"]

Shadow-piercing selectors

When you DO want a selector-based approach and know the shadow structure, hive-browser page shadow-query supports >>> shadow-piercing syntax:

hive-browser page shadow-query "reddit-search-large >>> #search-input" --json
hive-browser page shadow-query "#interop-outlet >>> #ember37 >>> p" --json

Returns the element's rect as fractions of the viewport (feed rect.cx / rect.cy straight into a --coordinate). Remember: a type action's --selector and --wait-for-selector do not support >>> — only page shadow-query does.

Navigation and waiting

The basics

hive-browser navigate <url> --wait-until load --json   # load | domcontentloaded | networkidle
hive-browser interact --action wait --wait-for-selector "h1" --timeout-ms 2000 --json
hive-browser interact --action wait --wait-for-text "Some text" --timeout-ms 2000 --json
hive-browser reload --json
hive-browser evaluate --js 'history.back()' --json     # back/forward via history API

All return real URLs and titles. On a fast page navigate --wait-until load returns in sub-second. A wait action with --wait-for-selector / --wait-for-text typically resolves in single-digit milliseconds on elements already in the DOM.

Timing expectations (measured against real sites)

SiteNavigate load time
wikipedia.org200–500 ms
reddit.com1.5–2 s
x.com/twitter1.2–1.6 s
linkedin.com (logged in)4–5 s

After navigate, always let SPA hydrate

Even after --wait-until load, React/Vue SPAs often render their real chrome in a second pass. Add await sleep(2) to await sleep(3) before querying for site-specific elements. Otherwise a wait action will fail on elements that do exist moments later.

Reading pages efficiently

  • Prefer hive-browser page snapshot over hive-browser page text "body" — returns a compact ~1–5 KB accessibility tree vs 100+ KB of raw HTML.
  • State-changing hive-browser interact actions (left_click, type, scroll) wait 0.5 s for the page to settle after a successful action, then attach a fresh accessibility snapshot under the snapshot key of their result. Use it to decide your next action — do NOT run hive-browser page snapshot separately after every action. Tune the capture via --auto-snapshot-mode: simple (the default — trims unnamed structural nodes), default (full tree), interactive (only controls — tightest token footprint), or off to skip the capture entirely (useful when batching several interactions and you don't need the intermediate trees). Run hive-browser page snapshot explicitly only when you need a newer view or a different mode than what was auto-captured.
  • Complex pages (LinkedIn, Twitter/X, SPAs with virtual scrolling) can have DOMs that don't match what's visually rendered — snapshot refs may be stale, missing, or misaligned with visible layout. Try the available snapshot first; when the target is not present in that snapshot or visual position matters, switch to hive-browser screenshot to orient yourself.
  • Only fall back to hive-browser page text for extracting specific small elements by CSS selector.

Typing and keyboard input

ALWAYS click before typing into rich-text editors

The single most common "looks like it worked but send button stays disabled" failure. If you're typing into a modern editor (X/Twitter's Draft.js compose, LinkedIn's post composer, Reddit's comment box, Gmail compose, Slack, Discord, Notion, Monaco, any contenteditable), click the input area first — a left_click action with a coordinate or a selectorbefore you type.

Why this is necessary:

  • React / Vue controlled components don't trust JS-sourced .focus(). React uses event delegation and watches for native pointer/focus events — a click dispatched via CDP fires the real pointerdown/pointerup/click/focus sequence that React listens to, and updates its internal state. A JS-only .focus() sets document.activeElement but the framework's controlled state doesn't see it.
  • Draft.js (X/Twitter compose) and Lexical (Gmail, LinkedIn DMs) use contenteditable divs with immutable editor state. They only enter "edit mode" after a real click on the editor surface. Typing at them without clicking routes keys to document.body or gets silently discarded.
  • Send/submit buttons are bound to framework state, not DOM state. They're typically disabled={!hasRealContent} where hasRealContent is computed from React/Vue/Svelte state. The input field can have characters in the DOM but the button stays disabled because the framework never saw a real input event.

The symptom is always the same: you type, the characters appear visually, and the send button doesn't enable. The agent then clicks send anyway, nothing happens, and it thinks the post failed.

Safe "click-then-type-then-verify" pattern

  1. Focus the real element via a real click (not JS .focus()). Use hive-browser page shadow-query "<selector>" --json to get coordinates, then hive-browser interact --action left_click --coordinate cx,cy --json. Wait ~0.5 s for the editor to open and focus to settle.

  2. Type the text with hive-browser interact --action type --text "..." --json — pass --selector for light-DOM inputs, or omit it for shadow-DOM / already-focused inputs. It uses CDP Input.insertText by default, the most reliable method for rich editors (Lexical, Draft.js, ProseMirror). Wait ~500 ms for framework state to commit.

  3. Verify the submit button is enabled before clicking it. Use hive-browser evaluate to check the button's disabled or aria-disabled attribute. Do NOT trust that typing worked — always check state.

    Partial visibility is fine. Small single-line inputs, chat boxes with fixed width, and search fields commonly clip or truncate long text visually — only the tail or head may be shown on screen. Don't treat that as failure. What matters is that the framework accepted the input: the submit button enabled, or element.value / innerText read via hive-browser evaluate contains the full string. If the visible pixels don't match what you typed but the button is enabled and the underlying value is correct, typing succeeded — proceed.

  4. Only click send if the button is enabled. If the button is still disabled, try the recovery dance: click the textarea again, press End, press a space, press Backspace — this forces React to recompute hasRealContent. Then re-check the button state.

Why the type action uses Input.insertText by default

Input.insertText commits text as if IME just committed it, bypassing the keyboard event pipeline. It works on every rich editor tested — Lexical (LinkedIn DMs, Gmail), Draft.js (X compose), ProseMirror (Reddit), Monaco, plain contenteditable — and is what Playwright uses under the hood.

Per-character Input.dispatchKeyEvent looks equivalent but fails on editors that route insertion through their own beforeinput state machine: the keys arrive, no text appears. This left LinkedIn's composer empty (Send disabled) in the 2026-04-11 run.

For per-keystroke dispatch (autocomplete testing, key-event-driven code editors), pass --no-use-insert-text to fall back to the keyDown/keyUp path. Pacing is fixed at 1ms.

Neutralizing beforeunload draft dialogs

When a composer has unsent text and you try to navigate away or close the tab, sites like LinkedIn pop a native "You have an unsent message, leave?" confirm dialog via window.onbeforeunload. Your automation hangs waiting on the dialog — hive-browser tab close and hive-browser navigate both time out.

Strip the handler via hive-browser evaluate before navigating (heredoc into --js - keeps the quote-heavy script intact; --js @file.js works too):

hive-browser evaluate --json --js - <<'JS'
(function(){
  window.onbeforeunload = null;
  window.addEventListener('beforeunload', function(e){
    e.stopImmediatePropagation();
  }, true);
  return true;
})()
JS
# Now hive-browser navigate / tab close work without hitting a confirm

Always include an equivalent cleanup block in any script that types into a compose UI — without it, a script crash mid-type leaves the tab in an unusable state with the draft modal blocking every subsequent automation call.

Verified site-specific quirks

SiteEditorWorkaround
X / Twitter composeDraft.jsClick [data-testid='tweetTextarea_0'] first, then a type action (default --use-insert-text handles Draft.js cleanly). First 1-2 chars may be eaten on the per-char fallback — accept truncation or prepend a throwaway char. Verify [data-testid='tweetButton'] has disabled: false before clicking.
LinkedIn (messaging, feed compose, invites)Lexical / contenteditable (in #interop-outlet)Use the LinkedIn SDK — composer flows live in hive.linkedin-core (primitives) and hive.linkedin-messaging (send/reply). If debugging primitives: hive-browser page shadow-query for the rect, click by --coordinate to focus, then type with no --selector (a --selector-based type can't reach shadow).
Reddit comment/post boxProseMirrorClick the textarea, wait 0.5s for the toolbar to mount, then type. Submit is button[slot="submit-button"] inside a shreddit-composer.
Gmail composeLexicalClick the body first. Gmail has a visible div[contenteditable=true][aria-label*='Message Body'] after opening a compose window.
Slack message boxcontenteditableClick first, then type. Send is a paper-plane button with data-qa='texty_send_button'.
DiscordSlateClick first. Discord's send is implicit on Enter (no button), so just press Enter after typing.
Monaco editors (GitHub code review, CodeSandbox)MonacoClick first, then a type action with --no-use-insert-text to force per-keystroke dispatch. Monaco listens for textarea input events on a hidden textarea — requires focus to be on that textarea.

Plain text into a real input

For plain <input> and <textarea> elements with no framework wrapper (forms on static sites, simple search bars that pass a selector string straight through), hive-browser interact --action type --selector "..." --text "..." --json is sufficient — the bridge's internal focus() call does the right thing. But when in doubt, click first. It's cheap insurance.

hive-browser interact --action type --selector "<selector>" --text "<text>" --json
  • Sends keyDown (with key, code, text fields populated) → keyUp per character (or a single Input.insertText by default)
  • Fires real keydown / keypress / input / keyup events — frameworks that branch on event.key or event.code see the right values
  • Matches what Playwright and Puppeteer send

Works on real <input>, <textarea>, and contenteditable elements. For shadow-DOM inputs, see the "shadow-heavy sites" section above — a selector-based type can't see past shadow boundaries; use a type action with no selector after a coordinate click focuses the element.

Keyboard shortcuts (Ctrl+A, Shift+Tab, Cmd+Enter)

hive-browser interact --action key --text "ctrl+a" --json        # Ctrl+A — select all
hive-browser interact --action key --text "Backspace" --json     # clear selected text
hive-browser interact --action key --text "meta+Enter" --json    # Cmd+Enter (mac) — submit
hive-browser interact --action key --text "shift+Tab" --json     # Shift+Tab — reverse focus

Modifiers can be joined into --text with +, or passed separately in the --modifiers flag (e.g. --modifiers ctrl). Accepted modifier names (case-insensitive): "alt", "ctrl" / "control", "meta" / "cmd", "shift".

Shortened here. Read the whole file on GitHub.

Signals

GitHub stars
11k
Forks
6k
Last commit
Sep 2026
Advanced
Catalog kind
skill
Gateway key
hive-browser-automation
Source
github.com/aden-hive/hive