pup-bridge-macos
Public Made by Adomby adom
The pup for macOS SKILL PACK: the AI skills that teach a cloud/container AI how to drive the macOS Puppeteer (pup) bridge (pup_* verbs): the everyday `pup` skill plus focused sub-skills for windows/sessions/tabs, screenshots+recording, credentials, lanes, settings, browsers+Chrome install, vendor logins and the Adom wiki. Installs into ~/.claude/skills + ~/.codex/skills. DOCS ONLY: this is NOT the bridge runtime. The bridge itself is one signed macOS binary that Adom Bridge for macOS installs an
name: pup-windows-sessions-tabs user-invocable: true description: "pup (Puppeteer bridge) windows, sessions, and tabs: how sessionIds work and why to task-prefix them, WINDOW OWNERSHIP so you never steal another AI thread's window (owner + session_owned_by_another_thread + takeover), the BACKGROUND-by-default rule (foreground:true is the only way to show a window; sizing/positioning does NOT foreground), running MANY TABS in one window instead of many windows, switching/listing/closing sessions and tabs, and verifying what you actually opened. Read when opening/reusing pup windows, managing tabs, deciding background vs foreground, or seeing errorCode session_owned_by_another_thread. Trigger words: pup sessionId, pup_open_window owner, session_owned_by_another_thread, takeover window, pup background, foreground:true, pup_open_tab, pup_switch_tab, pup_list_tabs, pup_switch_window, pup_list_windows, pup_close_window, pup tabs, many tabs one window, don't steal window, pup_lower_os_window, pup_raise_os_window, pup_alert_window."
Parent skill: pup-bridge · See also: pup (start here), pup-screenshots-recording, pup-browsers-and-chrome.
pup — windows, sessions & tabs
A session = one browser window that holds many tabs.
You address everything by sessionId. profile (defaults to sessionId) names the on-disk cookie jar.
sessionIds — task-prefix them, and pass owner
pup_open_window {sessionId} either creates a window (new id) or reuses/navigates an existing
one (id already exists). Because sessions are shared across every AI thread on this desktop, a generic
id collides:
- Task-prefix the id:
"<your-task>-web"(e.g.wikifix-preview), neverwiki/test/main. - Always pass
owner:"<your-task>". It's an advisory label that lets pup protect your window. - Pick a stable
profileacross runs if you want the cookie jar to persist (user logs in once, reuse for months):{"sessionId":"vendor-fetch","profile":"vendor","owner":"vendor-fetch", …}.
Window ownership — do NOT steal another AI thread's window
If you pup_open_window a sessionId that already exists with a new url, pup NAVIGATES that window
— destroying whatever another thread was doing there. pup guards this:
- Declared cross-owner reuse is REFUSED —
errorCode: session_owned_by_another_thread(also onpup_navigate/pup_close/pup_close_window). The_hinttells you the current owner- url + age.
- If YOU get that error — the window isn't yours. Open your OWN with a unique task-prefixed sessionId.
Pass
takeover:trueONLY if the user explicitly told you to repurpose that window. - Anonymous grabs are allowed but loudly reprimanded in the
_hint(with the previous url so you can put it back) — so ALWAYS declareownerto be a good citizen and to get protection. - Reusing your OWN window is good — same sessionId + same owner = a smooth refresh (e.g. re-rendering a page you just republished). Ownership only blocks cross-thread grabs.
- Unsure what exists?
pup_status '{}'lists every window with itsowner, currenturl, andageMinutes. Run it at the start of a task (especially after a context compaction) before picking an id.
# right — owned, task-prefixed
adom-bridge-cli pup_open_window '{"sessionId":"wikifix-preview","owner":"wikifix","url":"<url>"}'
# wrong — generic + anonymous: may hijack another thread's window (pup reprimands you in _hint)
adom-bridge-cli pup_open_window '{"sessionId":"wiki","url":"<url>"}'
Background by default — pup NEVER pops onto the user's screen unless asked
pup is the AI's workspace, not the user's. Every window opens in the background (pup's browser app is hidden, the Cmd+H state, so nothing covers the user and no focus is stolen) and stays fully drivable + screenshottable there — you do NOT lower it yourself.
foreground:trueis the ONLY thing that shows a window — and it is GATED (v1.8.70). Not sizing, not navigating, not screenshotting, not recording setup. Foregrounding requires BOTHforeground:trueANDforegroundReason:"<quote/paraphrase what the user said>"(min 10 chars). Without the reason the open still succeeds but the window stays BACKGROUND (foregroundDenied:'foreground_reason_required'). The reason is logged, recorded inlastAgentUpdate, and surfaced aslastForegroundin status — the user can always audit who put a window on their screen and why. Pass it ONLY when the user explicitly wants to WATCH (a demo, a "show me", a live recording); after the show-me,pup_lower_os_windowto return it.- Sizing/positioning does NOT foreground —
pup_open_window {width,height,x,y}places the window at an exact size/spot for clean screenshot framing while it stays in the background. So you can frame a shot without ever disturbing the user's main work. pup_alert_windowposts a macOS notification the user can click (a non-intrusive "it's ready/done" nudge that never takes focus) — prefer it over raising the window. Use it for an explicit end-of-task "it's ready".pup_raise_os_window/pup_focus_windowbring a window forward — GATED the same way: REFUSED (foreground_reason_required) unless you passforegroundReason. Use only for an explicit user show-me;pup_lower_os_windowtucks it back after. Raise steals focus and can leak a mid-typed keystroke into the page (pup blurs the page's focused element first to reduce it) — to just signal "I updated this window", useupdateNote/pup_alert_window, NOT raise.
Signalling what you changed — updateNote (so the user follows along)
When you drive a background window the user can't tell you touched it — and across several tabs they lose track of WHICH one you just changed. macOS has no taskbar button to flash, so say it in the record:
- A pure read should pass
pup_eval {"readOnly":true}so a polling loop doesn't count as an update. - Say what you did: pass
updateNote":"<short what-I-did>"on any mutating verb. pup recordslastAgentUpdate {tabId, verb, note, ts}and surfaces it top-level and per-tab inpup_list_tabs/pup_list_windows— so the user (or a Session Monitor) sees which tab you staged and why. Add anupdateNoteon the meaningful steps of a multi-tab task.
# default: background — drive it invisibly, user never sees a thing
adom-bridge-cli pup_open_window '{"sessionId":"job-web","owner":"job","url":"<url>"}'
adom-bridge-cli pup_screenshot '{"sessionId":"job-web"}'
# only if the user asked to SEE it:
adom-bridge-cli pup_open_window '{"sessionId":"demo-web","owner":"demo","url":"<url>","foreground":true}'
One window, many tabs (multi-URL tasks)
For a vendor fetch / multi-doc review, put the URLs in tabs of one window, not separate windows:
adom-bridge-cli pup_open_window '{"sessionId":"task-web","owner":"task","url":"https://first.example.com"}'
adom-bridge-cli pup_open_tab '{"sessionId":"task-web","url":"https://second.example.com"}' # → { tabId: "tab-2", tabCount: 2 }
adom-bridge-cli pup_switch_tab '{"sessionId":"task-web","tabId":"tab-2"}'
adom-bridge-cli pup_screenshot '{"sessionId":"task-web","tabId":"tab-2"}' # tabId optional — omit for the active tab
adom-bridge-cli pup_close_tab '{"sessionId":"task-web","tabId":"tab-2"}'
pup_open_tabreturns{tabId, tabCount, activeTabId}; pass thetabIdtoswitch/close/screenshot/eval/navigate(omit to target the active tab).- Closing the last tab closes the window.
- Popup tabs (e.g. a PDF the page opens) auto-attach and appear in
pup_list_tabs; a closed popup's last URL is retained so you canpup_fetch_urlit if you missed it.
Don't double-open; reuse correctly
- New session entirely →
pup_open_window. - Same session, new page in the same tab →
pup_navigate(withtabIdif multiple tabs). - A second view in the same session →
pup_open_tab(NOT a second window). - Switch which window is "active" →
pup_switch_window; list them →pup_list_windows.
Opening the same content under a new sessionId leaves the user staring at two identical windows — so
pup_status first and reuse when the URL is already open.
Clean up after yourself — close windows when the task is done
pup never auto-closes a window (a "stale" one might belong to a thread that's just paused between turns). So every window you open lingers on the user's real desktop until you close it. Left alone, a day of tasks piles up a dozen dead pup windows the user has to find and close.
- When you finish a task,
pup_close_windowthe windows you opened for it. Same for one-off windows (a quick screenshot/scrape) — close them right after. - Check what's yours:
pup_statuslists every window with itsownerandageMinutes, and now nudges you (a🧹 CLEANUPhint) when windows are old (≥30 min) or piling up. Act on it — close the ones you're done with. Only close windows you own; never close another thread's. - Long-running work? Reuse ONE window (
pup_navigate/pup_open_tab) instead of opening a fresh window each step, so there's only one thing to clean up. - If you truly need a window to persist across turns (a login you'll reuse for hours), that's fine — but it's a deliberate choice, and you still own closing it when done.
Verify what you opened — ok:true is not enough
pup_open_window returns ok:true as soon as navigation starts, not when content rendered. Confirm
with pup_eval '{"sessionId":"…","expr":"document.title"}' (fastest — 404s, login walls, anti-bot
blocks all change the title) or a pup_screenshot when the title isn't discriminating.
Verbs at a glance
| Verb | Does |
|---|---|
pup_open_window |
Create (or reuse) a session/window. Args: sessionId, owner, url?, profile?, foreground?, width/height/x/y?, takeover?, updateNote? |
pup_navigate |
Drive an existing tab to a URL (no new window) — sessionId, url, tabId? |
pup_open_tab / pup_switch_tab / pup_close_tab / pup_list_tabs |
Tabs within one window |
pup_switch_window / pup_list_windows |
Multiple sessions |
pup_status |
List every session + owner/url/ageMinutes (NOT a readiness check) |
pup_lower_os_window / pup_raise_os_window / pup_focus_window / pup_alert_window |
Hide / show / focus / notify |
pup_close_window (one session) / pup_close (all) |
Close — destructive |
---
name: pup-windows-sessions-tabs
user-invocable: true
description: "pup (Puppeteer bridge) windows, sessions, and tabs: how sessionIds work and why to task-prefix them, WINDOW OWNERSHIP so you never steal another AI thread's window (owner + session_owned_by_another_thread + takeover), the BACKGROUND-by-default rule (foreground:true is the only way to show a window; sizing/positioning does NOT foreground), running MANY TABS in one window instead of many windows, switching/listing/closing sessions and tabs, and verifying what you actually opened. Read when opening/reusing pup windows, managing tabs, deciding background vs foreground, or seeing errorCode session_owned_by_another_thread. Trigger words: pup sessionId, pup_open_window owner, session_owned_by_another_thread, takeover window, pup background, foreground:true, pup_open_tab, pup_switch_tab, pup_list_tabs, pup_switch_window, pup_list_windows, pup_close_window, pup tabs, many tabs one window, don't steal window, pup_lower_os_window, pup_raise_os_window, pup_alert_window."
---
**Parent skill:** pup-bridge · See also: `pup` (start here), `pup-screenshots-recording`, `pup-browsers-and-chrome`.
# pup — windows, sessions & tabs
A **session** = one browser **window** that holds **many tabs**.
You address everything by `sessionId`. `profile` (defaults to `sessionId`) names the on-disk cookie jar.
## sessionIds — task-prefix them, and pass `owner`
`pup_open_window {sessionId}` either **creates** a window (new id) or **reuses/navigates** an existing
one (id already exists). Because sessions are **shared across every AI thread on this desktop**, a generic
id collides:
- **Task-prefix the id:** `"<your-task>-web"` (e.g. `wikifix-preview`), never `wiki` / `test` / `main`.
- **Always pass `owner:"<your-task>"`.** It's an advisory label that lets pup protect your window.
- Pick a **stable `profile`** across runs if you want the cookie jar to persist (user logs in once, reuse
for months): `{"sessionId":"vendor-fetch","profile":"vendor","owner":"vendor-fetch", …}`.
## Window ownership — do NOT steal another AI thread's window
If you `pup_open_window` a sessionId that already exists **with a new url**, pup NAVIGATES that window
— destroying whatever another thread was doing there. pup guards this:
1. **Declared cross-owner reuse is REFUSED** — `errorCode: session_owned_by_another_thread` (also on
`pup_navigate` / `pup_close` / `pup_close_window`). The `_hint` tells you the current owner
+ url + age.
2. **If YOU get that error** — the window isn't yours. Open your OWN with a unique task-prefixed sessionId.
Pass **`takeover:true`** ONLY if the user explicitly told you to repurpose that window.
3. **Anonymous grabs are allowed but loudly reprimanded** in the `_hint` (with the previous url so you can
put it back) — so ALWAYS declare `owner` to be a good citizen and to get protection.
4. **Reusing your OWN window is good** — same sessionId + same owner = a smooth refresh (e.g. re-rendering
a page you just republished). Ownership only blocks *cross-thread* grabs.
5. **Unsure what exists?** `pup_status '{}'` lists every window with its `owner`, current `url`, and
`ageMinutes`. Run it at the start of a task (especially after a context compaction) before picking an id.
```bash
# right — owned, task-prefixed
adom-bridge-cli pup_open_window '{"sessionId":"wikifix-preview","owner":"wikifix","url":"<url>"}'
# wrong — generic + anonymous: may hijack another thread's window (pup reprimands you in _hint)
adom-bridge-cli pup_open_window '{"sessionId":"wiki","url":"<url>"}'
```
## Background by default — pup NEVER pops onto the user's screen unless asked
**pup is the AI's workspace, not the user's.** Every window opens **in the background** (pup's browser app is hidden,
the Cmd+H state, so nothing covers the user and no focus is stolen) and stays fully drivable + screenshottable there — you do NOT lower it yourself.
- **`foreground:true` is the ONLY thing that shows a window — and it is GATED (v1.8.70).** Not
sizing, not navigating, not screenshotting, not recording setup. Foregrounding requires BOTH
`foreground:true` AND `foregroundReason:"<quote/paraphrase what the user said>"` (min 10 chars).
Without the reason the open still succeeds but the window stays BACKGROUND
(`foregroundDenied:'foreground_reason_required'`). The reason is logged, recorded in
`lastAgentUpdate`, and surfaced as `lastForeground` in status — the user can always audit who
put a window on their screen and why. Pass it ONLY when the user explicitly wants to WATCH (a
demo, a "show me", a live recording); after the show-me, `pup_lower_os_window` to return it.
- **Sizing/positioning does NOT foreground** — `pup_open_window {width,height,x,y}` places the window
at an exact size/spot **for clean screenshot framing** while it stays in the background. So you can frame
a shot without ever disturbing the user's main work.
- **`pup_alert_window`** posts a macOS notification the user can click (a non-intrusive "it's
ready/done" nudge that never takes focus) — prefer it over raising the window. Use it for an explicit end-of-task "it's ready".
- `pup_raise_os_window` / `pup_focus_window` bring a window forward — **GATED the same way:
REFUSED (`foreground_reason_required`) unless you pass `foregroundReason`.** Use only for an
explicit user show-me; `pup_lower_os_window` tucks it back after. **Raise steals focus and can
leak a mid-typed keystroke into the page** (pup blurs the page's focused element first to reduce
it) — to just signal "I updated this window", use `updateNote` / `pup_alert_window`, NOT raise.
## Signalling what you changed — `updateNote` (so the user follows along)
When you drive a background window the user can't tell you touched it — and across several tabs they lose
track of WHICH one you just changed. macOS has no taskbar button to flash, so say it in the record:
- A pure read should pass `pup_eval {"readOnly":true}` so a polling loop doesn't count as an update.
- **Say what you did:** pass **`updateNote":"<short what-I-did>"`** on any mutating verb. pup records
`lastAgentUpdate {tabId, verb, note, ts}` and surfaces it **top-level and per-tab** in
`pup_list_tabs` / `pup_list_windows` — so the user (or a Session Monitor) sees which tab you
staged and why. Add an `updateNote` on the meaningful steps of a multi-tab task.
```bash
# default: background — drive it invisibly, user never sees a thing
adom-bridge-cli pup_open_window '{"sessionId":"job-web","owner":"job","url":"<url>"}'
adom-bridge-cli pup_screenshot '{"sessionId":"job-web"}'
# only if the user asked to SEE it:
adom-bridge-cli pup_open_window '{"sessionId":"demo-web","owner":"demo","url":"<url>","foreground":true}'
```
## One window, many tabs (multi-URL tasks)
For a vendor fetch / multi-doc review, put the URLs in **tabs of one window**, not separate windows:
```bash
adom-bridge-cli pup_open_window '{"sessionId":"task-web","owner":"task","url":"https://first.example.com"}'
adom-bridge-cli pup_open_tab '{"sessionId":"task-web","url":"https://second.example.com"}' # → { tabId: "tab-2", tabCount: 2 }
adom-bridge-cli pup_switch_tab '{"sessionId":"task-web","tabId":"tab-2"}'
adom-bridge-cli pup_screenshot '{"sessionId":"task-web","tabId":"tab-2"}' # tabId optional — omit for the active tab
adom-bridge-cli pup_close_tab '{"sessionId":"task-web","tabId":"tab-2"}'
```
- `pup_open_tab` returns `{tabId, tabCount, activeTabId}`; pass the `tabId` to
`switch`/`close`/`screenshot`/`eval`/`navigate` (omit to target the active tab).
- **Closing the last tab closes the window.**
- Popup tabs (e.g. a PDF the page opens) auto-attach and appear in `pup_list_tabs`; a closed popup's
last URL is retained so you can `pup_fetch_url` it if you missed it.
## Don't double-open; reuse correctly
- New session entirely → `pup_open_window`.
- Same session, new page in the same tab → `pup_navigate` (with `tabId` if multiple tabs).
- A second view in the same session → `pup_open_tab` (NOT a second window).
- Switch which window is "active" → `pup_switch_window`; list them → `pup_list_windows`.
Opening the same content under a new `sessionId` leaves the user staring at two identical windows — so
`pup_status` first and reuse when the URL is already open.
## Clean up after yourself — close windows when the task is done
pup **never auto-closes** a window (a "stale" one might belong to a thread that's just paused
between turns). So every window you open lingers on the **user's real desktop** until *you* close it.
Left alone, a day of tasks piles up a dozen dead pup windows the user has to find and close.
- **When you finish a task, `pup_close_window` the windows you opened for it.** Same for one-off
windows (a quick screenshot/scrape) — close them right after.
- **Check what's yours:** `pup_status` lists every window with its `owner` and `ageMinutes`, and
now nudges you (a `🧹 CLEANUP` hint) when windows are old (≥30 min) or piling up. Act on it — close
the ones you're done with. Only close windows **you own**; never close another thread's.
- **Long-running work?** Reuse ONE window (`pup_navigate` / `pup_open_tab`) instead of opening
a fresh window each step, so there's only one thing to clean up.
- If you truly need a window to persist across turns (a login you'll reuse for hours), that's fine —
but it's a deliberate choice, and you still own closing it when done.
## Verify what you opened — `ok:true` is not enough
`pup_open_window` returns `ok:true` as soon as navigation *starts*, not when content rendered. Confirm
with `pup_eval '{"sessionId":"…","expr":"document.title"}'` (fastest — 404s, login walls, anti-bot
blocks all change the title) or a `pup_screenshot` when the title isn't discriminating.
## Verbs at a glance
| Verb | Does |
|---|---|
| `pup_open_window` | Create (or reuse) a session/window. Args: `sessionId`, `owner`, `url?`, `profile?`, `foreground?`, `width/height/x/y?`, `takeover?`, `updateNote?` |
| `pup_navigate` | Drive an existing tab to a URL (no new window) — `sessionId`, `url`, `tabId?` |
| `pup_open_tab` / `pup_switch_tab` / `pup_close_tab` / `pup_list_tabs` | Tabs within one window |
| `pup_switch_window` / `pup_list_windows` | Multiple sessions |
| `pup_status` | List every session + `owner`/`url`/`ageMinutes` (NOT a readiness check) |
| `pup_lower_os_window` / `pup_raise_os_window` / `pup_focus_window` / `pup_alert_window` | Hide / show / focus / notify |
| `pup_close_window` (one session) / `pup_close` (all) | Close — destructive |