Authorization and scope. ble-doctor is for your own devices, CTF targets, and systems you have written permission to test. It is a defensive analysis tool.
probe-writestransmits to the target and is opt-in per characteristic. Do not point it at hardware you do not own or have explicit authorization to assess.
Hand it a BLE capture or a device address. It scores the security posture 0-100 and tells you what is weak: pairing method, unauthenticated writes, DFU exposure, address rotation.
ble-doctor analyze capture.pcap # offline: score a btmon/Ubertooth capture
ble-doctor scan --target AA:BB:CC:DD:EE:FF # live read-only GATT enumeration + score
ble-doctor probe-writes AA:BB:CC:DD:EE:FF --char <uuid> # opt-in: test one characteristic for open writes
Every subcommand takes --json for a stable machine-readable envelope. No telemetry; nothing
leaves your machine.
Get started
1. Install
uvx ble-doctor analyze capture.pcap # Python, via uv; nothing to install
pipx install ble-doctor
npx ble-doctor analyze capture.pcap # Node launcher: runs the same version through uvx or pipx
pip install ble-doctor
From a clone: pip install -e .. Requirements:
| Need | For |
|---|---|
| Python 3.10+ | everything |
tshark (Wireshark) on PATH |
analyze (capture parsing, SMP pairing dissection) |
bleak (installed with the package) and a BLE adapter |
scan, probe-writes |
bluesploit (optional) |
an INFO cross-check inventory in scan; never moves the score |
2. Score a capture
ble-doctor analyze capture.pcap
BLE Doctor — score: 85/100
[FAIL] mac_rotation (medium): ...
[PASS] pairing_method (info): ...
(Illustrative; real detail strings depend on the capture.) If the capture contains no SMP pairing exchange the pairing check is skipped and the reason is printed, not scored.
3. Enumerate a live device
ble-doctor scan --target AA:BB:CC:DD:EE:FF # or set BLE_DOCTOR_TARGET
--target takes an address or advertised name; with none given and exactly one device in range, it
uses that one. The connection is read-only.
4. Probe for unauthenticated writes (opt-in)
ble-doctor probe-writes AA:BB:CC:DD:EE:FF --char 0000ffe1-0000-1000-8000-00805f9b34fb
Connects without bonding and attempts one harmless write per named characteristic, then
classifies what the device answered. Nothing is written unless you name characteristics
(--char, --service) or pass --allow-all. Firmware-update characteristics are refused unless
you also pass --allow-dangerous, which can brick a target.
5. Teach your coding agent
ble-doctor install # Claude Code skill + Cursor rule + AGENTS.md block in this repo
ble-doctor install --print-only # show what would be written
CLI reference
ble-doctor scan [--target ADDR_OR_NAME] [--json]
Live checks: rig state, GATT enumeration, write surface, DFU, bluesploit inventory.
--target defaults to $BLE_DOCTOR_TARGET.
ble-doctor analyze <capture_file> [--json]
Offline checks on a btmon/btsnoop/Ubertooth capture: advertising-address rotation and SMP pairing.
ble-doctor probe-writes <address> [flags]
| Flag | Meaning |
|---|---|
--char UUID |
characteristic to probe; repeatable |
--service UUID |
service whose characteristics may be probed; repeatable |
--allow-all |
probe every writable characteristic (firmware-update ones still refused) |
--allow-dangerous |
also probe firmware-update/erase characteristics. Real side effects. |
--payload HEX |
probe payload in hex (default is a harmless one) |
--json |
machine-readable output |
ble-doctor install [--agent claude|cursor|codex] [--print-only] [--force] [--dir PATH]
Writes .claude/skills/ble-doctor/SKILL.md, .cursor/rules/ble-doctor.mdc and a managed block in
AGENTS.md. --agent is repeatable (default: all). Existing differing skill/rule files are kept
unless --force.
Exit codes: 0 ok, 1 the operation failed (missing tshark/bleak, unreadable capture), 2 bad arguments.
JSON output
analyze --json:
{
"pairing_skipped": "no SMP pairing exchange in capture",
"score": 100,
"findings": [
{"id": "mac_rotation", "severity": "info", "passed": true, "detail": "..."}
]
}
scan --json has the same score and findings, plus rig_state, gatt_checks_skipped
(null or a reason), gatt_enumeration (depth, service_count, writable_char_count),
bluesploit_skipped, bluesploit_recon, and, when enumeration succeeded, the raw gatt_info.
probe-writes --json: {"results": [...], "writable_chars": [...], "findings": {"score", "findings": [...]}}.
A non-null skip field means those checks did not run. It is never a pass.
The score
score = 100 x product(1 - weight/100) over failed checks, rounded half-up. Weights: high 30,
medium 15, low 5, info 0. Monotonic (more or worse findings never raise it), with diminishing
returns, and one HIGH leaves 70 rather than zero.
Check catalog
| ID | Severity | What it measures |
|---|---|---|
pairing_method |
high / medium / low / info | Pairing association model inferred from the SMP exchange (Core Spec Vol 3 Part H Table 2.8). Just Works is weakest; undeterminable or reserved IO capability is medium. |
unauthenticated_writes |
high / info | Characteristics writable without bonding. scan uses a heuristic; probe-writes measures it. |
dfu_entry_point |
high / info | HIGH only when the DFU control point is writable without bonding or encryption. A DFU service that is bond-protected, or merely present, is INFO. |
dfu_signing_enforced |
high / medium / info | Reads a recorded signing probe. With no probe it is INCONCLUSIVE at INFO (0 points): a queued probe, not a pass. |
dfu_surface_absent |
info | States whether "no DFU surface" is backed by a complete enumeration or is unknown. |
mac_rotation |
medium / info | Whether the advertising address rotates, duration-aware; too short a capture says so instead of judging. |
bluesploit_recon |
info | Read-only bluesploit module inventory as a manual-reproduction pointer. Never moves the score. |
Where a row lists several severities, the check picks one from the evidence; see detail.
Known limits
- Validated only against an nRF Connect test double and synthetic fixtures, not a genuine third-party target. Treat results as unproven on real hardware until #40 lands.
requires_bondingfromscanis a heuristic (vendor characteristics assumed bond-protected); onlyprobe-writesmeasures it.- DFU signing is not probed live yet (#41): it stays INCONCLUSIVE.
scancannot observe the pairing method (it is an SMP fact); useanalyzeon a capture.- bleak + BlueZ can wedge discovery with
org.bluez.Error.InProgress; the tool retries and then tells you tosudo systemctl restart bluetooth. - Radio-layer only. Companion-app and cloud-side security are out of scope.
- No SARIF, MCP server, or CI action yet: see the open issues.
Testing
pip install -e '.[dev]' && python -m pytest test/ -q
Capture tests need tshark and skip without it; CI installs it and fails if they skip.
See AGENTS.md for the contributor contract.
License
Metadata
Release files for ble-doctor 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| ble_doctor-0.1.0.tar.gz | 63.5 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ble_doctor-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 132.8 kB
Release files / ble_doctor-0.1.0.tar.gz
| Download URL | ble_doctor-0.1.0.tar.gz |
|---|---|
| Size | 63.5 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ca0f7336f09fbc08568018dac5773c23e2f7c58059e582315c30d5cf5e723128
|
|
BLAKE2b-256 checksum How to use checksums |
9e8e2efd17492712b8be9493ec4f95213865ff86a9e528905442e986ed220d5f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.
Transparency logRelease files / ble_doctor-0.1.0-py3-none-any.whl
| Download URL | ble_doctor-0.1.0-py3-none-any.whl |
|---|---|
| Size | 69.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ff3b7c10157c9e698b520bb3cc4604b7abf6807f9f0688e829a11fafca2bf7a2
|
|
BLAKE2b-256 checksum How to use checksums |
80e052fd64646f34e31c0c1bdce63612802f143ca3dbd7c415277a69a883eca5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 6, 2026.
Transparency log