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-credentials description: "How pup signs the user into sites AS THEM on macOS: pup's Keychain-backed credential vault (auto-capture on login + silent autofill on return, matched across a site's whole registrable domain), Google sign-in (pup_google_signin), syncing the vault with the user's other desktops (pup_vault_sync), HTTP Basic auth from the vault, and what to do when a saved password is stale. Importing the browser's own saved passwords is NOT available on macOS (pup_import_browser_logins answers unsupported_platform); logins grow by the user signing in once in a pup window. Trigger words: log me in, sign me in, saved password, pup_login, credential_set, pup_credentials, vault, stale password, google sign in in pup."
pup credentials — sign the user in AS THEM
⛔ THE RULE: serve the login from the vault, NEVER ask the human for a password
pup's adom-you vault EXISTS so the AI never has to ask the user for a username or password. If you find yourself about to ask "what's your Google password?" — STOP. You have verbs for this:
- It's probably already saved.
pup_credentials {}lists every host+username pup holds. To sign a window in,pup_login {submit:true, submitReason}— pup fills and submits from the vault. To hand the actual secret to a LOCAL desktop assistant,pup_credential_reveal {host, reason}(one consent click). - Not saved on THIS Mac? Ask the user to sign in ONCE in the pup window. pup captures the
login as they submit it, and serves it from then on. (Importing the browser's own saved passwords
is not available on macOS:
pup_import_browser_loginsanswersunsupported_platform.) - Saved on ANOTHER of the user's desktops? Sync it.
pup_vault_sync {peer, reason}merges this box's adom-you logins with a peer desktop's, BOTH directions, so both boxes hold the union (newest-wins on a conflict). ONE consent toast on this box; passwords move pup-to-pup over ab's cross-desktop egress and never touch an AI.targetsnames the peers. This is the answer when one box has learned some logins and another box built up others. - Only if a stored password is STALE do you fall to section 3 below. That is the LAST resort, not the first move.
Asking the human to paste a password in chat is almost always the wrong move: it exposes the secret, it does not grow the shared adom-you profile, and pup already has a cleaner path. Grow the vault (capture on login, sync) and serve from it.
pup is the user's doppelganger, so it needs to log in the way the user does. This skill is the whole
credential story: the vault, syncing it, and what to do when a stored password has gone stale. Verbs live in the pup bridge;
call them with adom-bridge-cli.
0. Google: pup_google_signin (never pup_login)
Google asks for the email on one page and the password on the next, so pup_login cannot do it and
says so (no_login_form). Use the Google verbs (2.0.596):
adom-bridge pup_google_accounts {} # who this user is on Google, cached a day
adom-bridge pup_google_signin {"account":"[email protected]","submitReason":"the task is the work calendar"}
Most users have several Google accounts, and pup never guesses which one. The first time it is
asked, pup searches everything it can reach on the Mac: the user's browser profiles as nb reports them
(with the label the user gave each, like "John Personal"), pup's own vault, and which accounts pup's
profile is already signed into. Each account comes back as work or personal (from
its domain) and likelyStale when it lives only in pup's vault and in no browser (those are usually
years old and Google demands a second step for them).
Call pup_google_signin with no account and more than one account exists, and the answer is
google_account_choice_required with the list. Decide, in order:
- The task. Company mail, calendars, docs, vendor portals: the work account. Family, banking, school, personal purchases: the personal one.
- Your own skills. The user's work and personal skills usually name their accounts. Search them.
- Ask the user. Then pass
accountas the email."work"or"personal"also works when exactly one account of that kind exists (a user with two work accounts gets the list again).
What the verb does, and what each answer means:
| Answer | Meaning | Do |
|---|---|---|
signedIn:true, alreadySignedIn:true |
pup's profile was already signed in; no password sent | drive the window |
signedIn:true |
email and saved password went through | drive the window |
google_password_missing |
not signed in and no saved password for that account | the user signs in once in the pup window (pup captures it) |
google_verification_required + options |
Google wants a second step | pup_google_signin {window, account, challenge:"text message"} |
google_verification_required (code) |
Google sent a code | pup_google_signin {window, account, code:"123456"} |
google_verification_required (other device) |
the user must tap Yes on their phone | tell them; pup already waited 45 s |
google_password_rejected |
the saved password is out of date | section 3; never retry the same one |
google_signin_unconfirmed |
Gmail opened as a different account | call again; do not assume |
Traps measured on John's laptop (2026-09-16):
- Landing on Gmail proves nothing.
mail.google.com/mail/u/?authuser=<email>for an account that is not signed in silently opens the signed-in account's inbox. pup confirms the account by name. - Never restart a sign-in stuck on a verification step. That resends the password and a new code.
Finish it with
{window, code}or{window, challenge}. - A text code can finish hands-free when your thread has the user's standing permission to read their texts (a personal texting skill). Otherwise ask the user for it.
- Google does not block pup's default window. A window opened with
lane:"cdp"carries a debug port and can draw "This browser or app may not be secure". - The vault holds one login per site AND username since 2.0.596. Before that a second Google account overwrote the first.
TOAST THE USER, AND PUT THE WINDOW ON THE TOAST (John, 2026-09-17)
"i didn't even realize you were waiting on me. why did you not ab toast me?" ... "when i just clicked that ab toast, i would have expected its action when i clicked the toast to open your pup window. i believe ab lets you attach those actions now." ... "whenever you need the users attention for a password, you should do it that way"
The moment a flow parks on the human, TOAST, and make the toast open the thing that is waiting:
adom-bridge notify_user {"title":"pup needs you to sign in once",
"body":"Sign in as [email protected] in the pup window (click here to open it).",
"level":"warning", "focus":{"hwnd": <the pup window's hwnd>}}
focus is what makes the click useful: {hwnd} raises that exact pup window, {host:true} reveals the
app Bridge runs under (use it for a Bridge consent, which is a notification inside Hydrogen, not a
window). A toast with no focus is the one the user clicks and nothing happens, which is worse than none.
pup does this itself from 2.0.603 on every no-password answer (google_password_missing,
google_verification_required, google_password_rejected, no_credential), so an AI that just relays the answer is already covered. Do it yourself too whenever
you are about to sit and wait: the user cannot see your thread.
Capture works on BOTH lanes (2.0.602, and why it did not)
Capture is what makes "sign in once" true: type a password into any pup window and pup stores it. It lived
ONLY on the CDP lane until 2.0.602, so every login typed into a normal (extension-lane) window was thrown
away and pup asked again next time. John typed his Google password into a pup window, pup kept nothing,
and the explanation "your browser never saved it" was about the wrong thing. If you are on a build older
than 2.0.602, a typed password is NOT stored on the default lane. Verify a capture instead of assuming it:
pup_credentials {} must list the host right after the sign-in.
1. The vault (how pup remembers logins)
- Auto-capture: when the user (or pup) signs into a site, pup grabs the credential and stores it
in the macOS Keychain. Passwords are NEVER
echoed back —
pup_credentialslists host + username only. - Autofill: on a return visit pup fills the saved login in the background (no Chrome
dropdown, no foregrounding). Submitting is reason-gated (
pup_login {submit:true, submitReason}) and confirms the live domain first (anti-phishing). - Registrable-domain matching: a login saved on
ms.example.comis offered onexample.comand its siblings, like a browser password manager. So the host a credential was saved under does not have to equal the host of the page you are filling. - Durable: the vault index lives in
~/.adom(not the bridge cache), so it survives pup updates. - Store one explicitly:
credential_set {host, username, password}. List:pup_credentials {}.
2b. Sync the vault across the user's desktops (a real union, not a push)
The user runs several desktops; each pup learns different logins over time (one signs into Gusto on
the laptop, another into a vendor portal on the office box). pup_vault_sync {peer, reason} makes
both complete:
adom-bridge-cli pup_vault_sync {"peer":"arav-rog","reason":"both my desktops should have all my logins"}
- Bidirectional. It pushes THIS box's logins to the peer AND pulls the peer's back, so afterwards both boxes hold the UNION. A one-way copy would leave each box missing what the other learned — which is exactly the failure this verb avoids.
- Newest-wins. If the same host has different passwords on the two boxes, the more recently updated one wins on both. Identical entries are skipped.
- No AI sees a password. The bytes go pup→ab→peer-ab→peer-pup over the account's cross-desktop
egress.
pup_vault_export/pup_vault_import(the internal halves) are refused for cloud/relay callers — only same-account desktops sync. - One consent. The initiating box shows one toast ("Sync your saved logins with ?"); the user clicks Allow. Like every consent verb, waiting on the click is not a timeout — re-call with the same args to re-attach.
- Run it again whenever a box has learned new logins; it is cheap and idempotent.
3. A saved password is STALE
The tell: pup fills + submits (filledUsername/filledPassword/submitted all true) but the site
says "Fail to authenticate" / "incorrect password". The stored secret is out of date, OR the
username itself is wrong (the same site can be saved under different usernames).
On a Mac the clean fix is the user: ask them to sign in once in the pup window (toast with the
window attached, see above). pup's capture rail stores the current login, with the right username,
and pup_login works from then on. If the user gives you the current details directly, store them
with credential_set {host, username, password} and retry. If another of their desktops holds a
fresher copy, pup_vault_sync {peer, hostFilter} brings it over. Registrable-domain matching
(section 1) handles a login saved on ms.<site> and used on <site>.
Privacy rules (non-negotiable)
- Only ever handle a login for the site the user asked about, on the user's own machine, at their request.
- A password you are given is used once to store it. Store it and move on — never print it, never repeat it in a later message, never log it.
- Prefer, when the user would rather pup never see the plaintext: have them log in once manually
(pup's capture rail grabs it fresh), or use a "Sign in with Google/Microsoft" SSO path
(
pup-vendor-login) that types no password at all.
When NOT to need any of this
Manufacturer-direct sites (TI, ST, ams, …) and datasheets need no login at all — see
ee-parts-sites.md. Reach for credential recovery only when a site truly gates what the user wants
behind their own account.
Read landed before you say a word about signing in (pup 2.0.642)
Every pup_open_window and pup_navigate response carries landed:{url,title,loginPage,signedIn,_hint}. The
shared adom-you profile keeps sessions (claude.ai, chatgpt.com, the wiki, vendor portals) for weeks, so most
opens land already signed in. An AI toasted "Sign into claude.ai in the pup window once" on 2026-09-19 while the
window was already on the user's chats. Never do that: read landed; signedIn:true means say nothing;
loginPage:true means try the vault, Google sign-in and the site's recipe in login-recipes/ first, and toast
the user only when pup itself cannot finish. Recipes: login-recipes/claude.ai.md, login-recipes/chatgpt.com.md.
HTTP Basic/Digest auth (the browser's native "Sign in" dialog) is answered from the vault on every lane (2.0.644)
Some downloads (SamacSys / componentsearchengine.com CAD models, vendor file servers) answer with a 401 and
Chrome pops its native "Sign in" dialog, which no page verb can see. pup answers it from the vault on both
lanes now, matched on the registrable domain (a componentsearchengine.com entry serves st., ms. and
nxp.componentsearchengine.com). With no entry, pup CANCELS the challenge so the dialog never blocks the
window, and the next pup_navigate / pup_fetch_url returns errorCode: no_credential naming the host, with
the routes: the user signs in once, pup_vault_sync {peer, hostFilter}, or credential_set {host:"<domain>", username, password}. A stored credential the server rejects is tried once, then reported
the same way (stale password: see login-recipes/recover-a-current-password.md). pup_fetch_url works on
the extension lane too, so a gated file can be fetched as bodyBase64 without a download dialog.
Where secrets live
Every password is a macOS Keychain item under the service adom-bridge-pup (keyed by host, or
host|username for a second account on the same host), indexed by ~/.adom/pup-credentials.index.json
(host + username only). pup's own browser runs with an in-memory cookie key (--use-mock-keychain),
so it never asks the user for Keychain access.
---
name: pup-credentials
description: "How pup signs the user into sites AS THEM on macOS: pup's Keychain-backed credential vault (auto-capture on login + silent autofill on return, matched across a site's whole registrable domain), Google sign-in (pup_google_signin), syncing the vault with the user's other desktops (pup_vault_sync), HTTP Basic auth from the vault, and what to do when a saved password is stale. Importing the browser's own saved passwords is NOT available on macOS (pup_import_browser_logins answers unsupported_platform); logins grow by the user signing in once in a pup window. Trigger words: log me in, sign me in, saved password, pup_login, credential_set, pup_credentials, vault, stale password, google sign in in pup."
---
# pup credentials — sign the user in AS THEM
## ⛔ THE RULE: serve the login from the vault, NEVER ask the human for a password
pup's adom-you vault EXISTS so the AI never has to ask the user for a username or password. If you
find yourself about to ask "what's your Google password?" — STOP. You have verbs for this:
1. **It's probably already saved.** `pup_credentials {}` lists every host+username pup holds. To
sign a window in, `pup_login {submit:true, submitReason}` — pup fills and submits from the vault.
To hand the actual secret to a LOCAL desktop assistant, `pup_credential_reveal {host, reason}`
(one consent click).
2. **Not saved on THIS Mac? Ask the user to sign in ONCE in the pup window.** pup captures the
login as they submit it, and serves it from then on. (Importing the browser's own saved passwords
is not available on macOS: `pup_import_browser_logins` answers `unsupported_platform`.)
3. **Saved on ANOTHER of the user's desktops? Sync it.** `pup_vault_sync {peer, reason}` merges this
box's adom-you logins with a peer desktop's, BOTH directions, so both boxes hold the union
(newest-wins on a conflict). ONE consent toast on this box; passwords move pup-to-pup over ab's
cross-desktop egress and **never touch an AI**. `targets` names the peers. This is the answer when
one box has learned some logins and another box built up others.
4. **Only if a stored password is STALE** do you fall to section 3 below. That is the LAST resort, not the first move.
Asking the human to paste a password in chat is almost always the wrong move: it exposes the secret,
it does not grow the shared adom-you profile, and pup already has a cleaner path. Grow the vault
(capture on login, sync) and serve from it.
pup is the user's doppelganger, so it needs to log in the way the user does. This skill is the whole
credential story: the vault, syncing it, and what to do when a stored password has gone stale. Verbs live in the `pup` bridge;
call them with `adom-bridge-cli`.
## 0. Google: `pup_google_signin` (never pup_login)
Google asks for the email on one page and the password on the next, so `pup_login` cannot do it and
says so (`no_login_form`). Use the Google verbs (2.0.596):
```
adom-bridge pup_google_accounts {} # who this user is on Google, cached a day
adom-bridge pup_google_signin {"account":"[email protected]","submitReason":"the task is the work calendar"}
```
**Most users have several Google accounts, and pup never guesses which one.** The first time it is
asked, pup searches everything it can reach on the Mac: the user's browser profiles as nb reports them
(with the label the user gave each, like "John Personal"), pup's own vault, and which accounts pup's
profile is already signed into. Each account comes back as `work` or `personal` (from
its domain) and `likelyStale` when it lives only in pup's vault and in no browser (those are usually
years old and Google demands a second step for them).
Call `pup_google_signin` with no `account` and more than one account exists, and the answer is
`google_account_choice_required` with the list. Decide, in order:
1. **The task.** Company mail, calendars, docs, vendor portals: the work account. Family, banking,
school, personal purchases: the personal one.
2. **Your own skills.** The user's work and personal skills usually name their accounts. Search them.
3. **Ask the user.** Then pass `account` as the email. `"work"` or `"personal"` also works when exactly
one account of that kind exists (a user with two work accounts gets the list again).
What the verb does, and what each answer means:
| Answer | Meaning | Do |
|---|---|---|
| `signedIn:true, alreadySignedIn:true` | pup's profile was already signed in; no password sent | drive the window |
| `signedIn:true` | email and saved password went through | drive the window |
| `google_password_missing` | not signed in and no saved password for that account | the user signs in once in the pup window (pup captures it) |
| `google_verification_required` + `options` | Google wants a second step | `pup_google_signin {window, account, challenge:"text message"}` |
| `google_verification_required` (code) | Google sent a code | `pup_google_signin {window, account, code:"123456"}` |
| `google_verification_required` (other device) | the user must tap Yes on their phone | tell them; pup already waited 45 s |
| `google_password_rejected` | the saved password is out of date | section 3; never retry the same one |
| `google_signin_unconfirmed` | Gmail opened as a different account | call again; do not assume |
Traps measured on John's laptop (2026-09-16):
- **Landing on Gmail proves nothing.** `mail.google.com/mail/u/?authuser=<email>` for an account that is
not signed in silently opens the signed-in account's inbox. pup confirms the account by name.
- **Never restart a sign-in stuck on a verification step.** That resends the password and a new code.
Finish it with `{window, code}` or `{window, challenge}`.
- **A text code can finish hands-free** when your thread has the user's standing permission to read
their texts (a personal texting skill). Otherwise ask the user for it.
- Google does not block pup's default window. A window opened with `lane:"cdp"` carries a debug port
and can draw "This browser or app may not be secure".
- **The vault holds one login per site AND username** since 2.0.596. Before that a second Google
account overwrote the first.
### TOAST THE USER, AND PUT THE WINDOW ON THE TOAST (John, 2026-09-17)
> "i didn't even realize you were waiting on me. why did you not ab toast me?" ... "when i just clicked
> that ab toast, i would have expected its action when i clicked the toast to open your pup window. i
> believe ab lets you attach those actions now." ... "whenever you need the users attention for a
> password, you should do it that way"
The moment a flow parks on the human, TOAST, and make the toast open the thing that is waiting:
```
adom-bridge notify_user {"title":"pup needs you to sign in once",
"body":"Sign in as [email protected] in the pup window (click here to open it).",
"level":"warning", "focus":{"hwnd": <the pup window's hwnd>}}
```
`focus` is what makes the click useful: `{hwnd}` raises that exact pup window, `{host:true}` reveals the
app Bridge runs under (use it for a Bridge consent, which is a notification inside Hydrogen, not a
window). A toast with no `focus` is the one the user clicks and nothing happens, which is worse than none.
pup does this itself from 2.0.603 on every no-password answer (`google_password_missing`,
`google_verification_required`, `google_password_rejected`, `no_credential`), so an AI that just relays the answer is already covered. Do it yourself too whenever
you are about to sit and wait: the user cannot see your thread.
### Capture works on BOTH lanes (2.0.602, and why it did not)
Capture is what makes "sign in once" true: type a password into any pup window and pup stores it. It lived
ONLY on the CDP lane until 2.0.602, so every login typed into a normal (extension-lane) window was thrown
away and pup asked again next time. John typed his Google password into a pup window, pup kept nothing,
and the explanation "your browser never saved it" was about the wrong thing. If you are on a build older
than 2.0.602, a typed password is NOT stored on the default lane. Verify a capture instead of assuming it:
`pup_credentials {}` must list the host right after the sign-in.
## 1. The vault (how pup remembers logins)
- **Auto-capture:** when the user (or pup) signs into a site, pup grabs the credential and stores it
in the **macOS Keychain**. Passwords are NEVER
echoed back — `pup_credentials` lists host + username only.
- **Autofill:** on a return visit pup fills the saved login in the **background** (no Chrome
dropdown, no foregrounding). Submitting is reason-gated (`pup_login {submit:true, submitReason}`)
and confirms the live domain first (anti-phishing).
- **Registrable-domain matching:** a login saved on `ms.example.com` is offered on `example.com`
and its siblings, like a browser password manager. So the host a credential was saved under does
not have to equal the host of the page you are filling.
- **Durable:** the vault index lives in `~/.adom` (not the bridge cache), so it survives pup updates.
- Store one explicitly: `credential_set {host, username, password}`. List: `pup_credentials {}`.
## 2b. Sync the vault across the user's desktops (a real union, not a push)
The user runs several desktops; each pup learns different logins over time (one signs into Gusto on
the laptop, another into a vendor portal on the office box). `pup_vault_sync {peer, reason}` makes
both complete:
```
adom-bridge-cli pup_vault_sync {"peer":"arav-rog","reason":"both my desktops should have all my logins"}
```
- **Bidirectional.** It pushes THIS box's logins to the peer AND pulls the peer's back, so afterwards
both boxes hold the UNION. A one-way copy would leave each box missing what the other learned —
which is exactly the failure this verb avoids.
- **Newest-wins.** If the same host has different passwords on the two boxes, the more recently
updated one wins on both. Identical entries are skipped.
- **No AI sees a password.** The bytes go pup→ab→peer-ab→peer-pup over the account's cross-desktop
egress. `pup_vault_export` / `pup_vault_import` (the internal halves) are refused for cloud/relay
callers — only same-account desktops sync.
- **One consent.** The initiating box shows one toast ("Sync your saved logins with <peer>?"); the
user clicks Allow. Like every consent verb, waiting on the click is not a timeout — re-call with the
same args to re-attach.
- Run it again whenever a box has learned new logins; it is cheap and idempotent.
## 3. A saved password is STALE
**The tell:** pup fills + submits (`filledUsername/filledPassword/submitted` all true) but the site
says **"Fail to authenticate"** / "incorrect password". The stored secret is out of date, OR the
**username itself is wrong** (the same site can be saved under different usernames).
On a Mac the clean fix is the user: ask them to sign in once in the pup window (toast with the
window attached, see above). pup's capture rail stores the current login, with the right username,
and `pup_login` works from then on. If the user gives you the current details directly, store them
with `credential_set {host, username, password}` and retry. If another of their desktops holds a
fresher copy, `pup_vault_sync {peer, hostFilter}` brings it over. Registrable-domain matching
(section 1) handles a login saved on `ms.<site>` and used on `<site>`.
## Privacy rules (non-negotiable)
- Only ever handle a login for the site the user asked about, on the user's own machine, at their request.
- A password you are given is used once to store it. **Store it and move on — never print it, never
repeat it in a later message, never log it.**
- Prefer, when the user would rather pup never see the plaintext: have them **log in once manually**
(pup's capture rail grabs it fresh), or use a **"Sign in with Google/Microsoft"** SSO path
(`pup-vendor-login`) that types no password at all.
## When NOT to need any of this
Manufacturer-direct sites (TI, ST, ams, …) and datasheets need no login at all — see
`ee-parts-sites.md`. Reach for credential recovery only when a site truly gates what the user wants
behind their own account.
## Read `landed` before you say a word about signing in (pup 2.0.642)
Every `pup_open_window` and `pup_navigate` response carries `landed:{url,title,loginPage,signedIn,_hint}`. The
shared adom-you profile keeps sessions (claude.ai, chatgpt.com, the wiki, vendor portals) for weeks, so most
opens land already signed in. An AI toasted "Sign into claude.ai in the pup window once" on 2026-09-19 while the
window was already on the user's chats. Never do that: read `landed`; `signedIn:true` means say nothing;
`loginPage:true` means try the vault, Google sign-in and the site's recipe in `login-recipes/` first, and toast
the user only when pup itself cannot finish. Recipes: `login-recipes/claude.ai.md`, `login-recipes/chatgpt.com.md`.
## HTTP Basic/Digest auth (the browser's native "Sign in" dialog) is answered from the vault on every lane (2.0.644)
Some downloads (SamacSys / componentsearchengine.com CAD models, vendor file servers) answer with a 401 and
Chrome pops its native "Sign in" dialog, which no page verb can see. pup answers it from the vault on both
lanes now, matched on the registrable domain (a `componentsearchengine.com` entry serves `st.`, `ms.` and
`nxp.componentsearchengine.com`). With no entry, pup CANCELS the challenge so the dialog never blocks the
window, and the next `pup_navigate` / `pup_fetch_url` returns `errorCode: no_credential` naming the host, with
the routes: the user signs in once, `pup_vault_sync {peer, hostFilter}`, or `credential_set
{host:"<domain>", username, password}`. A stored credential the server rejects is tried once, then reported
the same way (stale password: see `login-recipes/recover-a-current-password.md`). `pup_fetch_url` works on
the extension lane too, so a gated file can be fetched as `bodyBase64` without a download dialog.
## Where secrets live
Every password is a macOS Keychain item under the service `adom-bridge-pup` (keyed by host, or
`host|username` for a second account on the same host), indexed by `~/.adom/pup-credentials.index.json`
(host + username only). pup's own browser runs with an in-memory cookie key (`--use-mock-keychain`),
so it never asks the user for Keychain access.