A live roster of who has introduced themselves — for meetings where everyone goes around the room.
In meetings where everyone introduces themselves, somebody always asks "wait, has Alex gone yet?" and the room loses thirty seconds. Icebreaker Tracker puts the answer on screen for everyone: a live roster, who has been marked introduced, and who is coming up next.
Screen-share the page so the whole room can self-pace. It reads Zoom's Participants panel automatically on macOS and Windows, or you type names in by hand — the page looks and works the same either way.
One local process, one command. No Node, no Zoom account, no credentials, and nothing leaves your machine.
No meeting handy? The first screen has a Try a demo button that loads this sample roster so you can click around. Leaving demo mode clears the slate.
- In the browser, no install — https://rlorenzo.github.io/zoom-icebreaker/. The same page, running entirely client-side: add names by hand, mark people introduced, reorder, set a prompt, screen-share the tab. Your session is kept in the browser (localStorage) so a refresh mid-meeting doesn't lose the slate, and nothing is sent anywhere. The only thing it can't do is read Zoom's panel for you (that needs OS accessibility access) — so it's manual entry, and everything else is identical.
- Locally, with automatic Zoom reading — one Python command, below. Adds auto-filling the roster from Zoom's Participants panel on macOS and Windows.
uv sync # one-time: install dependencies
uv run tracker.pyOpen http://localhost:3000 and screen-share that browser tab in Zoom. That's the whole setup — names start filling in (or add them yourself), and you tap one button per person as they speak.
Don't have
uv? It's a single-binary Python installer — see the uv install guide. Or skip it entirely and use the hosted version.
- A live roster that updates for everyone watching the share, no refresh.
- One-tap "introduced" per person. The counts and the highlighted "up next" row update instantly.
- Automatic Zoom reading on macOS and Windows — it fills the roster from the Participants panel so you rarely type a name.
- Manual mode anywhere — no permission or wrong OS? Type names in. The page is identical.
- Private by design — no account, no history, and nothing sent anywhere. In the local app, state lives only in memory and is gone when you stop the process; the hosted version keeps it in your browser (localStorage) so a refresh doesn't lose the meeting, and clearing it is a click away.
The host and the room see the same page, and both matter:
- The host runs the command, opens the page, and screen-shares it. They are the only person who clicks anything — toggling "introduced" as each person speaks, occasionally adding a name, hitting reset at the end.
- The room watches over screen-share. They can't interact; they read. They want to know who has gone, who hasn't, and whether they're up next — on compressed video, often on a laptop, sometimes on a phone.
So the layout stays legible on a shared screen and reflows cleanly down to phone width. The "introduced" toggle is always manual, by design: it's a judgement call only the host can make as people actually speak.
You can always mix modes — let it auto-read and still add or remove people by hand.
Auto-read watches your screen's accessibility tree to list who is in the Participants panel. It never talks to Zoom's API or SDK, so it doesn't touch Zoom's terms — but it can break when Zoom redesigns the panel (see troubleshooting).
Auto-read needs pyobjc (installed by uv sync) and macOS Accessibility
permission. Grant permission to the app you run the command from (Terminal
or iTerm) in System Settings → Privacy & Security → Accessibility, then
reopen that terminal.
Start your meeting, open the Participants panel, then run uv run tracker.py.
Auto-read uses UI Automation
via the uiautomation package (also installed by uv sync). No extra
permission prompt — UIA is part of the standard Windows accessibility stack.
Start your meeting, open the Participants panel, then run uv run tracker.py. The --anchor-regex, --exclude, and --debug flags below
work the same as on macOS; --bundle is macOS-only and ignored here.
uv run tracker.py, open the URL, and share that tab in Zoom.- People appear automatically (or add them manually). "Started" shows when you began the session.
- Tap Mark introduced after each person speaks. The counts and the highlighted "up next" row update live for everyone watching.
- Reset session clears it for the next meeting.
--port N port to serve on (default 3000)
--interval N seconds between Zoom reads (default 5)
--no-ax manual entry only, never read Zoom
--anchor-regex text identifying the participants container
--exclude "a,b" extra whole-word non-name terms to filter out
--min-len N minimum name length (default 2)
--debug print anchor / raw-node diagnostics
Zoom's accessibility tree is undocumented and changes between versions. If names don't appear, see what Zoom is exposing and tune the matcher:
uv run ax_dump.py --grep '(?i)participant'
uv run tracker.py --anchor-regex 'participants|attendees' --debugKnown limitations:
- Virtualized participant lists may only expose names currently scrolled into view.
- Dial-in users sometimes appear as phone numbers rather than names.
- It reads who is present, not who has spoken. For an automatic "who has actually spoken" signal, a saved Zoom transcript is the better source — ask if you'd like that ingest added.
uv sync --dev # install dev tools
uv run pre-commit install # enable the git hook
uv run pre-commit run --all # run all checks onceTooling: ruff (lint + format), bandit (security), lizard (complexity),
pymarkdown (markdown), plus biome and html-validate for the web assets. The
same checks run on every PR via
.github/workflows/ci.yml.
The web assets (index.html, app.js, roster.js, demo.js, engine.js,
session.js, styles.css) are read into memory once at startup, so the server
never opens a file in response to a request. The trade-off: editing any of them
requires restarting the server (Ctrl-C and re-run) to see the change.
The same assets are deployed to GitHub Pages by
.github/workflows/pages.yml on every push to
main. There, app.js finds no /events backend and falls back to the local
in-browser engine (engine.js); tracker.py is not involved at all.
The README clip (docs/demo.webm, VP9) is generated from demo mode, so it
never needs a real meeting. With the server running, Playwright installed
(npx playwright install chromium), and an ffmpeg with libvpx-vp9 on PATH
(brew install ffmpeg, or point $FFMPEG at one), regenerate it with
npm run record:demo.
If this saved your meeting a few awkward seconds, you can sponsor the project on GitHub.