All notable changes to the Paired skill are documented in this file. The format follows Keep a Changelog and this project adheres to Semantic Versioning.
- Display name on the ClawHub registry was auto-derived from the publish staging directory basename during the v1.0.8 publish (became
"Paired Publish V108 225554313"). The clawhub CLI uses the directory name as the default display name when--nameis omitted, even on republish. Re-published v1.0.9 with--nameexplicitly set toPaired — Bluetooth Phone Bridgeto restore the canonical name.
No code changes from v1.0.8. Identical 57-file artifact. Tag added so the GitHub release history matches the ClawHub version timeline.
wrappers/paired-sms-send.pywait_for_compose()now detects the compose UI by package focus + uiautomator resource-ids instead of the obsoleteConversationComposeractivity name. Samsung Messages 11.5.x renamed the focused activity fromConversationComposertoWithActivity, which made the legacy substring check returnFalseon every poll. Result: every/sms NUMBER bodyinvocation aborted witherror=compose_did_not_appearafter the unlock and intent stages had already succeeded — to the user it looked like nothing happened. The new detection checks thatmCurrentFocusis owned bycom.samsung.android.messagingAND a uiautomator dump contains bothcomposer_root_viewandmessage_edit_textresource-ids. Both markers are stable across the 11.5 activity rename.- The check writes a temp dump to
/sdcard/_paired_compose_check.xml(distinct from/sdcard/_paired_ui.xmlused by the send-button locator) so the two phases don't overwrite each other's state.
- Verified end-to-end on a locked Note 9 / OneUI 12 / Messages 11.5.50.71:
wake → keyguard locked → auto_unlock OK → intent → compose_visible: true → send_button found → tapped → verified in Sent → screen off → relocked: true. - No other code changes. Wrappers, systemd units, and config templates are byte-identical to v1.0.7.
- New top-of-README section explains why ClawScan rates the skill
Reviewand VirusTotal Code Insight (PaLM) flags itsuspicious, and what's been done to make it safe to install regardless. Modelled on the equivalent section in the openclaw-magento-skill README. The intent is to meet users at the point where they see the warning and explain the reasoning before they have to ask. - README badge added:
ClawScan: Review (by design).
No code changes from v1.0.6. Identical 57-file artifact.
- Display name on the ClawHub registry was stored as the literal 6-character escape sequence
\u2014(backslash, u, 2, 0, 1, 4) instead of the actual em-dash character (—, U+2014, 3 bytes in UTF-8). Caused by shell-quoting layers during the v1.0.4/v1.0.5 publish from Windows → docker → SSH → kali → openclaw host. Re-published v1.0.6 with the name constructed viaprintf '\xe2\x80\x94'so the byte sequence is correct.
No code changes from v1.0.5. Identical 57-file artifact.
- Removed /proc env scraping in
wrappers/paired-respond.py. Earlier versions read the OpenClaw process's environment block via/proc/<pid>/environto harvest theGEMINI_API_KEYSvalue at runtime. VirusTotal Code Insight (PaLM) correctly flagged this as the same credential-harvesting technique used by malware, even though the intent was just to reuse the host's already-configured key. The mechanism was wrong shape regardless of intent. - New contract: Gemini keys come from
~/.config/paired/gemini-keys.conf(mode 0600, refuses to read if more permissive). One key per line or comma-separated. ThePAIRED_GEMINI_KEYSenv var is accepted as an explicit override. - Removed
SYSTEMD_UNITconstant and the/etc/systemd/system/openclaw.serviceread path. The skill no longer reads other services' unit files.
If you used the SMS auto-responder feature (Hi Agent, ... SMS → Gemini-drafted reply):
mkdir -p ~/.config/paired
echo 'YOUR_GEMINI_API_KEY' > ~/.config/paired/gemini-keys.conf
chmod 600 ~/.config/paired/gemini-keys.confThe skill won't read the key from any other location. If the file is missing or world-readable, paired-respond will log an error and skip the LLM call (it will still post a Telegram alert with the question, just without an auto-drafted reply).
v1.0.0 through v1.0.3 inadvertently published only SKILL.md and bin/ to ClawHub. The wrappers/ and systemd/ directories were silently dropped at publish time because ClawHub's listTextFiles() only includes files with extensions in its text-file allowlist (md, py, sh, js, etc), and the wrapper scripts were named paired-call, paired-inbox-hook, etc. with no extension. Likewise the .service units were not in the allowlist.
The OpenClaw safety scanner correctly flagged this as Supply Chain (High/Concern) for v1.0.3 — four of its ten findings (#2, #3, #6, #10) all traced to "the safety wrappers and systemd units described in SKILL.md are not in the published artifact."
v1.0.4 fixes the packaging:
- Wrappers renamed:
wrappers/paired-X→wrappers/paired-X.py(14 files) - Systemd units renamed:
systemd/X.service→systemd/X.service.txt(5 files) - SKILL.md adds a new "Installation" section showing how to symlink the
.pyfiles into~/bin/(dropping the suffix) and how to copy the.service.txtfiles into~/.config/systemd/user/as.service(also dropping the suffix) - Display name updated to "Paired — Bluetooth Phone Bridge" (was "Paired — Phone-as-Hardware for OpenClaw")
bin/bt-call.pynow enforces the trusted-numbers allowlist (OpenClaw scanner finding #2). The low-level dial primitive previously accepted arbitrary numbers; v1.0.4 refuses to dial unlisted numbers unless--confirmis passed. This brings the low-level primitive in line with the wrapper-enforced gating.
clawhub update paired # pull v1.0.4Follow the new "Installation" section in SKILL.md to wire the wrappers into ~/bin/ and the systemd units into ~/.config/systemd/user/. Earlier versions did not actually install the wrappers — they were referenced in SKILL.md but missing from the package. v1.0.4 is the first version where clawhub install paired actually delivers the wrapper layer.
- Removed hardcoded ADB device serial in
skill/wrappers/paired-call-and-speak. The earlierADB_DEVICE = "<serial>"constant was the skill author's own phone hardware serial — a piece of personally-identifying hardware data that shouldn't have shipped publicly. Replaced with a config-driven loader that readsadb_devicefrom~/.config/paired/paired.conf, falls back to thePAIRED_ADB_DEVICEenvironment variable, and finally defaults to lettingadbpick the only attached device. - Updated
paired.conf.exampleto document the newadb_devicekey.
If you used paired-call-and-speak with the previous version, the hardcoded
serial would have been ignored on your system unless you happen to have a
device with the matching serial. The fix is purely about removing identifying
data from the published package. To configure your own device:
echo 'adb_device = <YOUR_SERIAL>' >> ~/.config/paired/paired.conf
# or:
export PAIRED_ADB_DEVICE=<YOUR_SERIAL>
You can find your serial with adb devices.
Addresses 10 findings from the OpenClaw safety scanner. None of these were active vulnerabilities; they were architectural risks and missing safeguards appropriate for the high-impact capabilities this skill exposes (sending SMS, placing calls, controlling a paired phone via ADB).
- Memory poisoning fix (finding #9): new
paired-inbox-hookreplaces the legacypaired-sms-command-hook. The new hook reads commands from a dedicated, append-only inbox at~/.openclaw/paired/inbox/, with HMAC-SHA256 signature verification using a key at~/.config/paired/inbox.key(mode 0600). Commands include a timestamp (5-minute clock-skew window) and a unique nonce (replay protection). Agent session JSONL logs are no longer parsed as a command source.- One-time setup:
paired-inbox-hook --keygen - Run:
systemctl --user enable --now paired-inbox-hook.service - Test:
paired-inbox-hook --put '{"command":"phone_verb","verb":"status"}'
- One-time setup:
- Legacy hook deprecated:
paired-sms-command-hooknow refuses to run unless invoked with both--legacy-jsonl-sourceand--i-understand-the-risks. The systemd unit is no longer auto-enabled. - Pairing-agent default change (finding #7):
bt-agent.pydefault mode is nowpin(interactive). Wide-open--mode autowithout a--device-filternow requires--i-mean-itto opt in to that risk. The previous default (broad auto-confirm) was a footgun. - SMS fallback opt-in (finding #5):
paired-call-and-speakno longer automatically sends an SMS when TTS-during-call fails. The fallback is now opt-in per invocation via--with-sms-fallback. The new inbox-hook exposes the same flag aswith_sms_fallback: false(default) in the command JSON.
- Capability declaration (findings #4, #8, #10): SKILL.md frontmatter now
includes
capabilities,requires, andsafetyblocks declaring what the skill can do, what it needs, and what scope it operates in. This makes the high-impact actions explicit before installation. - Telegram dependency declared (finding #10): Telegram bot integration is
now listed in
requires.external_servicesand the security model is documented under a new## Security modelsection in SKILL.md. - Trust gating documented (finding #2): Outgoing SMS and outbound calls
via the inbox-hook now require either an entry in
~/.config/paired/trusted-numbers.confOR an explicit"confirmed": truefield in the command JSON. An empty trusted list is the safe default. - Reworded "guarantee" language (finding #6): SMS fallback is now described as "best-effort" rather than "guaranteed".
- Reworded execution-context steering (finding #1): The earlier "do not answer from documentation — execute the tools" line was rewritten to be advisory rather than directive. Status queries still favour execution; high-impact actions now explicitly call for user confirmation.
- Finding #3 (supply chain): the scanner reported that
paired-*wrappers were not in the artifact. They have always been atskill/wrappers/. v1.0.2 also addsskill/wrappers/paired-inbox-hookandskill/systemd/paired-inbox-hook.serviceto the bundle. - Finding #4 (ADB code execution): the scanner correctly classifies this
as
Noterather thanConcern— ADB control is the documented purpose of this skill. Capability tags now declare it explicitly.
clawhub update pairedto pull v1.0.2paired-inbox-hook --keygento generate the HMAC keysystemctl --user enable --now paired-inbox-hook.servicesystemctl --user disable --now paired-sms-command-hook.service(or stop it manually if already running)- Update any external Telegram relay you wrote to drop signed JSON files
into
~/.openclaw/paired/inbox/rather than relying on session-log tail. - If you used
paired-call-and-speakand want the previous SMS-fallback behaviour, add--with-sms-fallbackto your invocations.
- Removed hardcoded sudo password fallback in
skill/bin/bt-recover.pyandskill/bin/bt-pan.py. Earlier versions reados.environ.get("SUDO_PASS", "<literal>")with a non-empty default — the literal was a developer credential that should never have shipped. v1.0.1 removes the default entirely; the functions now usesudo -n(non-interactive, fails fast if no rule exists) whenSUDO_PASSis unset, orsudo -Sonly when the user explicitly provides one via env. - Updated documentation in both files to recommend passwordless sudo rules via
/etc/sudoers.d/for the small set of commands these tools invoke (rfkill,systemctl,hciconfig,ip,dhclient). - No code path requiring the leaked default existed —
SUDO_PASSwas already documented as the configured way to provide credentials. The default was a leftover from local development.
If you installed paired@1.0.0 and ran bt-recover or bt-pan without setting SUDO_PASS, your scripts attempted to authenticate sudo with the literal default. Update to v1.0.1 (clawhub update paired) and set up either a passwordless-sudo rule (recommended) or SUDO_PASS in your environment.
Initial public release.
- Bluetooth phone bridge for OpenClaw agents. Use the user's own paired phone — over Bluetooth or ADB — instead of renting a number from Twilio, Telnyx, Vapi, or Deepgram. Zero recurring cost, zero third-party dependencies.
- 38 generic Bluetooth tools covering BlueZ + ofono + ADB, all driving from a configurable phone MAC:
bt-list,bt-info,bt-pair,bt-trust,bt-untrust,bt-connect,bt-disconnect,bt-forget,bt-recover— pairing and connection lifecyclebt-adapters,bt-test— adapter management and 10-check stack healthbt-modems,bt-call— ofono modem state and HFP outgoing callsbt-contacts— PBAP phonebook pull (live phone contacts via Bluetooth)bt-sms-list— MAP read of SMS history (read-only, RFC-spec)bt-media,bt-volume,bt-play— AVRCP media control and audio routingbt-send,bt-receive,bt-browse— OBEX file transfer (push, pull, FTP)bt-pan— Bluetooth PAN tethering (use phone as network gateway)bt-gatt-tree,bt-gatt-read,bt-gatt-write— BLE service/characteristic IObt-audio,bt-battery— profile inspection and battery pollingbt-adb-*— companion ADB-over-USB tools for the few features Bluetooth blocks (SMS send on Samsung, screenshot capture, push/pull)bt-agent— passkey/PIN handling daemon
- 13 high-level integration wrappers for OpenClaw agent use:
paired-call— JSON-clean dial/answer/hangup wrapper aroundbt-callpaired-call-and-speak— dial then speak via Tasker TTS intent (with documented Samsung audio-focus block + SMS fail-soft)paired-call-handler— the per-call decision engine (trust check + action policy)paired-call-watch+paired-call-watch-tg-hook— real-time incoming-call daemon, sends Telegram alertspaired-sms-watch+paired-sms-watch-tg-hook— MAP-MNS push notification daemon, real-time SMS forwardingpaired-sms-send— autosend SMS via ADB UI automation (Samsung-firmware-aware, optional auto-unlock)paired-sms-command-hook— deterministic Telegram/sms NUMBER textand/phone NUMBERcommand parser, bypasses the LLMpaired-respond— optional LLM-drafted SMS reply trigger ("Hi Agent," prefix from trusted senders → Gemini-drafted reply staged to Telegram)paired-trusted— manage the trusted-numbers whitelistpaired-media— auto-fallback BT/AVRCP → ADB media controllerpaired-sco-agent— experimental HFP audio-fd handling for two-way SCO
- 4 systemd user units:
bt-agent,paired-call-watch,paired-sms-watch,paired-sms-command-hook— all template-friendly viaUser=%i - Configuration templates:
paired.conf.exampleandtrusted-numbers.conf.example, with sensible defaults - Telegram command vocabulary (all opt-in via
paired.conf[sms_command_hook]):/sms NUMBER text— send SMS via ADB autosend/phone NUMBER— dial outbound/phone NUMBER text— dial + Tasker TTS speak (with SMS fail-soft for blocked devices)/phone NUMBER attach <path>— dial + speak file content/phone hangup//phone end— end all active calls/phone status— active call state
- LLM-drafted SMS reply (showcase feature, configurable trigger phrase, default
"Hi Agent,") - Trust-gated incoming actions — non-trusted callers can be silently logged, alerted via Telegram, or auto-rejected per
paired.conf[incoming_*_action]
The release ships verbose architectural docs covering hard-won lessons:
- Samsung Telecom audio-focus block —
AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE | AUDIOFOCUS_FLAG_LOCKheld for the entire call lifecycle, blocking any third-party app from injecting audio (Tasker TTS included). SMS fail-soft compensates. - ofono + PipeWire SCO incompatibility — both Samsung BCM43142 BT 4.0 and RTL8761B BT 5.1 adapters tested; HFP audio routing remains an architectural limit on Debian 13 / PipeWire 1.4.x.
paired-sco-agentdocumented as experimental. - Samsung MAP send block — Bluetooth SMS-send via MAP UpdateInbox not implemented in Samsung firmware; ADB-over-USB autosend is the realistic path. Documented.
- OBEX-FTP browse — not advertised by some vendors; tools fall back to OBEX OPP push.
- PBAP, AVRCP, OBEX OPP, NAP — all confirmed working, drove design of the wrapper layer.
| Phone | Android | Carrier | Working | Blocked |
|---|---|---|---|---|
| Samsung Note 9 | Android 10 / OneUI 12 | UK MNO | Pairing, contacts (PBAP), SMS receive (MAP+MNS), outgoing calls (HFP), media (AVRCP), file transfer (OBEX OPP), PAN tethering, ADB SMS send | Two-way SCO audio, A2DP source, MAP send, in-call TTS |
| Adapter | Type | Working |
|---|---|---|
| BCM43142A0 | Internal BT 4.0 | Pairing, HFP, OBEX, AVRCP, MAP/MNS, PBAP |
| RTL8761B | USB BT 5.1 | Same as above (drop-in via --adapter hci1) |
These are documented exhaustively in docs/ARCHITECTURE.md and docs/HARDWARE-COMPATIBILITY.md:
- Samsung Note 9 + OneUI 12: no in-call TTS, no MAP send, no two-way SCO. SMS fail-soft compensates for the call-and-speak feature.
- Pixel + AOSP, LineageOS, rooted devices: in-call TTS likely works (untested).
- A2DP source profile: blocked by ofono+PipeWire conflict on Debian 13.
- Auto-unlock is opt-in only. PIN stored at
~/.config/paired/pinmode 0600. Documented security implications inpaired.conf.example. - Trusted numbers gate the LLM responder and the call-and-speak command. Empty whitelist = features off.
- No phone number, MAC, or chat ID is hardcoded. All identity comes from
paired.confandopenclaw.json.
Requires:
- Linux with BlueZ ≥ 5.55 and ofono ≥ 1.34 (Debian 11+, Ubuntu 22.04+, Fedora 36+)
- Python ≥ 3.9
- Optional but recommended: ADB tools (for
bt-adb-*family and SMS autosend) - OpenClaw ≥ 2026.4.x with the
telegramchannel plugin enabled