- Rust 98.1%
- Shell 1.9%
| apps | ||
| crates | ||
| docs/addressed/todos | ||
| tests | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| install.sh | ||
| README.md | ||
| ROADMAP.md | ||
| rust-toolchain.toml | ||
| TODO.md | ||
| TUTORIAL.md | ||
whoshome
whoshome is a Linux-first presence detector for Bluetooth and passive Wi-Fi devices.
It runs best as a long-lived local daemon and gives you a CLI plus a full-screen TUI for:
- seeing who is probably home right now
- labeling every device you can identify
- grouping devices under higher-level entities like
roommate0 - reviewing timelines, patterns, and signal strength history
- exposing derived signals like
residents-absent,unexpected-present, andwatchlist-absent - firing local hooks when arrivals, departures, or signals change
binaries
whoshomed: background scanner, API server, hook runnerwhoshome: CLI and TUI client
quick install
./install.sh
That script:
- installs
whoshomeandwhoshomedwithcargo install --path . --force - creates a starter config in
~/.cache/whoshome/config.tomlif one does not exist - starts a service for
whoshomedsystemd --userwhen Wi-Fi capture is disabled- a root-managed system service (
whoshomed-$USER.service) when Wi-Fi capture is enabled and passwordlesssudois available
Useful follow-up commands:
systemctl --user status whoshomed.service
sudo systemctl status whoshomed-$USER.service
journalctl --user -u whoshomed.service -f
sudo journalctl -u whoshomed-$USER.service -f
whoshome status
whoshome signals
whoshome tui
runtime model
The intended setup is:
whoshomedalways running in the backgroundwhoshometalking to the daemon over the local Unix socket when available- clean fallback to direct local storage access if the daemon socket is unavailable
The daemon stores state in:
~/.cache/whoshome/state.db~/.cache/whoshome/config.toml~/.cache/whoshome/sock~/.cache/whoshome/logs/
starter config behavior
The installer creates a conservative starter config:
- Bluetooth enabled on
hci0 - passive Wi-Fi disabled by default
When Wi-Fi capture is already enabled in your config, ./install.sh now also tries to grant
CAP_NET_ADMIN and CAP_NET_RAW to the installed whoshomed binary, plus CAP_NET_ADMIN
to /usr/sbin/ip and /usr/sbin/iw, because the current monitor-mode workflow shells out to
those tools to switch interface mode.
This is intentional. Passive Wi-Fi monitor mode usually needs extra privileges or adapter-specific setup, while Bluetooth is much more likely to work cleanly in a user service.
If Wi-Fi capture is enabled and passwordless sudo is available, ./install.sh prefers a dedicated system service so the daemon can hold the privileges needed for monitor mode and packet capture while still running as your user. If that path is unavailable, the practical options are:
- give the installed daemon binary
CAP_NET_ADMINandCAP_NET_RAW - give
/usr/sbin/ipand/usr/sbin/iwCAP_NET_ADMINwhile using the current helper flow - run the daemon in a more privileged service model
- keep Wi-Fi disabled and use Bluetooth-only collection
For example, after installing to ~/.cargo/bin/whoshomed:
sudo setcap cap_net_admin,cap_net_raw+ep "$HOME/.cargo/bin/whoshomed"
sudo setcap cap_net_admin+ep /usr/sbin/ip /usr/sbin/iw
./install.sh
Re-running cargo install --path . --force replaces the daemon binary, so you would need to re-apply setcap to whoshomed after reinstalling. The capabilities on /usr/sbin/ip and /usr/sbin/iw persist until you change or replace those binaries. The simplest path is usually to re-run ./install.sh, which will refresh the service model and helper capabilities for you.
To enable Wi-Fi later, edit ~/.cache/whoshome/config.toml and re-run ./install.sh. The installer will choose the appropriate service model and restart the daemon for you.
./install.sh
common commands
whoshome status
whoshome devices --unknown
whoshome label AA:BB:CC:DD:EE:FF phone-roommate0
whoshome onboard AA:BB:CC:DD:EE:FF phone-roommate0 --entity roommate0 --entity-kind person --kind phone --entity-tag resident
whoshome onboard AA:BB:CC:DD:EE:FF phone-roommate0 --also-device 11:22:33:44:55:66 --entity roommate0 --kind phone --alias roommate0-phone
whoshome alias add phone-roommate0 alex-phone
whoshome entity create roommate0 --kind person
whoshome attach alex-phone roommate0 --kind phone
whoshome tag add roommate0 resident
whoshome note set alex-phone "Usually appears after 18:00"
whoshome history roommate0 --hours 24
whoshome stats phone-roommate0 --days 7
whoshome signals
whoshome kinds
source <(whoshome completion bash)
whoshome hooks list
whoshome db prune
whoshome tui
whoshome kinds prints the built-in vocab for:
- entity kinds:
person,device,household,room,place,neighbor,vehicle,pet - device kinds:
phone,laptop,tablet,watch,earbuds,headphones,speaker,tv,console,tracker,keys,bag,accessory,appliance,vehicle - suggested tags and supported hook events
Entity kinds, device kinds, and hook events are validated in the CLI and TUI. Tags are deliberately freeform, but the built-in tag list is what completion and the default signal policy understand out of the box.
shell completion
whoshome can generate dynamic shell completion that includes:
- built-in entity kinds and device kinds
- built-in tags and hook events
- known raw ids, labels, aliases, entities, hooks, and signals from your live cache/daemon state
Examples:
source <(whoshome completion bash)
whoshome completion zsh > ~/.zfunc/_whoshome
whoshome completion fish > ~/.config/fish/completions/whoshome.fish
After loading completion, fields like --kind, --entity-kind, --tag, show, attach, onboard, and even raw observed ids become completable.
onboarding workflows
Fastest CLI flow for a newly recognized device:
whoshome onboard AA:BB:CC:DD:EE:FF phone-roommate0 \
--entity roommate0 \
--entity-kind person \
--kind phone \
--device-tag personal \
--entity-tag resident
AA:BB:CC:DD:EE:FF here means a raw observed identifier from whoshome devices --unknown or the TUI inbox. It can be either a Bluetooth MAC or a Wi-Fi MAC.
If you know both radio identifiers for the same physical device, you can onboard them together:
whoshome onboard AA:BB:CC:DD:EE:FF phone-roommate0 \
--also-device 11:22:33:44:55:66 \
--entity roommate0 \
--kind phone \
--alias roommate0-phone \
--entity-tag resident
That workflow:
- sets the primary device label
- auto-labels extra raw ids with derived suffixes like
-btor-wifi - optionally creates the entity if it does not exist yet
- attaches all onboarded raw ids to the entity with an optional role like
phone - applies device and entity tags in one shot
When Bluetooth or Wi-Fi advertisements include a recognizable human-readable device name, whoshome shows that name in the CLI and TUI lists. In practice this is often the fastest clue for onboarding because it may match what Android shows in Bluetooth or Wi-Fi settings.
The TUI has matching workflows:
oonboard the selected device withlabel | entity[:entity-kind] | member-kind | alias1,alias2 | extra-id1,extra-id2lset the primary labelaadd an aliasgattach the selected device to an entitytadd a tagssnooze the selected device so it drops out ofunexpected-presentbbulk-tag the unknown inbox withtag | countnadd or replace a noteuupsert a hook from the live dashboarddtoggle an existing hook withhook-name | on/offyseed a canned hook template withtemplate | hook-namevcycle inspect views between overview, timeline, and rules/events
Start it with:
whoshome tui
hooks
Hooks run from the daemon on matching events. Hook event names are validated against whoshome kinds; hook commands are trusted shell commands executed with sh -c, a 30-second timeout, and WHOSHOME_* environment variables describing the event.
The daemon secures its Unix socket under the cache directory before serving. Treat access to that socket as administrative access to this local whoshome instance, because it can label devices, edit hooks, and change automation behavior.
Create one:
whoshome hooks add watchlist-absent-on watchlist_absent "notify-send WatchlistAbsent"
List them:
whoshome hooks list
Recent daemon events are also queryable directly:
whoshome events --limit 20
Two appearance-inference events are also emitted for first-seen raw ids when the system has enough signal to make a guess:
device_returning_guess: this new raw id probably matches a previously seen device that has come back with a different identifierdevice_new_guess: this raw id looks genuinely new
Those events include a confidence score and the best prior-match candidate when one exists, so they can be used for notifications or manual triage.
The TUI rules view supports the same loop without leaving the dashboard:
ucreate or replace a hook withname | event_type | shell commanddtoggle an existing hook on or offycreate a canned desktop notification hook from a template likedesktop-unexpected | notify-unexpectedtabcompletes structured composer fields using built-in kinds/tags/events and your current devices, entities, and hooks- repeated
taborrightcycles forward through fuzzy-matched candidates, andshift-taborleftcycles backward ctrl-naccepts the current completion and jumps to the next structured field when the workflow supports it
When a TUI composer is open, Tab starts a completion session, applies the current candidate, and shows a richer suggestion strip with short descriptions in the popup. Matching is fuzzy, so partial fragments like rm0, veh, or watch can still surface useful candidates. Repeated Tab/Right and Shift-Tab/Left step through the available values, and Ctrl-N accepts the current completion and moves you to the next field.
Useful built-in hook templates now include:
desktop-unexpecteddesktop-watchlistdesktop-residents-absentdesktop-returning-devicedesktop-new-device
Hook commands receive these environment variables:
WHOSHOME_HOOK_NAMEWHOSHOME_EVENT_TYPEWHOSHOME_SUBJECT_KINDWHOSHOME_SUBJECT_KEYWHOSHOME_EVENT_IDWHOSHOME_EVENT_ATWHOSHOME_EVENT_PAYLOAD_JSON
derived signals
Current built-in signals:
residents-absentunexpected-presentwatchlist-absent
Inspect them with:
whoshome signals
whoshome query signal:watchlist-absent
Signal policy is configurable in ~/.cache/whoshome/config.toml under [signals].
Useful knobs include:
residents_absent_tagsandresidents_absent_fallback_to_any_entityunexpected_present_tags,unexpected_include_unknown_unlabeled, andunexpected_ignore_tagswatchlist_absent_block_tagsandwatchlist_absent_fallback_tags
Example:
[signals]
residents_absent_tags = ["resident"]
residents_absent_fallback_to_any_entity = false
unexpected_present_tags = ["suspicious"]
unexpected_include_unknown_unlabeled = true
unexpected_ignore_tags = ["ignore", "external", "snoozed"]
watchlist_absent_block_tags = ["watchlist"]
watchlist_absent_fallback_tags = ["resident", "guest"]
storage retention
whoshome stores compact evidence, not a packet archive. The daemon automatically applies retention during normal writes:
- raw sightings are stored at most once every 5 minutes per device and retained for 14 days
- per-device presence is materialized as current state instead of sampled forever; entity and signal samples are retained for 7 days
- daemon events are retained for 14 days, with a global cap
To clean up an existing oversized cache and reclaim disk space after pruning, stop whoshomed and run:
whoshome db prune
Use whoshome db prune --no-compact if the daemon is still running and you only want to delete rows outside retention without running SQLite VACUUM.
Wi-Fi noise filtering
Passive monitor-mode Wi-Fi is inherently noisy, so whoshome now drops several obvious low-value observations before they ever hit storage:
- only selected management frames are kept (
association_request,probe_request,authentication,disassociation) - beacons, probe responses, data frames, control frames, broadcast senders, multicast MACs, and all-zero MACs are ignored
- repeated sightings for the same raw id are coalesced to at most one stored record every 5 minutes
- especially low-value
00:00:*Wi-Fi junk is filtered out of the unknown inbox, recent events, andunexpected-present - weak one-off unlabeled Bluetooth sightings are also suppressed from
unexpected-presentuntil they look more substantial
This keeps unexpected-present, the unknown inbox, and the event stream much more usable while still preserving genuinely useful nearby-device evidence.
notes
- Bluetooth and passive Wi-Fi are probabilistic signals.
whoshometries to expose confidence and evidence, not fake certainty. - The first time you install as a user service, you may want lingering enabled if you want scanning to continue after logout:
sudo loginctl enable-linger "$USER"