Trust & safety
The 11 rules the bot can't break.
Every guardrail is enforced in code, not in a policy doc. Open the repo and grep — you'll find the exact line that stops the bot from misbehaving.
- 1
Never sends without your one-click approval
By default (DRAFT_MODE=true), every drafted email lands in a local review queue. The bot does not send a single byte to Gmail until you click 'Send' in the Review Queue UI at localhost:7868.
Why it matters: Eliminates 'the bot sent something I didn't approve' incidents. You are always the operator of every outbound message from your inbox.
Enforced in:
core/email_sender.py · send_application() · DRAFT_MODE check - 2
Never sends to an unverified address
Every recipient candidate goes through MX-record DNS validation. Addresses that don't resolve to a real mail server are dropped before they can reach SMTP.
Why it matters: Protects your sender reputation. One bounced email is fine; ten in a row gets you flagged as a spammer.
Enforced in:
core/email_validator.py · validate_email() - 3
Never sends to an address that previously bounced
Every bounce gets written to the invalid_emails table. The next time the bot considers that address — even months later — it skips silently.
Why it matters: Repeated sends to known-bad addresses are the #1 signal Gmail uses to quarantine your account.
Enforced in:
core/db.py · mark_invalid_email() / is_invalid_email() - 4
Never exceeds the safe daily cap
DAILY_EMAIL_CAP defaults to 50. The bot tracks every send in SQLite and stops the moment the day's count hits the cap, even mid-cycle.
Why it matters: Cold outreach over ~50/day flips Google's anti-spam classifier. Hard cap means you can't accidentally torch your domain.
Enforced in:
core/email_sender.py · emails_sent_today() check - 5
Never sends from a non-Gmail address
The SMTP client is hardcoded to smtp.gmail.com:587 with STARTTLS. There is no configuration for an arbitrary SMTP server, so the bot cannot be repurposed as a generic spam relay.
Why it matters: If someone steals your installer, they can't aim it at any other provider. It only knows Gmail.
Enforced in:
core/email_sender.py · send_email() · smtplib.SMTP() call - 6
Never opens an outbound connection you didn't approve
The bot's only outbound destinations are: Gmail SMTP, the job boards listed on /terms, jobybots.com (one license check per cycle), and the AI APIs you explicitly enable via API key.
Why it matters: No telemetry, no analytics, no 'phone home' for usage tracking. Inspect outbound traffic with Wireshark; you'll see only those five domains.
Enforced in:
core/net_safety.py · allowed-host list - 7
Never accesses files outside its own folder
The bot reads only what's inside the JobyBots folder: resume.pdf, .env, data/jobybot.db, data/*.html. It does not have admin/UAC/sudo permissions and cannot scan your disk.
Why it matters: Even if the bot were compromised, the blast radius is one folder. It cannot reach your Documents, browser cookies, SSH keys, or anything sensitive.
Enforced in:
All file I/O uses relative paths starting with ./ or ./data/ - 8
Never accepts remote commands
The Review Queue HTTP server binds to 127.0.0.1 only — never 0.0.0.0. A CSRF token cookie + matching header is required on every POST. The CSP header blocks third-party scripts from making requests against it.
Why it matters: Nothing on your home Wi-Fi, your office network, or the public internet can reach the queue API. Only YOUR browser, with the page YOU opened, can send actions.
Enforced in:
core/queue_server.py · ThreadingHTTPServer(("127.0.0.1", …)) + _csrf_ok() - 9
Never runs on a machine that isn't yours
On first cycle the bot binds itself to your machine's SHA-256 fingerprint via /api/license/bind. Subsequent cycles from a different machine for the same license are rejected with a clear error message.
Why it matters: Stops the 'I'll share the ZIP with friends' problem. Each paid license = one machine. Move to a new laptop anytime from /portal.
Enforced in:
core/license_check.py · verify_or_bind() - 10
Never auto-updates itself
There is no auto-updater. To upgrade you run UPDATE.bat which performs a 'git pull' — and you can read the diff first.
Why it matters: Auto-updaters are the most common supply-chain attack vector. You decide when (and whether) to take new code.
Enforced in:
Look — there's no updater script at all. Promise. - 11
Never auto-applies on LinkedIn without explicit opt-in
Easy Apply automation is OFF by default. Even when ENABLE_EASY_APPLY=true, the bot is in DRY-RUN mode (fills form, screenshots it, stops at Submit). 10/day hard cap. Random 20-60s jitter. Visible browser. Never auto-clicks Follow.
Why it matters: Easy Apply violates LinkedIn ToS §8.2. We refuse to make that a silent default. You opt in twice (enable + un-dry-run) before any application is submitted, and you watch every click in a real Chromium window. See /easy-apply for the full risk surface.
Enforced in:
core/easy_apply.py · run_easy_apply() · enable_easy_apply + dry_run gates
What the bot will never do
Things you might worry about that simply aren't in the code:
- ✗Never uploads your résumé to our servers (it stays in your folder)
- ✗Never uploads your .env, Gmail App Password, or any API key
- ✗Never reads emails in your inbox (it only sends; bounce-scan reads bounce metadata only)
- ✗Never imports your contacts, LinkedIn connections, or browser bookmarks
- ✗Never connects to social media beyond reading public job listings
- ✗Never sells, shares, or aggregates your data — there is no JobyBots server with your data on it
- ✗Never modifies system settings, scheduled tasks (except its own), or other applications
- ✗Never bundles ads, trackers, analytics SDKs, or third-party JavaScript
If you spot a boundary we missed
Responsible-disclosure friendly. Email security@jobybots.com with reproduction steps. Confirmed reports get acknowledged in CHANGELOG and a thank-you in the next release notes.