Origin Story: Someone on the team forwarded an email: "Hey, can everyone register and upvote these 5 VCF feature requests?" A normal person would have spent 3 minutes clicking buttons. Instead, we spent a weekend building a CLI tool to automate it. In Rust. Because of course we did.
What started as "I'll just automate this real quick" turned into a full-blown CLI application with cross-platform Chrome detection, proxy vote support, and more error handling than the actual voting portal probably has.
Was it worth it? Absolutely not. Would we do it again? Already planning v2.
A human-in-the-loop CLI tool that helps you navigate and upvote VMware Cloud Foundation (VCF) feature requests on Broadcom's Aha! Ideas portal. It opens a real browser, you log in like a normal human being, and then it clicks the vote buttons for you.
It does NOT:
- Store your credentials (we're not animals)
- Bypass CAPTCHAs (we're not those kind of engineers)
- Do anything without your explicit consent (every click requires you to press ENTER)
- Replace the 3 minutes it would have taken to just do it manually
It DOES:
- Look really cool in your terminal
- Generate a CSV report so you can prove to management that you voted
- Make you mass-upvote VMware feature requests like a civic duty
- Rust (1.70+)
- Chrome or Chromium (the tool finds it automatically)
- A Broadcom/Aha! account (the one thing we can't automate)
macOS
# Install Rust via rustup
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Restart your terminal, or:
source "$HOME/.cargo/env"
# Verify
rustc --versionChrome is usually already at /Applications/Google Chrome.app — the tool finds it automatically.
Linux
# Install Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source "$HOME/.cargo/env"
# Make sure Chrome/Chromium is installed
# Ubuntu/Debian:
sudo apt install chromium-browser
# Fedora:
sudo dnf install chromium
# Arch:
sudo pacman -S chromiumWindows
- Download and run rustup-init.exe
- Follow the prompts (default options are fine)
- Restart your terminal
- Chrome is usually already installed at
C:\Program Files\Google\Chrome\Application\chrome.exe
If Chrome is in a non-standard location:
$env:CHROME_PATH = "C:\path\to\chrome.exe"git clone https://github.com/vipurkumar/vcf-vote.git
cd vcf-vote
cargo build --releaseThe binary will be at target/release/vcf-vote.
# Dry run — opens browser, navigates to FRs, checks selectors, doesn't click anything
cargo run --release -- --ids VCF-I-979,VCF-I-3704,VCF-I-3707,VCF-I-2188,VCF-I-3659Here's what happens:
- A disclaimer prompt appears. Press ENTER to accept.
- Chrome opens to the Aha! portal.
- A prompt says "Please sign in" — go to the browser, log in, handle any 2FA/CAPTCHA.
- Press ENTER in the terminal when you're logged in.
- The tool navigates to each feature request page.
- In dry-run mode, it checks if the Vote button exists and reports back.
- A CSV report is saved and a summary table is shown.
# The --live flag enables actual clicking
cargo run --release -- --ids VCF-I-979,VCF-I-3704,VCF-I-3707,VCF-I-2188,VCF-I-3659 --liveIn live mode, the tool pauses before every single vote click and asks you to confirm. You're always in control.
cargo run --release -- \
--ids VCF-I-979,VCF-I-3704,VCF-I-3707,VCF-I-2188,VCF-I-3659 \
--live \
--company "Acme Corp" \
--use-case "Critical for our compliance and operations posture"This attempts to open the proxy vote dialog and prefill your company name and use case. You review the text before it submits.
cargo run --release -- \
--ids VCF-I-979,VCF-I-3704 \
--mode register_and_vote \
--liveThis opens the Broadcom registration page first, waits for you to create an account, then proceeds to the portal.
If Chrome isn't auto-detected:
# macOS
export CHROME_PATH="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
# Linux
export CHROME_PATH="/usr/bin/google-chrome-stable"
# Windows (PowerShell)
$env:CHROME_PATH = "C:\Program Files\Google\Chrome\Application\chrome.exe"
cargo run --release -- --ids VCF-I-979Usage: vcf-vote [OPTIONS]
Options:
--ids <IDS> Comma-separated FR IDs (e.g. VCF-I-979,VCF-I-3704)
--mode <MODE> "vote_only" or "register_and_vote" [default: vote_only]
--dry-run Validate selectors without clicking [default: true]
--live Actually vote (disables dry-run)
--company <COMPANY> Company name for proxy vote
--use-case <USE_CASE> Use case summary for proxy vote
--output <OUTPUT> CSV report path [default: vcf_votes_report.csv]
-h, --help Print help
These are the VCF feature requests that started this whole adventure:
| ID | Link |
|---|---|
| VCF-I-979 | https://vcf.ideas.aha.io/ideas/VCF-I-979 |
| VCF-I-3704 | https://vcf.ideas.aha.io/ideas/VCF-I-3704 |
| VCF-I-3707 | https://vcf.ideas.aha.io/ideas/VCF-I-3707 |
| VCF-I-2188 | https://vcf.ideas.aha.io/ideas/VCF-I-2188 |
| VCF-I-3659 | https://vcf.ideas.aha.io/ideas/VCF-I-3659 |
After a run, you get:
CSV Report (vcf_votes_report.csv):
fr_id,url,status,message,timestamp
VCF-I-979,https://vcf.ideas.aha.io/ideas/VCF-I-979,voted,vote clicked,2026-03-25T10:15:30Z
VCF-I-3704,https://vcf.ideas.aha.io/ideas/VCF-I-3704,already_voted,already voted,2026-03-25T10:15:35ZTerminal Summary:
## VCF Vote Summary
| Metric | Count |
|--------------|-------|
| Total FRs | 5 |
| Voted | 3 |
| Already voted| 1 |
| Dry run OK | 0 |
| Failed | 1 |
# All 20 tests
cargo test
# A specific test
cargo test test_valid_fr_ids
# With output
cargo test -- --nocapturemain.rs
|
+---> Async runtime (tokio)
|
+---> browser.rs — launches Chrome via CDP (chromiumoxide)
+---> voter.rs — navigates to FR pages, finds vote buttons via JS
+---> report.rs — writes CSV + prints summary
The async worker manages state through Arc<Mutex<AppState>>. Human-in-the-loop pauses work by prompting in the terminal and waiting for the user to press ENTER before proceeding.
Vote buttons are found using JavaScript evaluation — querying button, a, [role="button"] elements by text content. The selectors may need updating if the Aha! portal changes its UI.
The vote button detection in src/voter.rs uses placeholder selectors. After your first dry run with authentication, inspect the actual DOM and update:
find_vote_button()— locates the vote controlclick_vote_button()— clicks itattempt_proxy_vote()— opens proxy dialog, fills fields, submits
The --company and --use-case flags enable proxy voting — voting on behalf of your organization with a reason attached. Out of the box, the proxy selectors are placeholders and will likely report proxy fields not found. Here's how to make them work for your portal.
The Aha! Ideas portal's proxy vote UI varies by configuration. The tool uses JavaScript to find buttons and form fields by their text content and labels. Since every portal instance can have different button labels, field names, and dialog structures, you need to inspect yours once and update the selectors.
Navigate to any FR page while logged in, e.g.:
https://vcf.ideas.aha.io/ideas/VCF-I-979
Look for a button that opens a proxy vote dialog. Common labels:
- "Vote on behalf of..."
- "Add proxy vote"
- "Add details"
- "Vote for a customer"
Right-click it → Inspect. Note the exact text content.
Open src/voter.rs and find the attempt_proxy_vote function (~line 218). Update the js_open block to match your button's text:
// Current placeholder — update the text matches:
if (text.includes('proxy') || text.includes('add details') || text.includes('on behalf')) {
btn.click();
return true;
}Change the strings to match what you found. For example, if the button says "Vote for a customer":
if (text.includes('vote for a customer')) {
btn.click();
return true;
}After clicking the proxy button, a dialog should open with input fields. Inspect each field and note its:
aria-labelattributeplaceholderattributenameattribute
The js_fill block (~line 254) looks for fields by their aria-label or placeholder. Update the matching strings:
// Current placeholders — update these:
if (label.includes('company') || label.includes('organization')) {
// fills the company field
}
if (label.includes('use case') || label.includes('reason') || label.includes('details')) {
// fills the use case field
}If the fields use name attributes instead of aria-label/placeholder, change the selector logic:
const label = (
input.getAttribute('aria-label') ||
input.getAttribute('placeholder') ||
input.getAttribute('name') || // <-- add this
''
).toLowerCase();Look for the button that submits the proxy vote. Common labels:
- "Submit"
- "Save"
- "Add vote"
- "Confirm"
Update the js_submit block (~line 296) to match:
// Current placeholders — update these:
if (text.includes('submit') || text.includes('save') || text.includes('add vote')) {
btn.click();
return true;
}# Dry run first — verifies selectors without clicking
cargo run --release -- --ids VCF-I-979 --company "Acme Corp" --use-case "Testing proxy"
# If dry run looks good, go live
cargo run --release -- --ids VCF-I-979 --live --company "Acme Corp" --use-case "Critical for compliance"| Block | Location | Purpose |
|---|---|---|
js_open |
~line 227 | Clicks the button that opens the proxy vote dialog |
js_fill |
~line 254 | Fills in company name and use case fields |
js_submit |
~line 296 | Clicks the submit/save button to confirm the proxy vote |
- Use Chrome DevTools Console to test selectors live before editing code:
// Test if your button text match works: document.querySelectorAll('button').forEach(b => console.log(b.textContent.trim()))
- The tool always pauses before submitting — you'll see the filled dialog and can review/edit before pressing ENTER
- If the proxy dialog doesn't exist on your portal, the tool gracefully skips it and just votes normally
- After updating selectors, run
cargo build --releaseto recompile
This tool assists with navigation and clicking. It does not:
- Store, transmit, or process any credentials
- Bypass CAPTCHAs, bot detection, or rate limits
- Perform any action without explicit human confirmation
Use responsibly and in accordance with the portal's Terms of Use.
MIT — do whatever you want with it. If you end up building a CLI for your team's internal voting chore, we'd love to hear about it.
"We over-engineered a voting clicker and we're not sorry."