homenet-device-profile
Purpose
Answer "what is this endpoint and what is it doing?" for every device on the home LAN by combining:
- UniFi: active + historical clients, MAC, hostname, vendor OUI, IP, network/SSID, wired/wireless, uptime, signal
- Pi-hole: top clients by query count, top permitted/blocked domains, blocked %, per-IP activity
Result: one markdown report grouped by device type, with trust tier + cadence + DNS behavior per device. Unknown / unclassified devices get their top 5 permitted domains listed so they can be labeled manually.
Scope
- Device-first. Profiles describe endpoints, not users. Temporal (time-of-day) analysis is off by default and only enabled via
--include-temporal.
- Read-only. Never mutates UniFi or Pi-hole. If the profile suggests action (e.g. MAC should join an allowlist, volume spike on an IoT device), surface as a recommendation. Actuation lives in
homenet-allow-mac, homenet-filter, etc.
Invocation
/homenet-device-profile [--window DAYS] [--device MAC] [--include-temporal] [--write] [--force]
| Flag |
Default |
Meaning |
--window DAYS |
7 |
Pi-hole lookback (Pi-hole retention is whatever is configured; we report what is available) |
--device MAC |
unset |
Narrow to single device; emit deep-dive subsection |
--include-temporal |
off |
Include time-of-day cadence (opt-in for privacy) |
--write |
off |
Persist to HomeNetwork/devices/device-profiles.md; default is stdout only |
--force |
off |
Overwrite even if HomeNetwork tree has uncommitted changes |
Data sources
UniFi MCP
mcp__unifi__list_clients — current snapshot (IP, MAC, hostname, network, wired/wireless, signal, uptime, tx/rx bytes)
mcp__unifi__list_all_clients — historical (MAC, hostname, ID)
mcp__unifi__list_mac_filter — SSID allowlist membership
mcp__unifi__list_networks — network / VLAN map (for trust-tier assignment)
mcp__unifi__list_wlans — SSID map
mcp__unifi__get_client_history — per-device session history (only when --device is set)
Envelope gotcha: UniFi Network API v2 list endpoints return a bare JSON list, not the {meta, data} envelope used by v1 endpoints. Code that does response['data'] uniformly will hit AttributeError on v2 results. When consuming raw JSON here, check the shape before indexing (isinstance(resp, list) vs dict) or route v1 and v2 through separate extractors.
Pi-hole MCP
mcp__pihole__get_stats — LAN-wide baseline (total queries, total blocked)
mcp__pihole__get_top_clients count=50 — volume per client IP
mcp__pihole__get_top_clients count=30 blocked=true — blocked-volume per client IP
mcp__pihole__get_top_permitted count=50 — LAN-wide domain baseline
mcp__pihole__get_top_blocked count=30 — LAN-wide blocked-domain baseline
mcp__pihole__get_history — time-series (only when --include-temporal is set)
mcp__pihole__get_query_log filter — per-client deep dive (only when --device is set)
Execution recipe (agent instructions)
- Gather UniFi and Pi-hole data in parallel via the MCP tools listed above. Keep raw JSON in memory.
- Build join table keyed by current IP:
- Left join UniFi-active clients onto Pi-hole per-IP query counts.
- For UniFi-historical-only MACs, note them in an "offline" section; no DNS join.
- Flag
ip-drift if the same IP appears bound to multiple different MACs in UniFi's historical data within the window.
- Privacy-MAC handling: MAC with locally-administered bit (2nd nibble of first byte ∈ {2,6,a,e}) → mark as
privacy-mac-pool. Aggregate per SSID; do not emit per-MAC profile for privacy MACs since they rotate.
- Classify each non-privacy MAC using
~/.claude/scripts/device-signatures.yml:
- Type from OUI + DNS-cluster rules (first match wins). Fallback
unclassified.
- Trust tier from
list_mac_filter + network assignment.
- Cadence from
uptime_human + wired vs wireless (long uptime wired = infrastructure-like).
- DNS behavior =
volume_quintile (vs LAN baseline) + blocked_pct + top 3 matched dns_categories.
- Identity key. UniFi
dev_id is a DHCP fingerprint cluster, not a 1:1 device identifier. Multiple physical devices with similar DHCP fingerprints map to one dev_id. Always key profiles on the stable MAC plus hostname, never on dev_id.
- Diff against
~/.claude/state/device-profiles/last.json:
- New MACs since last run.
- New top-10 domains per device.
- Volume spike (2x+ rolling avg).
- Emit markdown report (see format below).
- If
--write: atomically replace HomeNetwork/devices/device-profiles.md. Sanity-check no plaintext passwords/PSKs/PPSK keys. Update ~/.claude/state/device-profiles/<ts>.json and symlink last.json.
Output format
# Device Profiles
Generated: <ISO>
Window: <N days>
UniFi snapshot: <snapshot ts or "live">
Pi-hole lookback: <since ts>
LAN baseline: <total_queries> queries, <blocked_pct>% blocked
## Summary
- Active devices: <N> (<W> wireless, <R> wired)
- Historical-only devices: <M>
- Classification coverage: <X>% typed
- Privacy-MAC pool size: <P>
- Flags: <count> ip-drift, <count> new-since-last, <count> volume-spike
## Active devices by type
### <type> (count)
| MAC | Hostname | IP | Trust | Cadence | Volume (quintile / raw) | Blocked % | Top DNS categories | Flags |
## Privacy-MAC pool
(per SSID aggregate)
## Unclassified devices
| MAC | Hostname | Vendor (OUI) | Top 5 permitted domains |
## Drift vs last run
(empty on first run)
## Recommendations
- Unlabeled active devices: ...
- Devices on allowlist with zero DNS in window: ...
- Devices with volume spike > 2x rolling avg: ...
Safety / refusals
- Read-only MCP calls only. No
update_*, create_*, delete_*, add_mac_filter, block_client etc.
- Before
--write: scan generated markdown for x_passphrase, password=, raw PPSK hex, full URLs with query strings. Refuse on match.
- Refuse
--write if HomeNetwork/devices/device-profiles.md has uncommitted git changes, unless --force.
- Privacy MACs are aggregated only.
--include-temporal requires explicit opt-in and gets a privacy note in the report header.
First-run note
The first run has no ~/.claude/state/device-profiles/last.json, so the Drift vs last run section will be "first run — no baseline, drift tracking begins with the next invocation." This is expected and not an error.
Related skills
homenet-document — full UniFi state dump. This skill reuses its snapshot if fresh, else fetches live.
homenet-review — allowlist reconciliation. This skill may inform a review but never mutates.
homenet-allow-mac / homenet-deny-mac / homenet-filter — mutation skills for any action the profile recommends.
Note on destructive siblings: every mutating homenet-* skill (homenet-filter, homenet-ppsk-add, homenet-ppsk-remove, homenet-allow-mac, homenet-deny-mac) uses a preview-then-apply flow with auto-snapshot and lockout guards (refuse empty-allowlist enables, refuse ppsk-remove that would brick the SSID). If the profile surfaces a recommendation, hand off to the relevant skill rather than calling the MCP directly, so the safety rails apply.
1---2name: homenet-device-profile3description: Generate a device-first behavior profile of the home LAN by joining UniFi client state with Pi-hole DNS data. Read-only. Classifies each device by type, trust tier, online cadence, and DNS behavior using explicit rules in ~/.claude/scripts/device-signatures.yml. Default output is stdout; --write persists to HomeNetwork/devices/device-profiles.md.4---56# homenet-device-profile78## Purpose910Answer "what is this endpoint and what is it doing?" for every device on the home LAN by combining:1112- **UniFi:** active + historical clients, MAC, hostname, vendor OUI, IP, network/SSID, wired/wireless, uptime, signal13- **Pi-hole:** top clients by query count, top permitted/blocked domains, blocked %, per-IP activity1415Result: one markdown report grouped by device type, with trust tier + cadence + DNS behavior per device. Unknown / unclassified devices get their top 5 permitted domains listed so they can be labeled manually.1617## Scope1819- **Device-first.** Profiles describe endpoints, not users. Temporal (time-of-day) analysis is off by default and only enabled via `--include-temporal`.20- **Read-only.** Never mutates UniFi or Pi-hole. If the profile suggests action (e.g. MAC should join an allowlist, volume spike on an IoT device), surface as a recommendation. Actuation lives in `homenet-allow-mac`, `homenet-filter`, etc.2122## Invocation2324```25/homenet-device-profile [--window DAYS] [--device MAC] [--include-temporal] [--write] [--force]26```2728| Flag | Default | Meaning |29|---|---|---|30| `--window DAYS` | `7` | Pi-hole lookback (Pi-hole retention is whatever is configured; we report what is available) |31| `--device MAC` | unset | Narrow to single device; emit deep-dive subsection |32| `--include-temporal` | off | Include time-of-day cadence (opt-in for privacy) |33| `--write` | off | Persist to `HomeNetwork/devices/device-profiles.md`; default is stdout only |34| `--force` | off | Overwrite even if HomeNetwork tree has uncommitted changes |3536## Data sources3738### UniFi MCP39- `mcp__unifi__list_clients` — current snapshot (IP, MAC, hostname, network, wired/wireless, signal, uptime, tx/rx bytes)40- `mcp__unifi__list_all_clients` — historical (MAC, hostname, ID)41- `mcp__unifi__list_mac_filter` — SSID allowlist membership42- `mcp__unifi__list_networks` — network / VLAN map (for trust-tier assignment)43- `mcp__unifi__list_wlans` — SSID map44- `mcp__unifi__get_client_history` — per-device session history (only when `--device` is set)4546Envelope gotcha: UniFi Network API v2 list endpoints return a bare JSON list, not the `{meta, data}` envelope used by v1 endpoints. Code that does `response['data']` uniformly will hit `AttributeError` on v2 results. When consuming raw JSON here, check the shape before indexing (`isinstance(resp, list)` vs `dict`) or route v1 and v2 through separate extractors.4748### Pi-hole MCP49- `mcp__pihole__get_stats` — LAN-wide baseline (total queries, total blocked)50- `mcp__pihole__get_top_clients count=50` — volume per client IP51- `mcp__pihole__get_top_clients count=30 blocked=true` — blocked-volume per client IP52- `mcp__pihole__get_top_permitted count=50` — LAN-wide domain baseline53- `mcp__pihole__get_top_blocked count=30` — LAN-wide blocked-domain baseline54- `mcp__pihole__get_history` — time-series (only when `--include-temporal` is set)55- `mcp__pihole__get_query_log filter` — per-client deep dive (only when `--device` is set)5657## Execution recipe (agent instructions)58591. **Gather** UniFi and Pi-hole data in parallel via the MCP tools listed above. Keep raw JSON in memory.602. **Build join table** keyed by current IP:61 - Left join UniFi-active clients onto Pi-hole per-IP query counts.62 - For UniFi-historical-only MACs, note them in an "offline" section; no DNS join.63 - Flag `ip-drift` if the same IP appears bound to multiple different MACs in UniFi's historical data within the window.643. **Privacy-MAC handling:** MAC with locally-administered bit (2nd nibble of first byte ∈ {2,6,a,e}) → mark as `privacy-mac-pool`. Aggregate per SSID; do **not** emit per-MAC profile for privacy MACs since they rotate.654. **Classify** each non-privacy MAC using `~/.claude/scripts/device-signatures.yml`:66 - Type from OUI + DNS-cluster rules (first match wins). Fallback `unclassified`.67 - Trust tier from `list_mac_filter` + network assignment.68 - Cadence from `uptime_human` + wired vs wireless (long uptime wired = infrastructure-like).69 - DNS behavior = `volume_quintile` (vs LAN baseline) + `blocked_pct` + top 3 matched `dns_categories`.70 - **Identity key.** UniFi `dev_id` is a DHCP fingerprint cluster, not a 1:1 device identifier. Multiple physical devices with similar DHCP fingerprints map to one `dev_id`. Always key profiles on the stable MAC plus hostname, never on `dev_id`.715. **Diff** against `~/.claude/state/device-profiles/last.json`:72 - New MACs since last run.73 - New top-10 domains per device.74 - Volume spike (2x+ rolling avg).756. **Emit** markdown report (see format below).767. If `--write`: atomically replace `HomeNetwork/devices/device-profiles.md`. Sanity-check no plaintext passwords/PSKs/PPSK keys. Update `~/.claude/state/device-profiles/<ts>.json` and symlink `last.json`.7778## Output format7980```81# Device Profiles8283Generated: <ISO>84Window: <N days>85UniFi snapshot: <snapshot ts or "live">86Pi-hole lookback: <since ts>87LAN baseline: <total_queries> queries, <blocked_pct>% blocked8889## Summary90- Active devices: <N> (<W> wireless, <R> wired)91- Historical-only devices: <M>92- Classification coverage: <X>% typed93- Privacy-MAC pool size: <P>94- Flags: <count> ip-drift, <count> new-since-last, <count> volume-spike9596## Active devices by type9798### <type> (count)99| MAC | Hostname | IP | Trust | Cadence | Volume (quintile / raw) | Blocked % | Top DNS categories | Flags |100101## Privacy-MAC pool102(per SSID aggregate)103104## Unclassified devices105| MAC | Hostname | Vendor (OUI) | Top 5 permitted domains |106107## Drift vs last run108(empty on first run)109110## Recommendations111- Unlabeled active devices: ...112- Devices on allowlist with zero DNS in window: ...113- Devices with volume spike > 2x rolling avg: ...114```115116## Safety / refusals117118- Read-only MCP calls only. No `update_*`, `create_*`, `delete_*`, `add_mac_filter`, `block_client` etc.119- Before `--write`: scan generated markdown for `x_passphrase`, `password=`, raw PPSK hex, full URLs with query strings. Refuse on match.120- Refuse `--write` if `HomeNetwork/devices/device-profiles.md` has uncommitted git changes, unless `--force`.121- Privacy MACs are aggregated only.122- `--include-temporal` requires explicit opt-in and gets a privacy note in the report header.123124## First-run note125126The first run has no `~/.claude/state/device-profiles/last.json`, so the **Drift vs last run** section will be "first run — no baseline, drift tracking begins with the next invocation." This is expected and not an error.127128## Related skills129130- `homenet-document` — full UniFi state dump. This skill reuses its snapshot if fresh, else fetches live.131- `homenet-review` — allowlist reconciliation. This skill may inform a review but never mutates.132- `homenet-allow-mac` / `homenet-deny-mac` / `homenet-filter` — mutation skills for any action the profile recommends.133134Note on destructive siblings: every mutating homenet-* skill (`homenet-filter`, `homenet-ppsk-add`, `homenet-ppsk-remove`, `homenet-allow-mac`, `homenet-deny-mac`) uses a preview-then-apply flow with auto-snapshot and lockout guards (refuse empty-allowlist enables, refuse ppsk-remove that would brick the SSID). If the profile surfaces a recommendation, hand off to the relevant skill rather than calling the MCP directly, so the safety rails apply.