Skip to content
Back to skills

Meetbot

BSecurity

The Rust meeting-recorder that replaced the Vexa Docker stack - how to run it, send bots, check health, roll back, and the landmines that cost hours to find. Use whenever a meeting was not recorded, the bot misbehaved in a call, or the recorder needs restarting/rolling back.

  • 33 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added September 23, 2026
ai-agentspythonrustgobashsqldockerdebugginggitapibackend

Works with

  • terminal
  • cli
  • api

Security analysis

B76/100
  • mediumUses curl or wget to download content
  • highCreates or modifies cron jobs for persistent execution
  • criticalDownloads and executes remote scripts — classic supply chain attack

Pro shows the line behind each finding and how to fix it

Scanned September 23, 2026

npx -y skills add BrianArfi/ai-second-brain --skill meetbot --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Meetbot?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Meetbot
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/brianarfi-meetbot/badge)](https://www.skillsdirectory.com/skills/brianarfi-meetbot)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: meetbot
description: The Rust meeting-recorder that replaced the Vexa Docker stack - how to run it, send bots, check health, roll back, and the landmines that cost hours to find. Use whenever a meeting was not recorded, the bot misbehaved in a call, or the recorder needs restarting/rolling back.
---

## Where the code lives

Source is tracked in the repo at `meetbot/`. The live deployment at `~/tools/meetbot`
**symlinks** `src/`, `assets/`, `spike/` and `Cargo.*` back to it, so there is one source
of truth and no drift. Only the runtime lives in the deployment dir and stays out of git:
`bin/` (binaries), `data/` (SQLite + the signed-in Chrome profile), `config.toml` (holds
the admin token), `target/`. `meetbot/config.example.toml` is the scrubbed template.

Note: these paths are WSL-host-specific. `~/tools/meetbot` and
`.` do not exist on macOS; meetbot
runs on the WSL automation host only.

## What this is

`meetbot` is a single Rust binary at `~/tools/meetbot` that joins the owner's
Google Meet / Teams meetings as a bot, captures call audio, sends it to the external
whisper.cpp server on the Windows host, and serves transcripts over an HTTP API that is
a **drop-in replacement for vexa-lite**.

It replaced a 1.25 GiB always-on Docker stack (vexa-lite + postgres + minio) with a
~3 MB idle process. The Python client `meeting-recorder/vexa_bots.py` drives both, and
switches between them on one env var, so rollback is one crontab edit.

| | vexa (rollback) | meetbot (live) |
|---|---|---|
| Runs as | 3 Docker containers | `systemd --user` unit `meetbot.service` |
| API | `:8056` | `:8060` |
| Store | postgres + minio | SQLite `data/meetbot.db` |
| Idle RAM | ~1.25 GiB | ~3 MB |

## Running it

```bash
systemctl --user status meetbot.service          # is it up
systemctl --user restart meetbot.service         # the standard remedy
journalctl --user -u meetbot.service -f          # watch a live join
curl -s http://localhost:8060/                   # liveness (200 = alive)
curl -s http://localhost:3737/api/vexa-health | python3 -m json.tool   # full health
```

Deploying a rebuild — the service holds the binary open, so `cp` fails with
`Text file busy`. Move it aside first:

```bash
export PATH="$HOME/.cargo/bin:$PATH"          # cargo is NOT on the default PATH
cd ~/tools/meetbot
cargo build --release && cargo test && cargo clippy --all-targets
cd bin && mv meetbot-live meetbot-live.bak-$(date +%s) && cp ../target/release/meetbot meetbot-live
systemctl --user restart meetbot.service
```

The service deliberately runs `bin/meetbot-live`, a **snapshot copy**, not
`target/release/meetbot` — so an in-progress rebuild can never be picked up by a restart
mid-meeting.

## Sending a bot by hand

```bash
cd .
MEETBOT=1 VEXA_API_BASE=http://localhost:8060 python3 meeting-recorder/vexa_bots.py \
  send --meet "https://meet.google.com/xxx-yyyy-zzz" --title "Meeting name"

# what would the cron cycle do right now, without doing it:
MEETBOT=1 VEXA_API_BASE=http://localhost:8060 python3 meeting-recorder/vexa_bots.py auto --dry-run

# diagnose a join failure stage-by-stage against a real meeting:
cd ~/tools/meetbot && ./bin/meetbot-live doctor <meet-code>
```

`doctor` is the maintenance tool: it dry-runs the whole join dance and names the exact
selector that failed. Reach for it first when Google changes the Meet UI.

## Cutover and rollback

Cutover is the `MEETBOT=1 VEXA_API_BASE=...` prefix on crontab line 29. Rollback is
removing it. **Back up the crontab before editing it** — see
[[feedback_crontab_edit_safety]].

```bash
crontab -l > /tmp/crontab.backup-$(date +%F-%H%M)     # always
crontab -l | grep "vexa_bots.py auto"                  # verify after
```

Vexa's containers are kept **warm, not deleted**, because a cold rollback is not a
rollback. If they are `Exited`, `docker start vexa-postgres vexa-lite` restores it.

## Landmines (each of these cost real debugging time)

**Google refuses automated browsers before the green room.** Two signals are
independently fatal: `navigator.webdriver === true` (chromiumoxide appends
`--enable-automation` from its own DEFAULT_ARGS, so `--disable-blink-features=AutomationControlled`
is mandatory) and a `HeadlessChrome` UA token. Symptom is identical for both:
`You can't join this video call`, refused in ~5 s, no name field and no join button at
all. A merely *stale* UA version is survivable — do not chase that.

**Meet reuses the same copy for two different failures.** `You can't join this video
call` means BOTH "refused before the green room" and "your knock was declined". They
have completely different causes. Check whether the log shows a `clicked join` line
before the denial — that is the only thing distinguishing them.

**The bot joins as the owner, so it can eject the owner.** When the same identity is already in
the call, Meet offers `Join here too` AND `Switch here`. `Switch here` MOVES the session
to the bot and drops the owner's — it did exactly that during a live Marketplace scrum on
20 Jul. `JOIN_BUTTON_TEXTS` order is load-bearing: `Join here too` must sit above the
bare `Join` prefix, and `Switch here` must never be in that list.

**`Join here too` is hidden behind a collapsed disclosure.** It lives inside
`Other ways to join`, whose collapsed children have a zero-size bounding box — and the
matcher skips zero-size elements. So the ONLY visible control is the session-stealing
one. Expand the disclosure before searching for a join control. This is why the logs
honestly read "the only control offered was Switch here" while the owner could plainly see
`Join here too` on his screen: both were true, from different vantage points.

**Material icon ligatures are text inside the button.** A control's `textContent` comes
back as `add_to_queueJoin here too`, so prefix matching misses it entirely. Both matchers
strip a leading lowercase/underscore run that butts against a capital (`__delig`). Any
new selector anchored on visible text inherits this hazard.

**A revoked Google session is worse than no session.** When Google invalidates the
stored login server-side, the cookies remain on disk (expiry years out) but Chrome still
sends them, Google rejects them and bounces to `/landing?pli=1` — which kills the guest
path too, so BOTH join routes die at once. Symptom: `clicked join` followed by a
navigation to `landing?...pli=1`. Remedy is re-seeding the profile, not clearing it.
Watch whether a re-seeded session survives more than a day: if it keeps getting revoked,
the copy-profile-per-session pattern is what trips Google's device heuristics, and the
architecture (not the login) is what needs changing.

**The fake camera is visible to everyone.** Chrome runs with
`--use-fake-device-for-media-stream`, whose "camera" is a green test pattern. The bot
turns camera + mic off in the green room before entering; if that ever regresses, the
whole room sees it.

**Recurring meetings share one Meet code.** `vexa_state.json` is keyed by
`platform/code`, so before 20 Jul the first occurrence permanently suppressed every
later one — four of that day's meetings were blocked by records from 8-17 Jul, silently.
Only an occurrence already handled **today** may suppress a join.

**Zero-segment terminal rows are not all the same.** `completed` + 0 segments is a
benign skip (never admitted / nobody spoke). `failed`/`stopped` + 0 segments is a
recorder failure and raises a **fail** heartbeat. The client checks status BEFORE
segment count; reversing that order laundered real losses into green heartbeats.

**cmd_auto reads the Work calendar only** (`gcal --profile work`). Meetings on the owner's
personal calendar are invisible to it — that is not a meetbot bug and vexa behaves the
same.

## Signed-in profile

The bot joins as an invited participant using a persistent Chrome profile at
`data/bot-profile` (config key `profile_template`). Anonymous guests lose: Meet runs a
bot check on the knock itself and auto-declines in ~1.5 s.

Each session gets a **copy** of that profile — Chrome locks a `user-data-dir`, so
concurrent bots sharing one would fight over it, and a session must never be able to
invalidate the stored login. Caches are skipped so the copy stays cheap.

Re-seeding it (when the Google session finally expires) needs a headed browser. WSLg is
available, so this opens a real window on the Windows desktop:

```bash
~/.cache/ms-playwright/chromium-1228/chrome-linux64/chrome \
  --user-data-dir=~/tools/meetbot/data/bot-profile \
  --no-first-run --no-default-browser-check --no-sandbox https://accounts.google.com/
```

Sign in with the **Work** account, then close the window. Use the same Chromium binary
meetbot uses — a session seeded in a different browser build gets re-verified by Google.

## Observability

The dashboard probe reads the **crontab** to decide which recorder is in charge, because
both ports answer while vexa is warm and liveness cannot identify the owner. The payload
carries an explicit `backend` field; an undeterminable backend reports `unknown`, never
green. Verify the monitor can go RED (`systemctl --user stop meetbot.service`, re-probe)
after touching it — a monitor that cannot go red is decoration.

`/tmp/vexa_auto.log` gets one line per cron cycle even when nothing happens, so log
staleness is a real signal rather than being indistinguishable from an idle day.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.