Part 1: The Problem — Why Every Browser Automation Tool is Broken
The Arms Race
Every major website (Reddit, Twitter, LinkedIn, Facebook, Amazon) has invested millions in detecting automated browsers. They check for:
-
navigator.webdriverflag (Playwright/Selenium set this totrue) - Chrome DevTools Protocol fingerprint
- Missing browser plugins (real Chrome has extensions, automated Chrome doesn't)
- Canvas/WebGL fingerprinting differences
- Mouse movement patterns (or lack thereof)
- Request timing patterns (bots are too fast, too regular)
- TLS fingerprinting (headless browsers have different TLS signatures)
- CDP (Chrome DevTools Protocol) artifacts
Tool-by-Tool Breakdown: Why They All Fail
Selenium
- Sets
navigator.webdriver = true— instant detection - Different browser fingerprint from real Chrome
- Launches a fresh profile every time (no cookies, no history = suspicious)
- Detectable through JavaScript:
window.chrome.runtimeis undefined - Many sites block it outright (Cloudflare, Akamai, PerimeterX)
Playwright
- Same
navigator.webdriverproblem - Uses a patched Chromium, not real Chrome — fingerprint differs
- Even with stealth plugins (
playwright-extra,puppeteer-extra-plugin-stealth), sophisticated sites detect it - Reddit specifically blocks Playwright — returns 403 "Blocked" on page load
- Can't use the user's existing cookies/session without complex injection
Puppeteer
- Shares Playwright's problems (both use CDP)
- Headless mode has distinct fingerprint vs headed mode
-
HeadlessChromein User-Agent is a dead giveaway - Even
--headless=new(new headless mode) gets caught by advanced detection
Chrome DevTools Protocol (CDP) Direct
- Requires Chrome launched with
--remote-debugging-port - Chrome REFUSES to enable debugging on the default profile: "DevTools remote debugging requires a non-default data directory"
- Non-default data directory = fresh profile = no cookies = no login
- Copying the profile is complex (multi-GB, encryption issues)
curl / HTTP Client Libraries
- No browser fingerprint at all — easily identified as a script
- No JavaScript execution capability
- Reddit rate-limits aggressively (IP block after a few rapid requests)
- Can't handle JavaScript-rendered content
- Session cookies (HttpOnly) can't be set from outside the browser
Undetected-Chromedriver / Stealth Plugins
- Cat-and-mouse game — patches lag behind detection updates
- Still launches a separate browser instance
- Doesn't have the user's real cookies, extensions, history
- Only delays detection, doesn't prevent it
- Breaks with every Chrome update
The Core Problem
All these tools share the same fundamental flaw: they create a synthetic browser environment. No matter how good the emulation, it's never identical to a real user's browser. Detection systems don't need to find one smoking gun — they correlate dozens of subtle signals.
Part 2: The Discovery — AppleScript is the Cheat Code
What is AppleScript Chrome Control?
macOS has a built-in automation framework called AppleScript. Chrome (and other apps) expose an AppleScript interface that allows external scripts to:
- Navigate tabs to URLs
- Execute JavaScript in any tab
- Read tab properties (title, URL)
- Control windows (open, close, resize)
The critical insight: this JavaScript executes inside the user's actual Chrome process. Not a copy. Not an emulation. The real thing.
Why This is Fundamentally Different
Traditional Automation:
Script → Launches new browser → Controls it → Website detects fake browser
AppleScript Automation:
Script → Talks to EXISTING Chrome → Chrome does the work → Website sees real user
The website cannot distinguish AppleScript-triggered JavaScript from user-triggered JavaScript because there is no difference at the browser level. The JavaScript runs in the exact same V8 context, with the exact same cookies, the same extensions, the same fingerprint. Chrome doesn't know or care that the command came from AppleScript instead of a keyboard.
What This Means
| Property | The website sees... |
|---|---|
| Browser fingerprint | The user's real Chrome (with all extensions, plugins) |
| Cookies | The user's real cookies (including HttpOnly ones!) |
| TLS fingerprint | Chrome's real TLS stack |
| navigator.webdriver |
false (not automation) |
| Login session | Already authenticated |
| IP address | The user's real IP (same as their normal browsing) |
| Canvas/WebGL | Real GPU rendering |
There is literally nothing to detect. The request IS from a real browser. Because it IS a real browser.
Part 3: The Full Technical Guide
3.1 One-Time Setup
Enable JavaScript from Apple Events
Chrome menu → View → Developer → Allow JavaScript from Apple Events ✓
Then restart Chrome (Cmd+Q, reopen).
This is a security feature — Chrome requires explicit opt-in before allowing external JavaScript execution. It persists across restarts.
Verify It Works
osascript -e 'tell application "Google Chrome" to tell active tab of first window to execute javascript "document.title"'
Should return the current tab's title. If you get an error about "JavaScript through AppleScript is turned off", restart Chrome.
3.2 Three Core Operations
Everything you can do reduces to three operations:
Operation 1: Navigate
osascript -e '
tell application "Google Chrome"
tell active tab of first window
set URL to "https://www.reddit.com"
end tell
end tell'
Operation 2: Execute JavaScript
osascript -e '
tell application "Google Chrome"
tell active tab of first window
execute javascript "1 + 1"
end tell
end tell'
# Returns: 2
Operation 3: Read Tab Properties
osascript -e '
tell application "Google Chrome"
return title of active tab of first window
end tell'
3.3 The document.title Trick — Getting Complex Data Out
AppleScript's execute javascript returns the result of the expression. But for async operations (like fetch()), we need a workaround:
- Run async JS that writes the result to
document.title - Wait for it to complete
- Read
document.title
# Step 1: Execute async JS, store result in title
osascript -e '
tell application "Google Chrome"
tell active tab of first window
execute javascript "fetch(\"/api/me.json\", {credentials: \"include\"}).then(r => r.json()).then(d => { document.title = \"RESULT:\" + JSON.stringify(d) })"
end tell
end tell'
# Step 2: Wait
sleep 2
# Step 3: Read result
osascript -e '
tell application "Google Chrome"
return title of active tab of first window
end tell'
# Returns: RESULT:{"data":{"name":"myuser","karma":123,...}}
Why document.title? It's the simplest channel to pass data from the browser JS context back to the shell. The title has a practical limit of a few KB, which is enough for most API responses.
3.4 Making Authenticated API Calls
Since the browser is already logged in, we can use fetch() with credentials: "include" to make API calls that automatically include all cookies — even HttpOnly ones that JavaScript can't read directly.
// This runs inside the logged-in Chrome — cookies are sent automatically
fetch("/api/endpoint.json", {
credentials: "include" // This is the magic — sends all cookies
})
For POST requests (posting, commenting, voting), most sites require a CSRF token:
// Step 1: Get CSRF/modhash token
let me = await fetch("/api/me.json", {credentials: "include"}).then(r => r.json());
let csrf = me.data.modhash; // Reddit calls it "modhash"
// Step 2: POST with the token
await fetch("/api/comment", {
method: "POST",
credentials: "include",
headers: {"Content-Type": "application/x-www-form-urlencoded"},
body: `thing_id=t3_abc123&text=Hello&uh=${csrf}&api_type=json`
});
3.5 JXA (JavaScript for Automation) — Avoiding Escaping Hell
AppleScript has painful string escaping (nested quotes are a nightmare). JXA is macOS's JavaScript-based alternative that solves this:
osascript -l JavaScript -e '
var chrome = Application("Google Chrome");
var tab = chrome.windows[0].activeTab;
tab.execute({javascript: "(" + function() {
// Write normal JavaScript here — no escaping needed!
var data = {hello: "world"};
document.title = JSON.stringify(data);
} + ")();"});
'
The trick: define a JS function, .toString() it (via + concatenation), and pass that string to execute. This means you write clean JavaScript without worrying about AppleScript string escaping.
3.6 Targeting Specific Tabs
# Active tab of first window (most common)
tell active tab of first window
# Specific tab by index
tell tab 3 of first window
# Specific window
tell active tab of window 2
# Find tab by URL
tell application "Google Chrome"
repeat with t in tabs of first window
if URL of t contains "reddit.com" then
execute javascript "document.title" in t
end if
end repeat
end tell
3.7 Opening New Tabs
osascript -e '
tell application "Google Chrome"
tell first window
make new tab with properties {URL:"https://www.reddit.com"}
end tell
end tell'
Part 4: Our Reddit Automation — The Full Story
The Journey (What We Tried)
We needed to automate Reddit posting and commenting. Here's every approach we tried, in order:
Attempt 1: Playwright MCP Browser
- Opened a new Playwright browser
- Navigated to reddit.com
- Result: Reddit returned 403 "Blocked" — detected automation immediately
- Even with injected cookies via
context.addCookies(), Reddit still blocked the page
Attempt 2: Cookie Injection
- User exported all cookies (including HttpOnly) from Cookie Editor extension
- Injected 14 cookies into Playwright via
context.addCookies() - Navigated to reddit.com
- Result: Still 403 "Blocked" — cookie injection doesn't fix the browser fingerprint
Attempt 3: Chrome DevTools MCP
- Launched Chrome with
--remote-debugging-port=9222 - Chrome error: "DevTools remote debugging requires a non-default data directory"
- Used
--user-data-dir=/tmp/chrome-debug→ works but fresh profile, no cookies - Could navigate to Reddit (not blocked!) but couldn't log in
- Tried injecting HttpOnly cookies via
document.cookie— can't set HttpOnly from JS - Result: Dead end — can't authenticate
Attempt 4: Reddit API via curl
- Used
token_v2(JWT from browser cookies) as Bearer token - First call: HTTP 200 — it works!
- Second call: HTTP 200 (features only, no user data)
- Third call: 403 "Blocked" — IP rate-limited
- Result: Token works but IP gets blocked after a few rapid requests
Attempt 5: AppleScript Chrome Control (THE WIN)
- Remembered macOS can control Chrome via AppleScript
- One prerequisite: enable "Allow JavaScript from Apple Events"
- Needed Chrome restart after enabling
- First test:
execute javascript "document.title"→ works! - Navigate to reddit.com → not blocked (it's the real Chrome)
-
fetch("/api/me.json")→ returns user data (already logged in!) - Post comment via
fetch("/api/comment")→ comment posted successfully - Result: Full automation with zero detection
The Working Pipeline
Step 1: Navigate to reddit.com (if not already there)
osascript → Chrome → set URL to reddit.com
Step 2: Scan for opportunities
osascript → Chrome → fetch("/r/SideProject/rising.json")
→ Filter: score > 2, comments < 15
Step 3: Generate comment
Claude Code / GPT-5.2 → draft value-first comment
Step 4: Human review
Show comment to user → user approves
Step 5: Get CSRF token (modhash)
osascript → Chrome → fetch("/api/me.json") → extract modhash
Step 6: Post comment
osascript → Chrome → fetch("/api/comment", {method: "POST", body: ...})
Step 7: Verify
Read API response → confirm comment ID and permalink
Actual Commands Used
# Check login status
osascript -e '..execute javascript "fetch(\"/api/me.json\",{credentials:\"include\"}).then(r=>r.json()).then(d=>{document.title=\"USER:\"+JSON.stringify({name:d.data.name,karma:d.data.total_karma})})"'
# → USER:{"name":"BP041","karma":35}
# Scan rising posts
osascript -e '..execute javascript "fetch(\"/r/SideProject/rising.json?limit=5\",{credentials:\"include\"}).then(r=>r.json()).then(d=>{let posts=d.data.children.map(c=>({t:c.data.title.substring(0,60),s:c.data.score,c:c.data.num_comments,id:c.data.name}));document.title=\"POSTS:\"+JSON.stringify(posts)})"'
# Get modhash
osascript -e '..execute javascript "fetch(\"/api/me.json\",{credentials:\"include\"}).then(r=>r.json()).then(d=>{document.title=\"MH:\"+d.data.modhash})"'
# Post comment (using JXA to avoid escaping issues)
osascript -l JavaScript -e '
var chrome = Application("Google Chrome");
var tab = chrome.windows[0].activeTab;
tab.execute({javascript: "(" + function() {
var body = new URLSearchParams({
thing_id: "t3_1qzoq6p",
text: "Your comment here",
uh: "modhash_here",
api_type: "json"
});
fetch("/api/comment", {
method: "POST",
credentials: "include",
headers: {"Content-Type": "application/x-www-form-urlencoded"},
body: body.toString()
}).then(r=>r.json()).then(d=>{document.title="POSTED:"+JSON.stringify(d)});
} + ")();"});
'
Part 5: Beyond Reddit — This Works Everywhere
The Pattern is Universal
Any website that you can use in Chrome, you can automate with this method:
| Platform | What you can automate |
|---|---|
| Post, comment, upvote, browse, subscribe | |
| Twitter/X | Tweet, reply, like, retweet, DM |
| Post, comment, connect, message | |
| Like, comment (web version) | |
| GitHub | Create issues, PRs, review code |
| Any SaaS | Any action you can do in the browser |
The Same Three Steps Apply
- Navigate to the site (or verify you're already there)
-
Execute fetch() calls using
credentials: "include" -
Read results via
document.title
For Sites with Complex SPAs
Some sites (Twitter, LinkedIn) are heavy SPAs where the API isn't as clean as Reddit's. For these, you can also use DOM manipulation:
// Click a button
document.querySelector('[data-testid="tweetButton"]').click()
// Fill an input
document.querySelector('[data-testid="tweetTextarea"]').innerText = "Hello world"
// Trigger React's change event
let el = document.querySelector('input');
let ev = new Event('input', {bubbles: true});
el.value = 'new value';
el.dispatchEvent(ev);
Part 6: Chrome Multi-Profile Bug & The System Events Fallback
The Problem
Chrome on macOS supports multiple user profiles (e.g. "Haoyang", "Work", "Personal"). When a user has multiple profiles, AppleScript's tell application "Google Chrome" can only see windows from ONE profile — and it's not always the one you need.
Symptoms:
osascript -e 'tell application "Google Chrome" to return count of windows'
# Returns: 0
# But the user clearly has Chrome windows open!
The user sees multiple Chrome windows with tabs, logged into various sites. But AppleScript reports 0 windows. This happens because Chrome's AppleScript interface only exposes windows from one profile context.
The Discovery
While tell application "Google Chrome" is blind, System Events can see ALL Chrome windows regardless of profile:
osascript -e '
tell application "System Events"
tell process "Google Chrome"
return name of window 1
end tell
end tell'
# Returns: "Reddit - The heart of the internet - Google Chrome - Haoyang"
# ^^^^ System Events sees it!
The Solution: Console + Clipboard
Since we can't use execute javascript on an invisible-to-AppleScript window, we use a keyboard-based approach:
-
Copy JavaScript code to clipboard via
pbcopy -
Open Chrome Console via keyboard shortcut
Cmd+Option+J -
Select all in console
Cmd+A(clear previous) -
Paste from clipboard
Cmd+V -
Execute by pressing
Enter -
Close console
Cmd+Option+J -
Read
document.titlevia System Events
Implementation
# Step 1: Copy JS to clipboard (use python3 to avoid shell escaping)
python3 -c "
import subprocess
js = '''(async()=>{
let r = await fetch('/api/me.json', {credentials: 'include'});
let d = await r.json();
document.title = 'USER:' + JSON.stringify({name: d.data.name, karma: d.data.total_karma});
})()'''
subprocess.run(['pbcopy'], input=js.encode(), check=True)
"
# Step 2: Open console, paste, execute, close console
osascript -e '
tell application "System Events"
tell process "Google Chrome"
set frontmost to true
delay 0.3
-- Open JS console (Cmd+Option+J)
key code 38 using {command down, option down}
delay 1
-- Select all (clear previous code)
keystroke "a" using {command down}
delay 0.2
-- Paste JS from clipboard
keystroke "v" using {command down}
delay 0.5
-- Press Enter to execute
key code 36
delay 0.3
-- Close console
key code 38 using {command down, option down}
end tell
end tell'
# Step 3: Wait and read result from window title
sleep 3
osascript -e '
tell application "System Events"
tell process "Google Chrome"
return name of window 1
end tell
end tell'
# Returns: "USER:{"name":"BP041","karma":37} - Google Chrome - Haoyang"
POST Example (Posting a Reddit Comment)
python3 -c "
import subprocess
js = '''(async()=>{
let me = await fetch('/api/me.json', {credentials: 'include'}).then(r=>r.json());
let uh = me.data.modhash;
let body = new URLSearchParams({
thing_id: 't3_1qzhcyg',
text: 'Your comment text here',
uh: uh,
api_type: 'json'
});
let r = await fetch('/api/comment', {
method: 'POST',
credentials: 'include',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: body.toString()
});
let d = await r.json();
let ok = d.json && d.json.errors.length === 0;
document.title = 'RESULT:' + (ok ? 'SUCCESS' : 'FAIL:' + JSON.stringify(d.json.errors));
})()'''
subprocess.run(['pbcopy'], input=js.encode(), check=True)
"
osascript -e '
tell application "System Events"
tell process "Google Chrome"
set frontmost to true
delay 0.3
key code 38 using {command down, option down}
delay 1
keystroke "a" using {command down}
delay 0.2
keystroke "v" using {command down}
delay 0.5
key code 36
delay 0.3
key code 38 using {command down, option down}
end tell
end tell'
sleep 5
osascript -e '
tell application "System Events"
tell process "Google Chrome"
return name of window 1
end tell
end tell'
# Returns: "RESULT:SUCCESS - Google Chrome - Haoyang"
Auto-Detection: Which Method to Use
#!/bin/bash
# Detect if Method 1 or Method 2 is needed
WINDOWS=$(osascript -e 'tell application "Google Chrome" to return count of windows' 2>/dev/null)
if [ "$WINDOWS" != "0" ] && [ -n "$WINDOWS" ]; then
echo "Method 1: AppleScript execute javascript (direct, fast)"
else
# Check if System Events can see Chrome windows
SE_WINDOW=$(osascript -e 'tell application "System Events" to tell process "Google Chrome" to return name of window 1' 2>/dev/null)
if [ -n "$SE_WINDOW" ]; then
echo "Method 2: System Events + Console + Clipboard (multi-profile fallback)"
else
echo "No Chrome windows found at all"
fi
fi
Why This Happens
Chrome's AppleScript dictionary (Google Chrome.sdef) exposes windows per-profile. When Chrome starts, it associates AppleScript access with one profile context. Windows from other profiles exist at the macOS level (visible to System Events, Accessibility API, and the user) but are invisible to tell application "Google Chrome".
This is likely a Chrome bug, not a feature. But the System Events fallback is reliable and battle-tested.
Method Comparison
| Method 1: execute javascript | Method 2: System Events + Console | |
|---|---|---|
| Speed | Fast (~2s per call) | Slower (~5s per call) |
| Reliability | High (when it works) | High (always works) |
| Multi-profile | Breaks | Works |
| Escaping | Painful (use JXA) | Easy (use python3 + pbcopy) |
| Read results | title of active tab |
name of window 1 (includes " - Google Chrome - Profile") |
| Prerequisites | Allow JS from Apple Events | Allow JS from Apple Events + Console access |
Part 7: Limitations and Gotchas
Known Limitations
| Limitation | Workaround |
|---|---|
| macOS only | No workaround — AppleScript is Apple-only |
| Chrome must be running | Script can launch Chrome: open -a "Google Chrome"
|
| Must be logged in already | Log in manually once, session persists |
| document.title size limit (~few KB) | Chunk large responses, or use multiple calls |
| Async results need sleep/polling |
sleep 2 between execute and read |
| AppleScript string escaping is painful | Use JXA (JavaScript for Automation) instead |
| One command at a time per tab | Use multiple tabs for parallelism |
Security Considerations
- "Allow JavaScript from Apple Events" means ANY AppleScript can execute JS in your Chrome — be aware of this if you run untrusted scripts
- Your browser cookies are used directly — treat this script with the same security as your browser
- Don't hardcode sensitive data in scripts — use environment variables
Rate Limiting Best Practices
- Wait 2+ seconds between API calls
- Don't post more than 5 comments per session
- Space out sessions (don't run hourly)
- Monitor for 429/403 responses and back off
Part 8: Quick Reference
Cheat Sheet
# Is Chrome running?
pgrep -x "Google Chrome" && echo "Running" || echo "Not running"
# Launch Chrome
open -a "Google Chrome"
# Quit Chrome
osascript -e 'tell application "Google Chrome" to quit'
# Get current URL
osascript -e 'tell application "Google Chrome" to return URL of active tab of first window'
# Get page title
osascript -e 'tell application "Google Chrome" to return title of active tab of first window'
# Navigate
osascript -e 'tell application "Google Chrome" to tell active tab of first window to set URL to "https://example.com"'
# Execute JS (sync)
osascript -e 'tell application "Google Chrome" to tell active tab of first window to execute javascript "1+1"'
# Execute JS (async) — write to title, then read
osascript -e 'tell application "Google Chrome" to tell active tab of first window to execute javascript "fetch(\"/api/data\").then(r=>r.json()).then(d=>{document.title=JSON.stringify(d)})"'
sleep 2
osascript -e 'tell application "Google Chrome" to return title of active tab of first window'
# New tab
osascript -e 'tell application "Google Chrome" to tell first window to make new tab with properties {URL:"https://example.com"}'
# Count tabs
osascript -e 'tell application "Google Chrome" to return count of tabs of first window'
# JXA (for complex JS without escaping pain)
osascript -l JavaScript -e '
var chrome = Application("Google Chrome");
var tab = chrome.windows[0].activeTab;
tab.execute({javascript: "document.title"});
'
The Golden Rule
If you can do it in Chrome DevTools Console, you can automate it with AppleScript. Same context, same cookies, same permissions. The website literally cannot tell the difference.
Discovered on 2026-02-09 while trying to automate Reddit posting. After Playwright, Selenium, Chrome DevTools Protocol, and curl all failed to bypass Reddit's anti-bot detection, AppleScript Chrome control worked on the first try — because it doesn't try to fake a browser. It IS the browser.
Top comments (0)