Paired is a high-capability phone agent. Every powerful behaviour below is an intentional, requested feature, each shipped with a safety control around it — none of it is an oversight or a bug. ClawHub's security scanners mark this skill "Needs review" because it openly discloses these capabilities; that badge reflects what the skill is built to do, not a vulnerability. Every scanner finding maps to one of the deliberate features in this table.
Capability (the feature)What it's forControl around itSend SMS / place callsThe agent texts and calls on your behalfTrusted-numbers allowlist or explicit --confirm per action; an empty allowlist blocks all outgoing SMS/callsSilent SMS send ( sms_send_silent)Headless send on rooted / WRITE_SMS devices where the Intent UI isn't usableOff unless PAIRED_ALLOW_SILENT_SMS=1; otherwise it refuses and points you to the UI send pathAuto-unlock the phoneUnlock a locked device to send/readOff unless --auto-unlock is passed per invocation; PIN is read only from a mode-0600 fileADB device control ( bt-* / ADB)Read notifications, send SMS, drive the screenOwner-operated, own-device-only; shell-level access is inherent to the featurePersistent listeners (systemd user services)Real-time SMS push, incoming-call alerts, command hookYou enable each service yourself; the command hook acts only on HMAC-signed inbox messages, never on raw SMS/chatRelay to TelegramReceive phone events on your own TelegramYour own bot token + chat only; it POSTs to the Telegram API and fetches no remote codeVoice cloningSpeak notes/calls in your own voiceClone your own voice only; disclose AI-generated audio to recipients — not for impersonationBluetooth auto-pairOne-shot "pair this device" convenienceDefault mode is interactive; --mode auto is an explicit opt-in (ideally with --device-filter)
In short: the "Needs review" badge is the expected, honest result for a skill with this much reach. It means "capable — install only on a host and phone you control," not "insecure." The safety model (allowlist + per-action confirm, opt-in flags, HMAC-signed command inbox, fail-closed defaults, own-device-only) is detailed in Consent, privacy & legal below and in the safety: block of this file's frontmatter.
This skill drives a real phone and can speak in a cloned voice. Those capabilities carry obligations that are the operator's responsibility:
bt-*/ADB expose shell-level control of the phone and microphone capture ( arecord) for voice setup. Treat the host, the phone, and every secret file ( pin, inbox.key, gemini-keys.conf, voice reference) as sensitive; keep them mode 0600 and off shared machines.trusted-numbers.conf short, leave auto-unlock and LLM auto-reply off unless you accept the trade-offs, and review the trusted list regularly.You are running on a Linux host with BlueZ + ofono installed and a phone paired over Bluetooth. The skill ships:
skill/bin/bt-*.py — BlueZ/ofono/ADB direct interfacesskill/wrappers/paired-*.py — JSON-clean interfaces designed for agents to callskill/systemd/*.service.txt — for persistent listeners (SMS push, call watch, command hook). The .txt suffix is a packaging convention; rename to .service when copying into ~/.config/systemd/user/ (see Installation below).After clawhub install paired:
bash
# 1. Symlink (or copy) the bin/ and wrappers/ scripts into ~/bin/, dropping .py from filenames
# so the user/agent can invoke `paired-sms-send` rather than `paired-sms-send.py`.
mkdir -p ~/bin
for f in ~/.openclaw/workspace/skills/paired/bin/*.py; do
ln -sf "$f" ~/bin/"$(basename "$f" .py)"
done
for f in ~/.openclaw/workspace/skills/paired/wrappers/*.py; do
ln -sf "$f" ~/bin/"$(basename "$f" .py)"
done
for f in ~/.openclaw/workspace/skills/paired/wrappers/*.sh; do
ln -sf "$f" ~/bin/"$(basename "$f" .sh)"
done
chmod +x ~/.openclaw/workspace/skills/paired/bin/*.py \
~/.openclaw/workspace/skills/paired/wrappers/*.py \
~/.openclaw/workspace/skills/paired/wrappers/*.sh
# 2. Optional: enable systemd user services. Strip the .txt suffix on copy.
mkdir -p ~/.config/systemd/user
for f in ~/.openclaw/workspace/skills/paired/systemd/*.service.txt; do
cp "$f" ~/.config/systemd/user/"$(basename "$f" .txt)"
done
systemctl --user daemon-reload
# 3. One-time inbox HMAC key generation (required for paired-inbox-hook)
paired-inbox-hook --keygen
# 4. Optional: enable the inbox hook (HMAC-signed command dispatcher)
systemctl --user enable --now paired-inbox-hook.service
The .py, .sh, and .service.txt extensions exist to satisfy the ClawHub packaging text-file allowlist; on disk in your ~/bin/ and ~/.config/systemd/user/ they should be the unsuffixed names referenced throughout this document. The same .txt suffix on config-templates/*.conf.example.txt and engines/*.Dockerfile.txt is there for the same reason — drop the trailing .txt when you copy a template, or pass the suffixed name to docker build -f directly.
When reasoning about a phone task, prefer the high-level paired-* wrappers — they handle trust checks, error formatting, and JSON output. Drop to bt-* only for diagnostic or low-level work. The low-level bt-call and bt-sms primitives now also enforce the trusted-numbers allowlist (since v1.0.4) and refuse to dial/SMS unlisted numbers unless --confirm is passed.
Acting on the world vs. answering questions: for status queries ("is my phone connected?", "any new SMS?"), running the tool and reporting the result is the right call. For high-impact actions (sending SMS, dialling calls, pairing new devices, unlocking the phone), confirm with the user first unless the request is unambiguous and the destination is on the trusted-numbers allowlist.
Phone identity comes from ~/.config/paired/paired.conf, key phone_bt_mac. If a command needs the phone's MAC, read it from the config rather than asking the user. If the config is missing, tell the user to copy paired.conf.example.txt and fill in the MAC.
bash
~/bin/bt-test # 10-check stack health (one-shot diagnostic)
~/bin/bt-adapters # list HCI adapters
~/bin/bt-list --paired # paired devices with CONN/PAIR/TRUST status
~/bin/bt-list --connected # only currently-connected
~/bin/bt-list --scan 10 # 10-second scan for nearby
~/bin/bt-info <MAC> # full device detail (UUIDs, RSSI, profiles)
~/bin/bt-recover # USB-reset adapter if hung
bash
~/bin/bt-pair <MAC> # initiate pairing (passkey via bt-agent)
~/bin/bt-pair <MAC> --connect # pair + trust + connect in one step
~/bin/bt-connect <MAC> # connect to an already-paired device
~/bin/bt-disconnect <MAC>
~/bin/bt-trust <MAC> | ~/bin/bt-untrust <MAC>
~/bin/bt-forget <MAC> # remove pairing entirely
Receive (read-only via Bluetooth, fully working on most phones):
bash
~/bin/paired-sms-watch --status # is the MNS push daemon running?
~/bin/paired-sms-watch --last 10 # last 10 SMS the daemon caught
~/bin/bt-sms-list --map <MAC> --max 10 # explicit MAP read of recent
~/bin/bt-adb-sms-list --limit 10 # ADB read of inbox (works while phone is locked)
~/bin/bt-adb-sms-list --sent --limit 10 # sent folder
Send (via ADB-over-USB autosend — Bluetooth MAP send is blocked on most Samsung firmware):
bash
~/bin/paired-sms-send <NUMBER> "<text>" --json
# Pass --auto-unlock to dismiss the lock screen using the PIN at
# ~/.config/paired/pin (mode 0600 enforced). Pass --relock to re-lock after.
# Without --auto-unlock, the tool returns error=keyguard_locked when phone is locked.
Telegram command shortcut: when the user types /sms NUMBER text in Telegram, run ~/bin/paired-sms-send NUMBER "text" --json and report the JSON result. Quote the entire body as one argument.
bash
~/bin/paired-call status --json # active calls in structured form
~/bin/paired-call dial <NUMBER> # initiate outbound
~/bin/paired-call answer # accept incoming
~/bin/paired-call hangup # end all calls
~/bin/paired-call-and-speak <NUMBER> "<msg>" # dial + speak via Tasker TTS (see limits)
~/bin/bt-modems --full # ofono modem state, network registration
~/bin/paired-call-watch --last 10 # last 10 incoming calls caught by daemon
~/bin/paired-call-watch --status # is the call watcher daemon running?
Real-time incoming-call alerts run as a systemd user service ( paired-call-watch.service) — caught calls go to the user's Telegram via paired-call-watch-tg-hook with sender + trust-status info.
Trusted-caller handling is configurable and defaults to notify-only. For a caller on the trusted-numbers list, paired-call-handler reads incoming_trusted_action from paired.conf:
notify (default / fail-closed) — do not touch the call; just send the Telegram alert and let it ring. No hangup, no SMS.hangup — hang up the call, no SMS.hangup_and_sms — hang up and send the "Agent is unavailable…" auto-reply SMS (the previous always-on behaviour; now explicit opt-in).Any unknown or missing value fails closed to notify, so no mistyped or injected value can trigger an automatic hangup or outbound SMS.
paired-sms-command-hook.service reads commands from a dedicated, append-only inbox at ~/.openclaw/paired/inbox/ (NOT from raw agent session logs — see Security model below) and dispatches recognised commands without invoking the LLM:
Telegram commandActionTrust checkUnderlying call/sms <num> <body>Send SMS via ADBtrusted-numbers allowlist required (or --confirm)paired-sms-send``/phone <num>Dial outboundtrusted-numbers allowlist required (or --confirm)paired-call dial``/phone <num> <msg>Dial + speak via Tasker TTS, optional SMS fallbacktrusted-numbers allowlist requiredpaired-call-and-speak``/phone <num> attach <path>Dial + speak file contenttrusted-numbers allowlist requiredas above/phone hangup (or /phone end)End all active callsnonepaired-call hangup``/phone statusActive call statenonepaired-call status
Trusted list at ~/.config/paired/trusted-numbers.conf — managed via ~/bin/paired-trusted add | remove | list. UK number normalization: +44, 0044, 44, and 07 formats all match the same entry. An empty trusted-numbers file blocks all outgoing SMS and calls except for explicit --confirm invocations. This is the safe default — fill the file in deliberately.
SMS fallback for /phone <num> <msg>: TTS during calls is blocked on some phone firmware (notably Samsung — see "Known phone-side limits" below). When TTS-during-call fails, the wrapper can also send an SMS with the same body so the recipient still gets the message. This is opt-in per invocation — pass --with-sms-fallback to enable it. Without that flag, a TTS failure returns an error and the wrapper does not send any SMS. The Telegram reply notes the chosen behaviour explicitly: "📞 TTS only" or "📞 TTS + 📨 SMS fallback (best-effort)".
This skill runs persistent systemd services that can dispatch phone actions automatically:
paired-sms-watch.service — listens for incoming SMS (via Bluetooth MAP-MNS), forwards alerts to Telegram. Read-only with respect to the phone.paired-call-watch.service — listens for incoming calls (via ofono D-Bus), forwards alerts to Telegram. Read-only.paired-sms-command-hook.service — reads command messages from ~/.openclaw/paired/inbox/, dispatches recognised commands. This is the surface that can act. It accepts commands ONLY from a directory the user controls, with a per-message HMAC signature using a secret in ~/.config/paired/inbox.key (mode 0600). Commands from any other source — raw session logs, the agent's chat memory, an SMS body, etc. — are NOT dispatched.Why the inbox model: earlier versions of this skill parsed the agent's session JSONL log directly. That made the session log a control surface — anything that landed in it (including unfiltered text from incoming SMS/calls) was a potential command source. The inbox model isolates the dispatch surface to messages the user (or a trusted bot relay) explicitly drops into the inbox dir, signed with the inbox key.
To stop all persistent services in one go:
bash
systemctl --user stop paired-sms-watch paired-call-watch paired-sms-command-hook
systemctl --user disable paired-sms-watch paired-call-watch paired-sms-command-hook
bash
~/bin/bt-contacts <MAC> --max 10 # list 10 contacts
~/bin/bt-contacts <MAC> --pull # pull entire phonebook to ~/Downloads/bluetooth/<mac>.vcf
~/bin/bt-contacts <MAC> --search "name" # search by name
bash
~/bin/paired-media status --json # current track + status (auto BT/ADB transport)
~/bin/paired-media play | pause | next | prev | stop
~/bin/paired-media volume 50 # set BT volume 0-100
~/bin/paired-media current # what's playing right now
Auto-detects connected phone, picks BT/AVRCP first then falls back to ADB media controller.
bash
~/bin/bt-send <FILE> <MAC> # push file to phone
~/bin/bt-receive # listen for incoming pushes (saves to ~/Downloads/bluetooth/)
~/bin/bt-browse <MAC> # OBEX-FTP browse (vendor-dependent)
bash
~/bin/bt-pan up <MAC> # connect as NAP client (phone-side BT-tethering must be ON)
~/bin/bt-pan down # disconnect
~/bin/bt-pan status # show bnep0 state
bash
~/bin/bt-gatt-tree <MAC> # enumerate services + characteristics
~/bin/bt-gatt-read <MAC> <UUID> # read a characteristic
~/bin/bt-gatt-write <MAC> <UUID> <HEX> # write a characteristic
bash
~/bin/bt-audio <MAC> --info # available profiles
~/bin/bt-volume <MAC> # current volume
~/bin/bt-play <FILE> <MAC> # play file through BT speaker
When an SMS arrives whose body starts with the phrase set in paired.conf[llm_trigger] (default: "Hi Agent,") and the sender is on the paired.conf[llm_trigger_whitelist], paired-respond will:
/sms commandThe owner decides whether to send the draft by tapping the /sms line. Draft-only is the default — no SMS is sent to the contact automatically. To opt into automatic sending, set llm_auto_reply=true in paired.conf; a whitelisted "Hi paired," message is then answered and the reply texted back automatically (still gated by the whitelist + per-sender cooldown). An empty whitelist disables the feature entirely. Logs at ~/.paired/sms-respond.log.
~/bin/bt-test~/bin/bt-list --paired~/bin/bt-list --connected | grep -i <phone-label>~/bin/bt-pair X --connect~/bin/bt-modems --full~/bin/paired-sms-watch --last 5~/bin/paired-sms-watch --status/sms NUMBER text → ~/bin/paired-sms-send NUMBER "text" --jsonpaired-sms-send LAST_SENDER "X" --json. Get LAST_SENDER from the most recent ~/.paired/sms-events.jsonl entry.~/bin/paired-call dial NUMBER --json~/bin/paired-call hangup --json~/bin/paired-media pause/play/next~/bin/paired-media currentThese are phone-firmware constraints, not skill bugs. The tools return clean errors and the docs explain workarounds.
MAP UpdateInbox and ofono SMS-send returns access-denied. Workaround: use paired-sms-send (ADB-over-USB autosend) — fully working.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE | AUDIOFOCUS_FLAG_LOCK for the entire ring+call lifecycle. No third-party app (Tasker included) can inject audio into the call audio path. The paired-call-and-speak tool runs but the recipient hears silence — SMS fail-soft compensates (the message body is also sent as SMS, recipient guaranteed to receive). On non-Samsung devices (Pixel/AOSP, LineageOS, rooted) this is expected to work normally.bt-send to push files instead.paired-sco-agent is shipped as experimental — see docs/ARCHITECTURE.md.paired.conf and bound to a whitelist. Default config has the whitelist empty, which keeps the feature off until the user explicitly trusts a number.paired.conf.example.txt for the warning.docs/ARCHITECTURE.md.bt-agent.service runs as a system service to handle pairing PIN/passkey requests.paired-* wrappers are the agent-facing interface; the underlying bt-* tools are CLI primitives that wrap BlueZ D-Bus and ofono D-Bus directly. Wrappers add JSON output, trust gating, fail-soft behaviour, and Telegram integration.See docs/HARDWARE-COMPATIBILITY.md for the full matrix. Tested combinations:
PhoneAndroidWhat worksWhat's blockedSamsung Note 910 / OneUI 12Pairing, contacts, SMS receive, outgoing calls, media, file push, PAN, ADB SMS sendIn-call TTS, two-way SCO, MAP send, A2DP sourceAdapterTypeStatusBCM43142A0Internal BT 4.0All features tested workingRTL8761BUSB BT 5.1All features tested working
Pair your phone: bash
~/bin/bt-list --scan 10 # find your phone in the scan output
~/bin/bt-pair <MAC> --connect # pair, trust, connect
Write your config: bash
cp config-templates/paired.conf.example.txt ~/.config/paired/paired.conf # drop the trailing .txt on copy — it's a ClawHub packaging suffix
$EDITOR ~/.config/paired/paired.conf # set phone_bt_mac, adapter, etc.
Set up the trusted-numbers list (optional, recommended): bash
cp config-templates/trusted-numbers.conf.example.txt ~/.config/paired/trusted-numbers.conf
~/bin/paired-trusted add 07911123456 "main mobile"
~/bin/paired-trusted list
Enable the systemd user services you want: bash
systemctl --user enable --now paired-sms-watch.service # real-time SMS push
systemctl --user enable --now paired-call-watch.service # incoming call alerts
systemctl --user enable --now paired-sms-command-hook.service # /sms /phone Telegram commands
Verify: bash
~/bin/bt-test # 10-check stack health
If everything's green, the agent is ready to use the skill.