Warning
This script is LLM slopcoded. But I read the code and tested all behavior paths that I know of. Read the code first and use at your own risk.
Keep selected Bluetooth devices paired and visible in GNOME while preventing them from reconnecting to the computer.
- Appplies
untrustto devices to prevent computer → device reconnection (re-applied on startup and device disconnect) AuthorizeService()denial to prevent device → computer reconnection- Device can still be manually reconnected in GNOME Settings or Quick Settings
This was written for a bluetooth headset that aggressively reconnects to previously paired hosts. The headset stays paired, remains available in GNOME Quick Settings, and can still be connected manually from the normal GNOME Bluetooth UI.
Tested on Fedora Silverblue 43. Required Fedora packages (should be installed by default):
python3
python3-dbus
python3-gobject-base
Edit MANUAL_ONLY in bluetooth-manual-only.py to add MAC addresses of any devices you would like to be controlled by this script.
Install the files:
install -Dm755 bluetooth-manual-only.py \
~/.local/libexec/bluetooth-manual-only.py
install -Dm644 bluetooth-manual-only.service \
~/.config/systemd/user/bluetooth-manual-only.serviceFor the cleanest first start, disconnect the headset if it is currently connected. Then enable the user service:
systemctl --user daemon-reload
systemctl --user enable --now bluetooth-manual-only.serviceCheck its status and logs with:
systemctl --user status bluetooth-manual-only.service
journalctl --user -u bluetooth-manual-only.serviceNo root privileges are required.
To stop using it:
systemctl --user disable --now bluetooth-manual-only.serviceThe device remains paired. If desired, restore the normal GNOME/BlueZ trust state afterward by connecting it and running:
bluetoothctl trust 12:34:56:AB:CD:EFBlueZ distinguishes between the underlying Bluetooth connection and authorization of individual services such as A2DP, HFP, and AVRCP.
For the tested headset, a headset-initiated reconnect behaves like this:
headset initiates Bluetooth connection
|
v
Device1 Connected=True
|
v
BlueZ asks its default Agent1 to AuthorizeService()
|
v
bluetooth-manual-only rejects the service requests
|
v
BlueZ tears the connection back down
A manual connection from GNOME Quick Settings behaves differently. GNOME asks BlueZ to initiate the connection locally, and on the tested headset that path does not invoke AuthorizeService() on the default agent. The connection therefore proceeds normally.
The daemon exports a small org.bluez.Agent1 object and registers it with:
Capability = NoInputNoOutput
It intentionally calls RegisterAgent() but does not call RequestDefaultAgent().
When no other desktop agent is active, BlueZ can use this registered agent as its default. The agent rejects authorization and pairing requests, which closely matches the ordinary behavior when no interactive default agent is available. Trusted devices normally bypass this authorization path, so existing trusted Bluetooth devices continue to work normally.
The full GNOME Settings → Bluetooth panel registers its own BlueZ agent and explicitly requests to become the default agent.
That is intentional and is left untouched.
While the Bluetooth Settings UI is open:
GNOME Settings agent
|
+-- becomes BlueZ's default agent
|
+-- normal GNOME authorization behavior applies
When GNOME Settings closes, its agent disappears and BlueZ can fall back to the already-registered bluetooth-manual-only agent. The daemon does not need to detect GNOME Settings, unregister itself, or proxy requests to GNOME.
GNOME's normal authorization behavior may set a paired device to Trusted=True. A trusted device bypasses AuthorizeService(), which would defeat the manual-only policy after GNOME Settings closes.
To avoid that, the daemon keeps every MAC in MANUAL_ONLY untrusted while it is disconnected.
It deliberately does not clear Trusted=True while the device is connected. This allows a connection accepted by the GNOME Bluetooth Settings UI to behave normally for the lifetime of that connection.
Once the headset disconnects:
Connected=False
|
v
daemon sets Trusted=False
|
v
next headset-initiated reconnect must use AuthorizeService()
|
v
rejected
If the daemon starts while a manual-only device is already connected, it likewise leaves the current trust state alone until that connection ends.
BlueZ's Blocked property prevents the device from connecting at all. That also prevents the normal GNOME Connect button from working, so it does not meet the goal here.
The device remains:
Paired=True
Blocked=False
Trusted=False # while disconnected
Linux's Bluetooth MGMT interface exposes enough information to distinguish locally initiated from remotely initiated connections, so an alternative implementation could watch MGMT connection events and disconnect selected remote-initiated links.
That approach requires lower-level access and additional privilege.
For the tested headset it is unnecessary: incoming HFP/A2DP/AVRCP service requests reliably invoke BlueZ Agent1.AuthorizeService(), while a manual GNOME Quick Settings connection does not. The Agent1 approach therefore provides the desired behavior entirely through the normal BlueZ D-Bus API and can run as an unprivileged systemd --user service.
When the headset attempts an unsolicited reconnect, the base Bluetooth link may briefly appear as connected before BlueZ reaches service authorization.
On the tested headset the observed sequence was approximately:
Connected=True
AuthorizeService(HFP) -> rejected
AuthorizeService(A2DP) -> rejected
AuthorizeService(AVRCP)-> rejected
~3 seconds
Connected=False
A brief connected indication is therefore expected.
The daemon watches the org.bluez D-Bus owner. If bluetoothd restarts, it automatically registers its Agent1 again and re-applies the disconnected-device trust policy. No service restart should be necessary.
Add MAC addresses to the MANUAL_ONLY set in the script:
MANUAL_ONLY = {
"12:34:56:AB:CD:EF",
"AA:BB:CC:DD:EE:FF", # another device
}Then restart the service:
systemctl --user restart bluetooth-manual-only.serviceThe approach depends on the device's remotely initiated profiles going through BlueZ service authorization. That behavior was explicitly tested with one headset; other devices may behave differently.