How this repo is actually operated: live Reddit mutations go through a
saved Chrome debug profile + SQLite queue/worker (agentctl /
reddit-tool). Password/cookie batch runs via main.py and Docker headless
are a legacy / owner escape hatch, not the agent or day-to-day path.
Agent runbook: AGENTS.md. Skill fast path: .claude/skills/reddit-bot/
(mirrored to .codex/skills/reddit-bot/).
- Features
- Installation
- Quick Start
- Saved Chrome Debug Profiles
- Configuration
- Input Formats
- Supported Actions
- Anti-Detection
- Orchestration
- Credentials & Security
- Reporting & Notifications
- Database Tracking
- Docker (legacy batch)
- CLI Reference
- Testing
- Architecture
- Contributing
- Queue + worker — submit actions, lease Chrome profile/debug port, run one pass
reddit-tool do— one-shot: build action → queue submit → worker → result- Saved Chrome debug profiles — manually authenticated sessions; no scripted login
- Healer extension — control discovery (confidence, bbox, state) before clicks
doctor— read-only diagnostics (DB, identity, DevTools, queue, executor)resolve-url— convert/r/.../s/...share shortlinks to canonical/comments/URLs- Schedules + executor — project-owned RRULE schedules; macOS LaunchAgent wakeup
- Quotas & leases — daily action quotas and profile leases in SQLite (WAL mode)
- Identity defaults — sole DB association or
REDDIT_BOT_DEFAULT_USER - Local web UI —
make uion127.0.0.1:8765; optionalREDDIT_BOT_UI_TOKENfor writes
- Upvote / Downvote posts
- Comment under posts
- Join / Leave communities
- Save / Hide posts
- Direct Message users
- Post Submission — text, link, image posts
- Crosspost content to other subreddits
- Follow / Unfollow users
- Update Profile bio
- Human search / search-upvote compound flows
- Retry logic with exponential backoff — failed actions automatically retry up to 3 times with increasing delays (2s, 4s, 8s)
- Action verification — checks DOM state after actions to confirm success (e.g.,
aria-pressedon vote buttons) - Screenshot on failure — captures a browser screenshot when an action fails, saved to
screenshots/directory - Graceful degradation — if one action or account fails, the bot continues with the remaining work
- Queue retry — requeue failed jobs via
agentctl queue retry
- Proxy support — load a list of proxies and rotate them per account session (legacy batch)
- User-Agent rotation — randomize the browser fingerprint with realistic Chrome UA strings
- Headless mode — run without a visible browser window (
--headless; legacy batch / Docker) - Rate limiting — configurable random delays between actions and between accounts
- Randomized action ordering — shuffle the action list per account so each executes a unique sequence
- Human-like mouse movement — move the cursor along Bezier curves before clicking elements
- Anti-automation flags — disables
navigator.webdriverand Chrome automation indicators
- YAML config file — store attach-mode and batch settings in
config.yaml - Action files — pipe-delimited (
|), CSV, and JSON for queue submit / schedules - URL validation — rejects non-Reddit and share shortlinks on queue/schedule submit
- Accounts-file credentials — password/cookie batch path remains available as legacy (see below)
- Project schedules — register RRULE work with
agentctl schedules register --links - Daily action quotas — atomic reservations so parallel agents share one budget
- Parallel agents — coordinated through SQLite queue leases (not ad hoc lock files)
- Session persistence — cookie restore for legacy
main.pybatch runs
- Structured logging — JSON log output option for machine parsing, with colored terminal output
- Execution summary — ASCII table printed at the end showing success/failure per action
- Progress bar — visual progress indicator via
tqdmfor long runs - Webhook notifications — send results to Discord, Slack, or any generic JSON webhook on completion or failure
reddit-tool errors/overview— recent failures and control-plane snapshot
- SQLite (WAL) — action log, queue, leases, quotas, schedules, profile associations
- Duplicate prevention — skips actions already successfully performed by the same account
- Daily stats — tracks action counts per account per day for quota enforcement
- Unit tests — control plane, actions, database, UI API, and parsing coverage
- CI pipeline — GitHub Actions workflow for automated testing on supported Python versions
- Plugin architecture — actions are modular classes; add new actions without modifying core bot logic
- Installable package —
pyproject.tomlforpip install .support - Dockerfile — legacy headless batch only (not for Chrome profile attach)
Requires Python 3.10 or newer.
git clone https://github.com/markmelnic/reddit-bot
cd reddit-bot
pip install -e .pip install -e ".[dev]"or with uv:
uv sync --extra devAfter install, CLIs are available as reddit-tool, reddit-agentctl, and
reddit-ui. From a checkout, the same entry points are:
.venv/bin/python scripts/reddit_tool.py --help
.venv/bin/python scripts/agentctl.py --help
.venv/bin/python scripts/reddit_ui.py --helpNote: Chrome and chromedriver are managed by
webdriver-managerwhen the bot launches a browser. The primary path attaches to an already-running saved Chrome debug profile instead.
make uiThe dashboard binds to 127.0.0.1:8765 by default and uses the same
agentctl/reddit-tool control plane as the terminal workflow. See
docs/local-ui.md. Optional write protection:
export REDDIT_BOT_UI_TOKEN='your-long-random-token'
make uiWhen set, every UI POST must send X-Reddit-Bot-Token.
Primary path: saved Chrome profile → queue / reddit-tool do. Agents
should follow AGENTS.md. Do not invent commands; use --help.
.venv/bin/python scripts/agentctl.py status
# or, when something looks wrong:
.venv/bin/python scripts/reddit_tool.py doctor --json.venv/bin/python scripts/reddit_healer_debug.py open-profile
# Log in manually once inside that window. Do not script Reddit login.Default profile: Chrome Reddit Bot Debug Profile on 127.0.0.1:9222.
.venv/bin/python scripts/agentctl.py profiles associate \
--profile-name "Chrome Reddit Bot Debug Profile" \
--reddit-user "u/Particular-Arm2102"With a sole association (or REDDIT_BOT_DEFAULT_USER set), identity flags can
be omitted on later commands.
.venv/bin/python scripts/reddit_tool.py do \
--action upvote \
--link "https://www.reddit.com/r/example/comments/abc123/title/" \
--reddit-user "u/Particular-Arm2102"do builds the action file, submits to the queue, runs one worker pass, and
prints the outcome. Share shortlinks (/r/.../s/...) are rejected — resolve first:
.venv/bin/python scripts/reddit_tool.py resolve-url \
--link "https://www.reddit.com/r/example/s/SHAREID" --json.venv/bin/python scripts/agentctl.py queue submit \
--reddit-user "u/Particular-Arm2102" \
--links links.txt
.venv/bin/python scripts/agentctl.py queue worker --once.venv/bin/python scripts/reddit_tool.py menuPassword/cookie multi-account runs and dry-run previews still use main.py.
This is an owner escape hatch, not the agent default:
python main.py --accounts accounts.txt --links links.txt --verbose
python main.py -a accounts.txt -l links.txt --dry-run --verbose
python main.py --config config.yamlFor attach mode without the queue (also owner-only):
.venv/bin/python main.py -a accounts.txt -l links.txt --verbose \
--use-existing-chrome \
--chrome-debugging-address 127.0.0.1:9222 \
--chrome-extension-healerThe preferred local workflow is to use manually authenticated Chrome profiles and attach through Chrome DevTools. This avoids scripted Reddit login and lets the Reddit Bot Healer extension identify controls before actions are clicked.
Default saved profile:
| Name | Path | DevTools |
|---|---|---|
Chrome Reddit Bot Debug Profile |
/Users/raulvecchione/Library/Application Support/Chrome Reddit Bot Debug Profile |
127.0.0.1:9222 |
Open the default profile:
.venv/bin/python scripts/reddit_healer_debug.py open-profileOpen a saved profile for another Reddit account on a different port:
.venv/bin/python scripts/reddit_healer_debug.py open-profile \
--profile-name "Chrome Reddit Bot Debug Profile - account2" \
--port 9223 \
--url "https://www.reddit.com/login/"Log in manually in that Chrome window. Do not automate Reddit login for this workflow. The helper passes --load-extension=chrome_extension/reddit_healer; if Chrome does not show Reddit Bot Healer under chrome://extensions, enable Developer mode and load that unpacked extension manually.
Check the extension bridge:
.venv/bin/python scripts/reddit_healer_debug.py ping-bridge \
--debug-address 127.0.0.1:9223Find a control candidate before clicking:
.venv/bin/python scripts/reddit_healer_debug.py find-control \
--debug-address 127.0.0.1:9223 \
--intent downvote \
--url "https://www.reddit.com/r/example/comments/abc/title/"The expected action report includes candidate confidence, bounding box, state, and evidence. Click only when the command or user request explicitly calls for a real action.
Run live work through the control plane (preferred):
.venv/bin/python scripts/agentctl.py queue submit \
--profile-name "Chrome Reddit Bot Debug Profile - account2" \
--links links.txt
.venv/bin/python scripts/agentctl.py queue worker --onceOwner-only direct attach (escape hatch, not for agents):
.venv/bin/python main.py -a accounts.txt -l links.txt --verbose \
--use-existing-chrome \
--chrome-debugging-address 127.0.0.1:9223 \
--chrome-extension-healerFor attach mode, accounts.txt is only an account label. The active Reddit
account is whatever is manually logged in inside the Chrome profile on that
port. Use one saved profile/port per Reddit account.
Print reusable setup details for any profile:
.venv/bin/python scripts/reddit_healer_debug.py profile-info \
--profile-name "Chrome Reddit Bot Debug Profile - account2" \
--port 9223Settings can be provided via YAML config file, CLI flags, or environment variables. Priority order (highest to lowest):
- CLI arguments — override everything
- Environment variables — override config file
- YAML config file — base configuration
accounts_path: "accounts.txt"
links_path: "links.txt"
verbose: true
headless: false
dry_run: false
rotate_user_agent: true
randomize_actions: true
human_mouse: false
proxy:
enabled: true
proxy_list_path: "proxies.txt"
rotate_per_account: true
rate_limit:
min_action_delay: 2.0
max_action_delay: 8.0
min_account_delay: 5.0
max_account_delay: 15.0
daily_action_quota: 50
parallel_accounts: 1
session_persistence: true
screenshot_on_failure: true
db_path: "reddit_bot.db"
webhook:
enabled: true
url: "https://discord.com/api/webhooks/..."
on_completion: true
on_failure: trueSee config.example.yaml for the full template with comments.
| Variable | Description |
|---|---|
REDDIT_ACCOUNT_1 |
Account credentials as username|password |
REDDIT_ACCOUNT_2 |
Second account, and so on |
REDDIT_BOT_KEY |
Passphrase for encrypted credential files |
REDDIT_BOT_ACCOUNTS |
Path to accounts file |
REDDIT_BOT_LINKS |
Path to links file |
REDDIT_BOT_HEADLESS |
Enable headless mode (true/false) |
REDDIT_BOT_DRY_RUN |
Enable dry run (true/false) |
REDDIT_BOT_DB_PATH |
Path to SQLite database |
REDDIT_BOT_LOG_DIR |
Directory for durable JSONL logs |
REDDIT_BOT_LOG_FILE |
File name for durable bot logs |
REDDIT_BOT_WEBHOOK_URL |
Webhook URL for notifications |
REDDIT_BOT_USE_EXISTING_CHROME |
Use already-authenticated Chrome profile/instance (true/false) |
REDDIT_BOT_CHROME_USER_DATA_DIR |
Path to Chrome user data directory |
REDDIT_BOT_CHROME_PROFILE_NAME |
Chrome profile folder name (e.g. Default) |
REDDIT_BOT_CHROME_DEBUGGING_ADDRESS |
Existing Chrome debugger address (e.g. 127.0.0.1:9222) |
REDDIT_BOT_SELECTOR_CACHE_PATH |
Path for healed Reddit selector cache |
REDDIT_BOT_SELECTOR_DIAGNOSTICS_DIR |
Directory for selector diagnostics when healing fails |
REDDIT_BOT_SELECTOR_FALLBACK_WAIT |
Short wait for legacy selector fallbacks |
REDDIT_BOT_SELENIUM_IMPLICIT_WAIT |
Default Selenium implicit wait |
REDDIT_BOT_CHROME_EXTENSION_HEALER_ENABLED |
Enable the Reddit healer Chrome extension bridge |
REDDIT_BOT_CHROME_EXTENSION_PATH |
Path to the unpacked healer extension |
REDDIT_BOT_CHROME_EXTENSION_BRIDGE_TIMEOUT_MS |
Timeout for extension bridge requests |
REDDIT_BOT_CHROME_EXTENSION_MIN_CONFIDENCE |
Minimum control confidence required before clicking |
REDDIT_BOT_DEFAULT_USER |
Default Reddit identity when no --reddit-user / --profile-name is passed (must match a chrome_profile_accounts row; sole association is preferred) |
REDDIT_BOT_UI_TOKEN |
Optional shared secret for local UI write (POST) routes |
Action/links files are used by queue submit, schedules, and legacy batch.
Accounts files are for the legacy main.py password path only (control
plane uses Chrome profile associations).
Pipe-delimited (default):
username1|password1
username2|password2
CSV:
username,password
username1,password1
username2,password2JSON:
[
{"username": "username1", "password": "password1"},
{"username": "username2", "password": "password2"}
]Pipe-delimited (default):
https://www.reddit.com/r/ProgrammerHumor/comments/abc123/title|upvote
https://www.reddit.com/r/ProgrammerHumor/comments/xyz789/title|comment|Great post!
https://www.reddit.com/r/ProgrammerHumor/|join
https://www.reddit.com/r/ProgrammerHumor/comments/abc123/title|save
https://www.reddit.com/user/someone|follow
CSV:
link,action,comment,title,subreddit,body,recipient,message
https://reddit.com/r/test/comments/abc,upvote,,,,,,
https://reddit.com/r/test/comments/abc,comment,Hello!,,,,,
https://reddit.com/r/test,join,,,,,,
,dm,,,,,targetuser,Hello from the bot!
,post_text,,,My Post Title,ProgrammerHumor,Post body text,,JSON:
[
{"link": "https://reddit.com/r/test/comments/abc", "action": "upvote"},
{"link": "https://reddit.com/r/test/comments/abc", "action": "comment", "comment": "Hello!"},
{"action": "dm", "recipient": "targetuser", "title": "Subject", "message": "Hello!"},
{"action": "post_text", "subreddit": "ProgrammerHumor", "title": "My Post", "body": "Content here"}
]| Action | Description | Required Fields |
|---|---|---|
upvote |
Upvote a post | link |
downvote |
Downvote a post | link |
comment |
Post a comment | link, comment |
join |
Join a subreddit | link |
leave |
Leave a subreddit | link |
save |
Save a post | link |
hide |
Hide a post | link |
dm |
Send a direct message | recipient, message (optional: title) |
post_text |
Create a text post | subreddit, title (optional: body, flair) |
post_link |
Create a link post | subreddit, title, body (the URL) |
post_image |
Create an image post | subreddit, title, body (image file path) |
crosspost |
Crosspost to another sub | link, subreddit (optional: title) |
follow |
Follow a user | link (user profile URL) |
unfollow |
Unfollow a user | link (user profile URL) |
update_bio |
Update profile bio | body (bio text) |
The flags below apply mainly to legacy main.py batch sessions. The
control-plane path relies on a real saved Chrome profile (manual login, Healer
extension) rather than scripted UA/proxy login.
Create a proxies.txt file:
host1:port1
host2:port2
host3:port3:username:password
python main.py -a accounts.txt -l links.txt --proxy-list proxies.txtProxies rotate round-robin for each new account session.
python main.py -a accounts.txt -l links.txt --rotate-uaEach browser session starts with a random Chrome user agent from a pool of realistic UA strings.
python main.py -a accounts.txt -l links.txt --human-mouseUses Bezier curves to generate natural cursor paths before clicking elements. Install the optional dependency group before enabling it:
pip install -e ".[mouse]"Without the optional extra, human mouse mode falls back to the standard Selenium pointer click path.
python main.py -a accounts.txt -l links.txt --headlessRuns Chrome without a visible window. Useful for servers and Docker.
python main.py -a accounts.txt -l links.txt --randomize-actionsShuffles the action list for each account so they don't all perform the same sequence.
LLM agents should start with the agent runbook and skill, not main.py:
# Read AGENTS.md for policy; use the reddit-bot skill for the fast path.
.venv/bin/python scripts/agentctl.py status
.venv/bin/python scripts/reddit_tool.py doctor --json
.venv/bin/python scripts/reddit_tool.py capabilitiesLive mutations: reddit-tool do or queue submit + one worker pass:
.venv/bin/python scripts/agentctl.py profiles associate \
--profile-name "Chrome Reddit Bot Debug Profile" \
--reddit-user "u/Particular-Arm2102"
.venv/bin/python scripts/reddit_tool.py do \
--action upvote \
--link "https://www.reddit.com/r/example/comments/abc/title/" \
--reddit-user "u/Particular-Arm2102"
# Equivalent multi-action flow:
.venv/bin/python scripts/agentctl.py queue submit \
--reddit-user "u/Particular-Arm2102" \
--links links.txt
.venv/bin/python scripts/agentctl.py --config config.yaml queue worker --onceAgents can also queue by Chrome profile with --profile-name. The queue uses
SQLite WAL, leases, and atomic daily quota reservations. See
docs/agentic-operations.md and
docs/scheduler-and-rate-limits.md.
Use scripts/reddit_tool.py (reddit-tool after install) for day-to-day work.
Live actions still go through the project queue, schedules, executor, leases,
and quotas.
.venv/bin/python scripts/reddit_tool.py menu
.venv/bin/python scripts/reddit_tool.py overview
.venv/bin/python scripts/reddit_tool.py doctor --json
.venv/bin/python scripts/reddit_tool.py resolve-url --link "<share_or_post_url>" --json
.venv/bin/python scripts/reddit_tool.py schedules
.venv/bin/python scripts/reddit_tool.py queue --status failed
.venv/bin/python scripts/reddit_tool.py executor
.venv/bin/python scripts/reddit_tool.py errorsAdd a one-time scheduled action (writes the action file automatically):
.venv/bin/python scripts/reddit_tool.py schedule add \
--name "Upvote example post" \
--link "https://www.reddit.com/r/example/comments/abc/title/" \
--action upvote \
--at "2026-07-06T09:00:00"Register a recurring schedule from an existing links/action file:
.venv/bin/python scripts/reddit_tool.py schedule add \
--name "Weekday Reddit actions" \
--links links.txt \
--weekly MO,WE,FR \
--time 09:30Submit immediate queue work without running it yet:
.venv/bin/python scripts/reddit_tool.py queue add --links links.txtRun due project schedules and exactly one worker pass:
.venv/bin/python scripts/reddit_tool.py schedule run-due --run-worker.venv/bin/python scripts/agentctl.py limits set \
--account "Particular-Arm2102" \
--daily-action-quota 25
.venv/bin/python scripts/agentctl.py limits listLegacy config.yaml rate_limit.daily_action_quota still applies to direct
main.py batch runs; agents should use agentctl limits.
These flags remain for owner-controlled multi-account batch runs. Prefer the queue for anything an agent or schedule should own.
# Parallel browsers (legacy batch)
python main.py -a accounts.txt -l links.txt --parallel 3
# Simple interval schedule inside main.py (prefer agentctl schedules)
python main.py -a accounts.txt -l links.txt --schedule "0 */6 * * *"
# Cookie session files under .sessions/
python main.py -a accounts.txt -l links.txt --session-persistenceDirect attach without the queue (owner escape hatch):
.venv/bin/python scripts/reddit_healer_debug.py open-profile \
--profile-name "Chrome Reddit Bot Debug Profile" \
--port 9222
.venv/bin/python main.py -a accounts.txt -l links.txt \
--use-existing-chrome \
--chrome-debugging-address 127.0.0.1:9222 \
--chrome-extension-healerYou can also pass --chrome-user-data-dir when not attaching to a running
debugger. Attach mode is safer: login stays manual, DevTools is inspectable,
and the Healer extension can already be loaded in that profile.
Use one Chrome user-data-dir and one port per Reddit account. Example:
.venv/bin/python scripts/reddit_healer_debug.py open-profile \
--profile-name "Chrome Reddit Bot Debug Profile - account3" \
--port 9224 \
--url "https://www.reddit.com/login/"After manual login, associate and queue with that profile/port — do not reuse one debugger address across accounts.
Primary operation uses manually authenticated Chrome profiles — no
password file required. The sections below apply only to the legacy
main.py accounts-file path.
First, encrypt your accounts file:
from bot.utils.credentials import encrypt_file
encrypt_file("accounts.txt", "accounts.bin", "your-secret-passphrase")Then run with the encrypted file:
export REDDIT_BOT_KEY="your-secret-passphrase"
python main.py -a accounts.bin -l links.txt --encrypt-credentialsexport REDDIT_ACCOUNT_1="username1|password1"
export REDDIT_ACCOUNT_2="username2|password2"
python main.py -l links.txtEvery bot run creates durable JSON-line logs at logs/reddit-bot.log by default, even when --verbose is not enabled. The Chrome debug helper writes command failures to logs/reddit-healer-debug.log.
Weekly troubleshooting instructions live in docs/weekly-log-maintenance.md.
Override the bot log location from config, environment variables, or CLI:
log_dir: "logs"
log_file: "reddit-bot.log"python main.py -a accounts.txt -l links.txt --log-dir logs --log-file reddit-bot.logWith --verbose, a summary table is printed after completion:
================================================================================
EXECUTION SUMMARY
================================================================================
Duration: 142.3s | Total: 12 | Success: 10 | Failed: 2
--------------------------------------------------------------------------------
Status Action Link Message
--------------------------------------------------------------------------------
OK upvote https://reddit.com/r/test/comme.. Vote registered
OK comment https://reddit.com/r/test/comme.. Comment posted
FAIL join https://reddit.com/r/private Message: NoSuchElem..
OK follow https://reddit.com/user/someone User followed
================================================================================
webhook:
enabled: true
url: "https://discord.com/api/webhooks/YOUR_WEBHOOK_ID/YOUR_WEBHOOK_TOKEN"Sends a rich embed with color-coded success/failure status.
webhook:
enabled: true
url: "https://hooks.slack.com/services/YOUR/SLACK/WEBHOOK"Any other URL receives the full execution summary as JSON.
python main.py -a accounts.txt -l links.txt --screenshot-on-failureScreenshots are saved to screenshots/ with descriptive filenames including the action name and timestamp.
Reddit changes UI markup frequently. The bot keeps a local selector cache in .selector-healing/reddit_selectors.json and writes compact diagnostics to .selector-healing/diagnostics/ when an action cannot find a control.
When a normal selector misses, actions can run a browser-side probe that logs a reddit-bot:self-healing response in the page console, reads the structured result back through Selenium, and stores the discovered selector for future runs.
The repo includes an unpacked Chrome extension at chrome_extension/reddit_healer. When enabled, vote actions ask the extension for a control candidate before falling back to Selenium selectors. The extension observes Reddit from inside Chrome, including dynamic DOM state, open shadow DOM, button attribute changes, clicks, page console logs, and fetch/XHR responses.
Enable it in config:
chrome_extension_healer_enabled: true
chrome_extension_path: "chrome_extension/reddit_healer"
chrome_extension_min_confidence: 0.8Or from the legacy CLI:
python main.py -a accounts.txt -l links.txt --chrome-extension-healerQueue workers honor the same config when attaching to a saved profile. For
Chrome launched by the bot, the extension is loaded from chrome_extension_path.
For attach mode, load the unpacked extension in that Chrome profile first
(open-profile already passes --load-extension).
When operating through saved debug profiles, use the helper first:
.venv/bin/python scripts/reddit_healer_debug.py ping-bridge --debug-address 127.0.0.1:9222
.venv/bin/python scripts/reddit_healer_debug.py find-control --debug-address 127.0.0.1:9222 --intent upvote --url "<POST_URL>"Report the returned best candidate's confidence, bounding box, state, and evidence before executing the click in manual testing tasks.
All coordination and action history live in SQLite (reddit_bot.db by default).
Connections open with WAL (PRAGMA journal_mode=WAL, synchronous=NORMAL)
so readers (UI, agents, status) are less blocked by writers. Backup with the
SQLite backup API, or stop writers first — do not copy only the main file while
the process is running.
-- View all actions
SELECT * FROM action_log ORDER BY timestamp DESC;
-- View failures
SELECT * FROM action_log WHERE success = 0;
-- View daily stats per account
SELECT * FROM account_stats WHERE action_date = date('now');The action log prevents duplicate successful actions for the same account. The
same database also stores the agent queue, leases, quotas, schedules, and
Chrome profile ↔ Reddit user associations. Details:
docs/scheduler-and-rate-limits.md.
Legacy only. The image runs
main.py --headlessfor password/cookie batch jobs. It is not the Chrome debug-profile attach path and is not suitable for the queue-first agent workflow on a local Mac with saved profiles.
docker build -t reddit-bot .
docker run -v $(pwd)/accounts.txt:/app/accounts.txt \
-v $(pwd)/links.txt:/app/links.txt \
reddit-bot -a accounts.txt -l links.txt --verboseWith a config file:
docker run -v $(pwd)/config.yaml:/app/config.yaml \
-v $(pwd)/accounts.txt:/app/accounts.txt \
-v $(pwd)/links.txt:/app/links.txt \
reddit-bot --config config.yamlFor day-to-day operation, use Quick Start above (profile + reddit-tool /
agentctl) instead of Docker.
Verify flags with --help — do not invent subcommands.
.venv/bin/python scripts/agentctl.py --help
.venv/bin/python scripts/agentctl.py status
.venv/bin/python scripts/agentctl.py profiles --help
.venv/bin/python scripts/agentctl.py queue --help
.venv/bin/python scripts/agentctl.py schedules --help
.venv/bin/python scripts/agentctl.py limits --help
.venv/bin/python scripts/agentctl.py executor --help
.venv/bin/python scripts/reddit_tool.py --help
.venv/bin/python scripts/reddit_tool.py doctor --json
.venv/bin/python scripts/reddit_tool.py resolve-url --link URL
.venv/bin/python scripts/reddit_tool.py do --action upvote --link URL
.venv/bin/python scripts/reddit_tool.py capabilitiesAfter install: reddit-agentctl, reddit-tool, reddit-ui.
usage: reddit-bot [-h] [-a ACCOUNTS] [-l LINKS] [-c CONFIG] [-v]
[--headless] [--dry-run] [--proxy-list PROXY_LIST]
[--rotate-ua] [--randomize-actions] [--human-mouse]
[--manual-login] [--use-existing-chrome]
[--chrome-user-data-dir CHROME_USER_DATA_DIR]
[--chrome-profile-name CHROME_PROFILE_NAME]
[--chrome-debugging-address CHROME_DEBUGGING_ADDRESS]
[--parallel PARALLEL] [--schedule SCHEDULE]
[--session-persistence] [--encrypt-credentials]
[--screenshot-on-failure] [--webhook-url WEBHOOK_URL]
Legacy multi-account Selenium batch entry (owner escape hatch).
options:
-h, --help Show this help message and exit
-a, --accounts Path to accounts file (pipe, CSV, or JSON)
-l, --links Path to actions file (pipe, CSV, or JSON)
-c, --config Path to YAML configuration file
-v, --verbose Enable verbose logging to stdout
--headless Run browser in headless mode
--dry-run Log actions without executing them
--proxy-list Path to proxy list file (host:port per line)
--rotate-ua Randomize User-Agent per session
--randomize-actions Shuffle action order per account
--human-mouse Use Bezier curve mouse movements
--manual-login Pause for manual browser login when automatic login fails
--use-existing-chrome Use an already logged-in Chrome instance instead of automated login
--chrome-user-data-dir CHROME_USER_DATA_DIR
Chrome user-data-dir to reuse profile/session
--chrome-profile-name CHROME_PROFILE_NAME
Chrome profile directory under user-data-dir (default: Default)
--chrome-debugging-address CHROME_DEBUGGING_ADDRESS
Existing Chrome debugger address (e.g. 127.0.0.1:9222)
--parallel N Number of parallel browser instances
--schedule CRON Cron expression for scheduled runs
--session-persistence Save/restore browser sessions
--encrypt-credentials Accounts file is encrypted
--screenshot-on-failure Capture screenshots on action failure
--webhook-url URL Webhook URL for notifications
--log-dir DIR Directory for durable bot logs
--log-file FILE File name for durable bot logs
Run the test suite:
# Install dev dependencies
pip install -e ".[dev]"
# Run all tests
make test
# Run all tests through uv
uv run pytest -q
# Run tests with the CI coverage report
pytest tests/ -q --cov=bot --cov=main --cov=args --cov-report=term-missing
# Run specific test module
pytest tests/test_config.py -q
pytest tests/test_database.py -qCurrent coverage baseline: 63% total statement coverage from the CI coverage command.
TOTAL 5365 statements, 1971 missed, 63% covered
Tests cover:
- Configuration loading, merging, and env var parsing
- Input file parsing (pipe-delimited, CSV, JSON)
- URL validation
- Credential encryption/decryption
- Proxy loading and rotation
- Database action logging and queries
- Execution summary reporting
Two operational paths share the same Selenium action plugins and SQLite DB:
| Path | Entry | When to use |
|---|---|---|
| Control plane (primary) | scripts/agentctl.py, scripts/reddit_tool.py, make ui |
Agents, schedules, day-to-day live work via queue + Chrome debug profile |
| Legacy batch (escape hatch) | main.py, Docker ENTRYPOINT |
Owner multi-account password/cookie/headless batch; not for agents |
reddit-bot/
├── main.py # Legacy batch entry (owner escape hatch)
├── args.py # main.py CLI argument parser
├── config.example.yaml # Example configuration file
├── pyproject.toml # Package configuration
├── Dockerfile # Legacy headless batch image only
├── Makefile # test, check-skill, sync-skill, ui
├── AGENTS.md # Agent runbook and live-action policy
├── AGENT.md # Short Chrome-profile playbook
├── bot/
│ ├── __init__.py
│ ├── bot.py # Core RedditBot class (Selenium lifecycle)
│ ├── config.py # BotConfig dataclass with YAML/env support
│ ├── database.py # SQLite WAL: action log, queue, leases, schedules, quotas
│ ├── action_schema.py # Machine-readable reddit-tool action schema
│ ├── agentctl.py # Agent-safe control plane CLI
│ ├── tool_cli.py # Human-friendly reddit-tool CLI and JSON envelope
│ ├── skills_sync.py # .claude -> .codex skill mirror utility
│ ├── reporting.py # Summary, durable structured logging, webhooks
│ ├── control/ # Shared control-plane helpers
│ │ ├── doctor.py # Read-only diagnostics
│ │ ├── resolve.py # Share-URL resolution helpers
│ │ ├── profiles.py # Profile discovery + identity defaults
│ │ ├── queue.py # Queue submit/worker helpers
│ │ ├── limits.py # Quota helpers
│ │ ├── executor.py # LaunchAgent / executor helpers
│ │ ├── status.py # status JSON assembly
│ │ ├── errors.py # CLI exception types
│ │ └── schedules.py # RRULE, cadence, and schedule ID helpers
│ ├── actions/ # Plugin-based action system
│ │ ├── base.py # BaseAction ABC and ActionResult
│ │ ├── registry.py # Action name -> class mapping
│ │ ├── search.py # Human search and search_upvote flows
│ │ ├── vote.py # Upvote/downvote
│ │ ├── comment.py # Comment on post
│ │ ├── community.py # Join/leave subreddit
│ │ ├── save_hide.py # Save/hide post
│ │ ├── post.py # Text/link/image post, crosspost
│ │ ├── dm.py # Direct messages
│ │ ├── follow.py # Follow/unfollow users
│ │ └── profile.py # Update bio
│ ├── utils/ # Shared utilities
│ │ ├── chrome_extension_bridge.py # Reddit healer extension bridge
│ │ ├── chromedriver.py # ChromeDriver resolution helper
│ │ ├── reddit_urls.py # Canonical / share URL helpers
│ │ ├── clock.py # UTC timestamp helpers
│ │ ├── timeouts.py # Randomized delays
│ │ ├── retry.py # Exponential backoff decorator
│ │ ├── mouse.py # Bezier curve mouse movement
│ │ ├── self_healing.py # Runtime selector healing
│ │ ├── visible_vote.py # Visible vote control diagnostics/clicking
│ │ ├── user_agents.py # UA string rotation
│ │ ├── credentials.py # Account parsing and encryption (legacy batch)
│ │ ├── input_parser.py # Action file parsing
│ │ ├── validators.py # URL validation
│ │ └── proxy.py # Proxy loading and rotation
│ └── web/ # Localhost-only dashboard server
│ └── server.py # API and static file server
├── chrome_extension/ # Reddit healer Chrome extension
├── docs/ # Agent operations, scheduling, UI, and maintenance docs
├── scripts/ # Thin CLI launchers (agentctl, reddit_tool, ui, healer)
├── tests/ # Unit and API test suite
├── web/ # Zero-build dashboard frontend
└── .github/
└── workflows/
└── ci.yml # GitHub Actions CI pipeline
Create a new action by extending BaseAction:
# bot/actions/my_action.py
from bot.actions.base import BaseAction, ActionResult
class MyCustomAction(BaseAction):
name = "my_action"
def execute(self, link="", **kwargs):
self.logger.info(f"Running my action on {link}")
if self.config.dry_run:
return ActionResult(success=True, action="my_action", link=link, message="Dry run")
# Your Selenium logic here
self._navigate(link)
element = self._find_with_fallbacks(
(By.CSS_SELECTOR, "button.my-button"),
(By.XPATH, "//button[text()='Click Me']"),
)
self._click(element)
return ActionResult(success=True, action="my_action", link=link, message="Done")Register it in bot/actions/registry.py:
from .my_action import MyCustomAction
# In ActionRegistry._action_map:
"my_action": MyCustomAction,- Fork the repository
- Create your feature branch (
git checkout -b feature/new-action) - Write tests for any new functionality
- Run the test suite (
make test) - Commit your changes
- Push to your branch
- Open a Pull Request
MIT License - see LICENSE for details.