| name | safari-browser |
| description | macOS native browser automation via Safari + AppleScript. Use when the user needs to automate websites that require login (Plaud, Elementor, banking, social media) — Safari preserves localStorage and cookies permanently. Also use when agent-browser fails due to session/auth issues, or when the user explicitly asks to use Safari. Triggers on: "login to site", "automate with Safari", "use Safari", "session expired with agent-browser", "Plaud upload", or any website automation where persistent auth is needed. |
| allowed-tools | ["Bash(safari-browser:*)","Bash(safari-browser *)"] |
Safari Browser Automation
macOS native browser automation CLI using Safari + AppleScript. Core advantage: permanent login — uses the user's existing Safari session directly.
When to Use safari-browser vs agent-browser
| Condition | Use |
|---|
| Website requires login (Plaud, Elementor, banking, etc.) | safari-browser |
| Need localStorage/cookies from existing session | safari-browser |
agent-browser state save/load fails | safari-browser |
Vue/React sites where fill doesn't trigger reactivity | safari-browser (JS events dispatched) |
| Headless / CI / public sites | agent-browser |
| Cross-platform needed | agent-browser |
| Sites that block headless browsers (banks, social media) | safari-browser (real browser, no bot detection) |
| AI + human collaboration (user watches and takes over) | safari-browser (shared Safari window) |
| Need CDP accessibility tree | agent-browser (safari-browser has JS-based snapshot) |
Core Workflow
safari-browser open "https://example.com"
safari-browser snapshot
safari-browser snapshot -c
safari-browser snapshot --json
safari-browser click @e2
safari-browser fill @e1 "user@example.com"
safari-browser press Enter
safari-browser get url
safari-browser get text "h1"
safari-browser wait --url "dashboard"
safari-browser wait --js "document.querySelector('.loaded')"
Command Reference
Navigation
safari-browser open <url> [--new-tab] [--new-window]
safari-browser back
safari-browser forward
safari-browser reload
safari-browser close
Element Interaction
safari-browser click <selector>
safari-browser dblclick <selector>
safari-browser fill <selector> <text>
safari-browser type <selector> <text>
safari-browser select <selector> <value>
safari-browser hover <selector>
safari-browser focus <selector>
safari-browser check <selector>
safari-browser uncheck <selector>
safari-browser scroll <dir> [pixels]
safari-browser scrollintoview <selector>
safari-browser highlight <selector>
safari-browser drag <src> <dst>
Keyboard
safari-browser press Enter
safari-browser press Tab
safari-browser press Escape
safari-browser press Control+a
safari-browser press Shift+Tab
Find Elements (by text/role/label/placeholder)
safari-browser find text "Submit" click
safari-browser find role "button" click
safari-browser find label "Email" fill "user@example.com"
safari-browser find placeholder "Search" fill "query"
JavaScript
safari-browser js "document.title"
safari-browser js --file script.js
safari-browser js "JSON.stringify(localStorage)"
safari-browser js --large "document.body.innerText"
safari-browser js --output /tmp/page.txt "document.body.innerText"
Large output handling: Safari's do JavaScript silently drops results >~1MB. Use --large to force chunked read, or --output to write to file. The CLI auto-retries with chunked read when it detects empty output.
Page & Element Info
safari-browser get url
safari-browser get title
safari-browser get text [selector]
safari-browser get source
safari-browser get html <selector>
safari-browser get value <selector>
safari-browser get attr <selector> <name>
safari-browser get count <selector>
safari-browser get box <selector>
State Checks
safari-browser is visible <selector>
safari-browser is exists <selector>
safari-browser is enabled <selector>
safari-browser is checked <selector>
Snapshot (Element Discovery + Page State)
safari-browser snapshot
safari-browser snapshot -c
safari-browser snapshot -d 3
safari-browser snapshot --page
safari-browser snapshot --page --json
safari-browser snapshot --page -s "main"
safari-browser snapshot -s "form.login"
safari-browser snapshot --json
All selector-accepting commands support @eN refs from the last snapshot.
Screenshot, PDF & Upload
safari-browser screenshot [path]
safari-browser screenshot --full path
safari-browser pdf --allow-hid [path]
safari-browser upload <selector> <file>
safari-browser upload --js <sel> <file>
safari-browser upload --native <sel> <file>
Upload behavior: With Accessibility permission granted, upload uses the native file dialog by default (clipboard paste for path input — fast, supports all characters including CJK and spaces). Without permission, it automatically falls back to JS DataTransfer injection and prints a hint to stderr. Use --js to force JS mode, or --native/--allow-hid to force native mode (backward compatible).
PDF export: Uses clipboard paste for path input (instead of keystroke), precise waits (repeat until exists) instead of blind delays, and AXDefault button click (locale-independent) for the Save button. Still requires --allow-hid.
Tab Management
safari-browser tabs
safari-browser tabs --json
safari-browser tab <n>
safari-browser tab new
Wait
safari-browser wait <ms>
safari-browser wait --url <pattern>
safari-browser wait --js <expr>
safari-browser wait --timeout <ms>
Storage
safari-browser cookies get [name]
safari-browser cookies get --json
safari-browser cookies set <name> <value>
safari-browser cookies clear
safari-browser storage local get <key>
safari-browser storage local set <key> <value>
safari-browser storage local remove <key>
safari-browser storage local clear
safari-browser storage session get/set/remove/clear
safari-browser storage local get token --url plaud
safari-browser storage local get token --url oauth
Multi-Window Targeting (#17 #18 #21 #23)
When Safari has more than one window open, every command that reads from
or drives a document accepts global targeting flags:
safari-browser documents
safari-browser get url --url plaud
safari-browser get title --window 2
safari-browser snapshot --url plaud
safari-browser wait --for-url "/dashboard" --url plaud
safari-browser screenshot --window 2 out.png
safari-browser pdf --window 2 --allow-hid out.pdf
safari-browser upload --native "input[type=file]" file.mp3 --window 2
Reusing a lock flag across commands — use a bash array, not a string.
To lock several commands to the same document, store the flag in an array so
it word-splits in both shells. A plain string + unquoted $LOCK breaks under zsh
(Claude Code's Bash runs zsh), which does NOT word-split unquoted variables —
--url plaud is then passed as one token and the CLI errors Unknown option '--url plaud':
LOCK=(--url plaud)
safari-browser snapshot "${LOCK[@]}"
safari-browser get url "${LOCK[@]}"
Default screenshot behavior (#23 R7): when AX permission is granted, default screenshot (no flag) uses AX SPI for reliable identity (no title/bounds heuristic). Without AX permission, falls back to legacy CG name match.
screenshot --window N (#23 R6 C1): requires Accessibility permission. Uses _AXUIElementGetWindow private SPI for reliable AS↔CG mapping. Eliminates the wrong-window failure modes that bedevil bounds-/title-based matching. Strict fail-closed: throws windowIdentityAmbiguous when multiple visible windows can't be uniquely identified, instead of silently picking one.
Wait command rename (#23 BREAKING): wait --url <pattern> → wait --for-url <pattern>. The old --url flag is now a global targeting flag.
Settings
safari-browser set media dark
safari-browser set media light
Debug
safari-browser console --start
safari-browser console
safari-browser console --clear
safari-browser errors --start
safari-browser errors
safari-browser mouse move <x> <y>
safari-browser mouse down / up / wheel <dy>
Common Patterns
Login-Required Site (e.g., Plaud)
safari-browser open "https://web.plaud.ai"
safari-browser wait --js "!document.querySelector('.login-form')"
TOKEN=$(safari-browser js "localStorage.getItem('tokenstr')")
curl -H "Authorization: $TOKEN" https://api.example.com/data
Form Submission
safari-browser open "https://example.com/form"
safari-browser fill "input#name" "John"
safari-browser fill "input#email" "john@example.com"
safari-browser select "select#country" "TW"
safari-browser check "input#agree"
safari-browser click "button[type='submit']"
safari-browser wait --url "success"
File Upload
safari-browser open "https://example.com/upload"
safari-browser upload "input[type='file']" "/path/to/document.pdf"
safari-browser upload --js "input[type='file']" "/path/to/document.pdf"
safari-browser upload --native "input[type='file']" "/path/to/document.pdf"
safari-browser wait 5000
Human-like Delays (Anti-Bot)
When automating sites that detect bots (banks, hospital EMR, social media), use Cauchy-distributed random delays instead of fixed sleep:
sleep $(python3 -c "import random, math; print(max(2, round(3 + math.tan(math.pi * (random.random() - 0.5)) * 0.8, 1)))")
Use between every safari-browser command when operating sensitive sites. Never use fixed intervals — that's how bots get detected.
Troubleshooting
- Binary killed (exit 137 / SIGKILL) — macOS Sequoia kills unsigned binaries that call osascript. Fix:
codesign --force --sign - ~/bin/safari-browser. This is done automatically by make install.
- Large JS output returns empty — Safari
do JavaScript silently drops results >~1MB. Use safari-browser js --large "..." or --output /tmp/result.txt for chunked read.
Limitations
- macOS only — requires Safari + AppleScript
- Not headless — Safari always has a visible window
- JS returns strings only — use
JSON.stringify() for objects
- JS output ~1MB limit — use
--large flag for bigger results (auto-retries with chunked read)
- No network interception — Safari has no API for this
- JS-based snapshot —
snapshot uses DOM scanning (not CDP accessibility tree), 90% as effective
- Automation permission required — first run prompts for Terminal → Safari access (System Settings → Privacy & Security → Automation)
- upload smart default — uses native file dialog when Accessibility permission is granted; auto-falls back to JS DataTransfer otherwise. Force with
--js or --native
- pdf requires Accessibility permission — System Settings → Privacy & Security → Accessibility (uses clipboard paste + AXDefault button click)
Playbooks
Site-specific operation guides live as sibling skills named safari-<site>-<action>/SKILL.md — Claude Code auto-surfaces them by description when the user's intent matches. Seeds currently shipping: safari-plaud-upload, safari-github-star. To add a personal playbook (private site, custom flow), drop a SKILL.md at ~/.claude/skills/safari-<site>-<action>/SKILL.md using the same convention; Claude loads user-local skills natively and no custom precedence is added by this plugin. Contribution guide: plugins/safari-browser/skills/CONTRIBUTING-PLAYBOOKS.md. Authoritative spec: openspec/specs/playbook-skills/spec.md in the safari-browser repo.