Simple browser automation via CDP
A personal project by an enthusiast — contributions welcome!
⬇ Download for macOS (Apple Silicon) ⬇ Download for macOS (Intel) ⬇ Download for Linux
easybrawto is a small CLI tool that lets you automate any website using a simple .auto script.
chrome.persistProfile('my_profile')
functions searchWikipedia {
.navigate('https://en.wikipedia.org')
.waitLoad()
.insertText('search', 'Crystal language')
.clickButton('Search')
.waitLoad()
.screenshot('result.png')
.log('Done!')
}
run searchWikipedia
./easybrawto run script.autoNo Selenium. No WebDriver. No Python. No npm install.
Just a binary and a .auto file.
This is a personal project. I'm not a professional developer — I built this because I needed browser automation that wouldn't get blocked by websites, and I wanted something dead simple to write and read.
It talks directly to Chrome via the Chrome DevTools Protocol (CDP) — the same internal protocol the browser's DevTools uses. Real browser, real profile, no injected drivers.
It's rough around the edges. There are probably bugs. But it works for most common tasks, and the script format is simple enough that an AI agent can write it for you.
If you find it useful, great. If you find bugs or want to contribute, even better — I'm still learning Crystal and open to help.
1. Download the binary for your platform above
2. Make it executable:
chmod +x easybrawto3. Write a script — save as script.auto:
chrome.persistProfile('my_profile')
functions openSite {
.navigate('https://example.com')
.waitLoad()
.screenshot('result.png')
.log('Done!')
}
run openSite
4. Run:
./easybrawto run script.autoRequires Chrome, Brave, or Edge installed on your machine.
Four test sites are live and ready — no setup needed. Each one has three sections that require scrolling, a cookie popup, and a newsletter modal that appears mid-scroll.
| Level | Stack | URL |
|---|---|---|
| Level 1 | Plain HTML + CSS + JS | easybrawto-level1.pages.dev |
| Level 2 | Tailwind CSS | easybrawto-level2.pages.dev |
| Level 3 | React (hooks, controlled inputs) | easybrawto-level3.pages.dev |
| Level 4 | Next.js style (hashed classes, data-attributes) | easybrawto-level4.pages.dev |
The matching .auto scripts are in test-scripts/scripts/. Point them at the live URLs and run:
./easybrawto run test-scripts/scripts/level1.autoThe four levels are progressively harder for automation tools — but the .auto scripts stay nearly identical across all of them. That's the point.
Level 1 — plain HTML, direct IDs and names. The easiest baseline. Level 2 — Tailwind CSS, no IDs on most elements. Forces use of
nameandaria-label. Level 3 — React with controlled inputs and async rendering. Tests framework compatibility. Level 4 — Next.js style with hashed CSS classes anddata-attributes. Closest to real production sites.
The sites are also in test-scripts/ if you want to run them locally:
cd test-scripts && python3 -m http.server 8080Play with them. Break them. Modify the scripts. It's the best way to understand what easybrawto can and can't do.
Place at the top of the script, before any functions.
# Persistent profile — saves cookies, logins, sessions between runs
# First run creates the profile. Next runs reuse it.
chrome.persistProfile('profile_name')
# Use a specific profile from your browser installation
chrome.profile('/Users/you/Library/Application Support/Google/Chrome', 'Profile 3')
# Clean temporary profile — deleted after the script ends (default)
chrome.tempProfile()
# Choose browser — default is chrome
chrome.browser('brave')
chrome.browser('edge')
| Command | What it does |
|---|---|
.navigate('url') |
Go to a URL |
.waitLoad() |
Wait for the page to finish loading |
.waitFor('selector') |
Wait for a specific element to appear |
.waitForText('text') |
Wait for a specific text to appear anywhere on the page |
.waitSeconds(n) |
Wait a fixed number of seconds |
.insertText('selector', 'text') |
Type into a field — works with React, Vue, Angular |
.clearField('selector') |
Clear a field before typing |
.clickButton('text or selector') |
Click a button, link, or element |
.clickIfExists('text or selector') |
Click only if the element exists — never fails |
.pressKey('key') |
Press Enter, Tab, Escape... |
.selectOption('selector', 'value') |
Select a dropdown option by visible text |
.checkBox('selector') |
Check a checkbox |
.scroll('direction', amount) |
Scroll the page — down, up, top, bottom |
.reload() |
Reload the current page |
.goBack() |
Navigate back in history |
.goForward() |
Navigate forward in history |
.getValue('selector') |
Read the current value of an input field |
.getAttribute('selector', 'attr') |
Read any HTML attribute of an element |
.runJS('code') |
Run arbitrary JavaScript on the page |
.screenshot('file.png') |
Save a screenshot |
.log('message') |
Print a message in the terminal |
| What you write | What it matches |
|---|---|
'Sign in' |
Button or link with that visible text |
'search' |
Input field with name="search" |
'#email' |
Element with id="email" |
'.submit-btn' |
Element with class="submit-btn" |
'Enter your email' |
Input with that placeholder |
'Send message' |
Input with that aria-label |
The selector cascade tries multiple strategies automatically — text, name, id, class, placeholder, aria-label — so scripts stay readable without inspecting the DOM for the perfect CSS selector.
functions login {
.navigate('https://site.com/login')
.waitLoad()
.insertText('#email', 'user@mail.com')
.insertText('#password', 'password')
.clickButton('Sign in')
.waitLoad()
}
functions doWork {
.navigate('https://site.com/dashboard')
.screenshot('dashboard.png')
}
run login
run doWork
chrome.persistProfile('work')
functions login {
.navigate('https://site.com/login')
.waitLoad()
.insertText('#email', 'user@mail.com')
.insertText('#password', 'password')
.clickButton('Sign in')
.waitLoad()
.log('Logged in!')
}
run login
Run once to log in. Next time it opens already logged in.
chrome.persistProfile('automation')
functions fillForm {
.navigate('https://site.com/contact')
.waitLoad()
.waitSeconds(2)
.clickIfExists('Accept cookies')
.insertText('nome', 'John Doe')
.insertText('email', 'john@mail.com')
.selectOption('assunto', 'Support')
.insertText('mensagem', 'Hello from easybrawto!')
.checkBox('#terms')
.clickButton('Send')
.waitForText('Message sent')
.screenshot('sent.png')
}
run fillForm
chrome.persistProfile('my_profile')
functions browse {
.navigate('https://site.com')
.waitLoad()
.waitSeconds(3)
.clickIfExists('Accept cookies')
.scroll('down', 500)
.waitSeconds(2)
.clickIfExists('No thanks')
.scroll('bottom')
.screenshot('result.png')
}
run browse
chrome.persistProfile('my_profile')
functions readData {
.navigate('https://site.com/profile')
.waitLoad()
.getValue('#username')
.getAttribute('.profile-link', 'href')
.screenshot('profile.png')
}
run readData
chrome.persistProfile('my_profile')
functions customJs {
.navigate('https://site.com')
.waitLoad()
.runJS('document.querySelector(".cookie-banner").remove()')
.screenshot('clean.png')
}
run customJs
chrome.persistProfile('my_profile')
functions history {
.navigate('chrome://history')
.waitLoad()
.screenshot('history.png')
}
run history
Requires Crystal.
git clone https://github.com/Saimonsanbr/easybrawto
cd easybrawto
crystal build src/main.cr -o easybrawto --release- Reads the
.autoscript and parses functions and commands - Launches Chrome/Brave/Edge with
--remote-debugging-port=9222 - Connects via WebSocket to the Chrome DevTools Protocol
- Sends CDP commands and JavaScript directly to the open tab
- Each command runs in sequence, waiting for the browser before continuing
No WebDriver. No browser extension. No persistent injected scripts.
Working:
navigate,waitLoad,waitFor,waitForText,waitSecondsinsertText— compatible with React, Vue, Angular, Shadow DOMclearField,clickButton,clickIfExists,pressKeyselectOption— by visible text or valuecheckBoxscroll— down, up, top, bottomreload,goBack,goForwardgetValue,getAttributerunJS— arbitrary JavaScript escape hatchscreenshot,log- Persistent, temporary, and system profiles
- Chrome, Brave, Edge on macOS
Known limitations (maybe you can help me):
- No Windows binaries yet
waitLoadcan be slow on heavy SPAs — preferwaitForwhen possible- No variables or conditionals in scripts yet
Coming nex if god allows:
watch()— background observers that react to popups automaticallyrules{}block — global script behavior configuration- Variables in scripts
- Windows support
Issues and PRs are welcome. I'm not an expert in Crystal — if you see something wrong or have a better approach, please open an issue or send a PR. This project exists because I needed it, and it'll get better with help.
MIT