Metadata-Version: 2.5
Name: harald
Version: 0.2.2
Summary: helps coding agents work out the bluetooth le and classic protocol of gadgets you own
Project-URL: homepage, https://github.com/k0nnect/harald
Project-URL: source, https://github.com/k0nnect/harald
Project-URL: issues, https://github.com/k0nnect/harald/issues
Author: k0nnect
License-Expression: MIT
License-File: LICENSE
Requires-Python: >=3.11
Requires-Dist: bleak>=3.0
Requires-Dist: cryptography>=44
Requires-Dist: dbus-fast>=2; sys_platform == 'linux'
Requires-Dist: mcp>=2.3
Requires-Dist: pyobjc-framework-iobluetooth>=11; sys_platform == 'darwin'
Description-Content-Type: text/markdown

<p align="center">
  <img src="https://raw.githubusercontent.com/k0nnect/harald/main/assets/banner.svg" alt="harald ハーラル ᚼᛅᚱᛅᛚᛏᚱ. teach your agent to speak bluetooth." width="100%">
</p>

<p align="center">
  <a href="https://pypi.org/project/harald/"><img alt="pypi" src="https://img.shields.io/pypi/v/harald?style=flat&label=pypi&labelColor=2b2b2b&color=c8102e"></a>
  <a href="#四-agent-setup"><img alt="works with: claude code · codex · cursor" src="https://img.shields.io/badge/works%20with-claude%20code%20%C2%B7%20codex%20%C2%B7%20cursor-2b4bd8?style=flat&labelColor=2b2b2b"></a>
  <a href="#三-devices"><img alt="devices: 1 decoded" src="https://img.shields.io/badge/devices-1%20decoded-4a4640?style=flat&labelColor=2b2b2b"></a>
  <a href="https://github.com/k0nnect/harald/blob/main/LICENSE"><img alt="license: mit" src="https://img.shields.io/badge/license-mit-4a4640?style=flat&labelColor=2b2b2b"></a>
</p>

<p align="center">
  <a href="#二-install">install</a> &nbsp;/&nbsp;
  <a href="#一-how-it-works">how it works</a> &nbsp;/&nbsp;
  <a href="#三-devices">devices</a> &nbsp;/&nbsp;
  <a href="#五-bluetooth-classic">bluetooth classic</a> &nbsp;/&nbsp;
  <a href="#七-contributing-a-device">add your device</a>
</p>

![a claude code session uses harald to find a sony wh-1000xm5's control channel over bluetooth classic, is refused until a human arms the headphones, then turns noise cancelling on and reads the battery](https://raw.githubusercontent.com/k0nnect/harald/main/assets/demo.gif)

**fig. 1** · claude + harald find a sony wh-1000xm5's control channel over bluetooth classic, wait for you to arm it, then turn noise cancelling on. real hardware, sped up.

## 一 how it works

harald is an mcp server, a cli and a playbook for coding agents. it gives your agent
hands and ears for bluetooth le and classic, so it can work out the protocol of a
gadget you own. then you control that gadget from your own code instead of the
vendor's app.

| *listen* | *decode* | *speak* |
| :--- | :--- | :--- |
| harald reads what the vendor app says to the gadget, from your phone's bluetooth log. | your agent works out the language, keys and checksums included, by diffing payloads, testing keys and searching the vendor's android app. | you get the protocol written up and a standalone driver script with its own small cli. no vendor app. |

## 二 install

you need [uv](https://docs.astral.sh/uv/). then:

```sh
uv tool install harald
```

that puts `harald` on your path for the commands below. if your shell can't find it,
run `uv tool update-shell`. to try harald without installing it, run
`uvx harald --help`: uvx fetches harald and runs it once. tested on mac; linux is
experimental, see below.

the first time harald scans, macos asks whether your terminal app may use bluetooth.
allow it. if you missed the prompt, turn it on in system settings › privacy & security
› bluetooth.

two optional tools, for the steps that need them:

- `brew install jadx` lets harald read a vendor's android app.
- `brew install libimobiledevice` records your iphone's bluetooth log from the terminal.

harald also works without an agent:

```text
harald scan                          list bluetooth le devices nearby
harald connect <address>             list a device's services and characteristics
harald read <address> <char>         read a characteristic once
harald write <address> <char> <hex>  write bytes, once you have armed the device
harald capture open <file>           show a phone capture as a timeline
harald apk <file>                    search a vendor android app for uuids and keys
harald devices                       list the device library
harald drive <slug> <command>        run a library device's driver
```

`harald --help` lists the rest, and [bluetooth classic](#五-bluetooth-classic) has
the commands for classic gadgets.

**linux (experimental)**

harald works on linux too, for bluetooth le and classic, through bluez. you need bluez
5.50 or newer with bluetoothd running, and your user in the `bluetooth` group: log out
and back in after adding it. harald needs no root, and there is no permission prompt
as on macos, but it can't reach bluetooth from inside a sandbox such as snap or
flatpak. the install above works as it is, on uv's own python too. if harald says
bluetooth is off, run `bluetoothctl power on`, with `rfkill unblock bluetooth` first
if it is blocked, and check bluetoothd with `systemctl status bluetooth`.

pair a classic gadget in your own terminal, in this order:

1. `bluetoothctl pairable on`. without it, bluez makes a bond that is gone after the
   next disconnect.
2. `bluetoothctl scan on`, until the gadget shows up.
3. `bluetoothctl pair <address>`.
4. `bluetoothctl trust <address>`.

if harald says the gadget has forgotten the pairing, run
`bluetoothctl remove <address>` and pair it again.

linux is experimental: tested against an emulated gadget, not yet on real hardware.
if you run it on linux, tell us how it went in an issue.

## 三 devices

| device | what harald found | status |
| :--- | :--- | :--- |
| sony wh-1000xm5 headphones | noise cancelling, ambient and battery over bluetooth classic | decoded |
| your device | open a pull request with its `device.toml`, `protocol.md` and `driver.py` | wanted |

each decoded device lives in
[`devices/`](https://github.com/k0nnect/harald/tree/main/devices) with its protocol
written up and a standalone driver. `harald devices` lists them, and
`harald drive <slug> <command>` runs one, for example `harald drive <slug> on`.

## 四 agent setup

add the mcp server to your agent, then install the playbook: the step-by-step method
your agent follows to crack a gadget.

**claude code**, in your project folder:

```sh
claude mcp add harald -- uvx harald mcp
harald playbook install claude-code --dest .claude/skills/harald
```

use `--dest ~/.claude/skills/harald` to have the skill in every project.

**codex**

add this to `~/.codex/config.toml`, or run `codex mcp add harald -- uvx harald mcp`:

```toml
[mcp_servers.harald]
command = "uvx"
args = ["harald", "mcp"]
```

then, in your project folder:

```sh
harald playbook install codex
```

this writes `AGENTS.md`. if your project already has one, harald leaves it alone; use
`--dest ~/.codex` to install it for every project instead.

**cursor**

add this to `.cursor/mcp.json` in your project, or to `~/.cursor/mcp.json` for every
project:

```json
{
  "mcpServers": {
    "harald": {
      "command": "uvx",
      "args": ["harald", "mcp"]
    }
  }
}
```

cursor has no playbook; it uses harald's tools directly.

then ask your agent to work out your gadget with harald. it will ask you for two things
only you can do.

**record your phone talking to the gadget.** a computer cannot overhear phone-to-gadget
traffic without extra hardware, so the log comes from your own iphone. a bluetooth
classic gadget's log comes from an android phone instead; see
[bluetooth classic](#五-bluetooth-classic).

1. on the iphone, install apple's bluetooth logging profile: "bluetooth for ios/ipados"
   on apple's [profiles and logs](https://developer.apple.com/bug-reporting/profiles-and-logs/?name=bluetooth)
   page. it is free.
2. connect the iphone to your mac by cable. open packetlogger, from apple's
   [additional tools for xcode](https://developer.apple.com/download/all/), and choose
   file › new ios trace. or, from the terminal: `idevicebtlogger capture.pklg`, which
   records until you press ctrl-c.
3. in the vendor app, do one thing at a time, a few seconds apart, in the order your
   agent asks for.
4. stop the trace, save the `.pklg` file and give your agent its path. to look at it
   yourself: `harald capture open capture.pklg`.

if packetlogger records nothing, remove the logging profile and install it again.

**get the vendor's android app.** if the commands look encrypted, your agent asks for
the apk of the vendor app you already use. you don't need an android phone, and
harald never downloads apps: download it yourself from
[apkmirror](https://www.apkmirror.com/), which verifies each app's signature, or with
[apkeep](https://github.com/EFForg/apkeep) (`apkeep -a <package name> .`). the package
name is the `id=` part of the app's play store address.

## 五 bluetooth classic

some gadgets, often headphones and speakers, are controlled over bluetooth classic
instead of bluetooth le: the vendor app talks to an rfcomm channel, a plain byte
stream. harald reaches those too, on a mac or linux; bluetooth le works as before.

pair the gadget first: on a mac in system settings > bluetooth, keeping it connected
there; on linux with `bluetoothctl`, as in [install](#二-install). harald doesn't
search for or pair classic gadgets, and `harald scan` only sees bluetooth le. classic
addresses look like `aa:bb:cc:11:22:33`.

```text
harald classic devices                         list the gadgets paired with this computer
harald classic services <address>              list a gadget's rfcomm services
harald classic send <address> <service> <hex>  send messages, once you have armed it
harald classic listen <address> <service>      show what a channel says, sending nothing
harald capture open <file> --channel n         show one rfcomm channel of a capture
```

`<service>` is a service uuid or channel number from `harald classic services`; add
`--refresh` there when the list looks out of date. a mac reads that list from its
own cache; on linux harald asks the gadget itself, so the gadget has to be on and in
range. `harald classic send` takes one or more messages and sends them in order,
listening for replies after each one (`--listen s`, 2 seconds by default). if the
grant runs out or the link fails partway, it prints what it sent and received, says
why on stderr and exits 1. `harald classic listen` sends nothing and listens for
`--seconds s` (5 by default). either listens for at most 30 seconds at a time.

like `harald write`, `harald classic send` needs `harald arm <address>` first, and
every message spends one write from the grant, a query too. harald never opens the
channels of protected profiles such as hands-free or file transfer; see
[safety](#六-safety). your agent gets the same four as mcp tools: `classic_devices`,
`classic_services`, `classic_send` and `classic_listen`. none of them arms.

**classic captures come from android.** an iphone capture of a classic gadget shows
only apple's iap2 wrapper, which harald can't unwrap yet, so record the vendor app
from an android phone's bluetooth log:

1. turn on developer options (tap the build number seven times in the phone's about
   screen), then turn on the bluetooth hci snoop log in them, choosing enabled
   rather than filtered if it asks.
2. with the vendor app closed, turn the phone's bluetooth off and on, so the log is
   running before the app opens its channel.
3. in the vendor app, do one thing at a time, a few seconds apart, as your agent
   asks.
4. with usb debugging on, run `adb bugreport`. its zip holds `btsnoop_hci.log`; give
   your agent that file's path, or look at it yourself with
   `harald capture open btsnoop_hci.log`.

`harald apk` flags the rfcomm sockets and uuids in a vendor's android app as well as
its bluetooth le calls.

## 六 safety

harald is for gadgets you own: interoperability and research. don't use it on anyone
else's devices.

- **writes need you.** harald refuses every write until you run, in your own terminal:

  ```sh
  harald arm <address> --writes n --minutes m
  ```

  that grants a budget of n writes for m minutes (50 and 30 if you leave the flags
  out), and each write spends one. it shows the grant and waits for you to type
  `arm`, and it refuses to run without a terminal. `harald status` shows what is
  armed and `harald disarm <address>` revokes it. there is no mcp tool that arms.
- **the gate has limits.** `harald arm` stops casual or accidental arming by an
  agent, but it is not a sandbox. grants live in a file, `~/.harald/arm.json`, and
  any agent that can run arbitrary commands or write files on your machine could get
  around the gate. what keeps a cooperative agent out is the playbook's rule: it
  never runs `harald arm`, `harald write`, `harald drive`, `harald classic send` or
  a driver from its own shell; you do. it sends classic messages only through its
  `classic_send` tool.
- **firmware updates are blocked.** writes to firmware-update (dfu) services stay
  refused even when a device is armed, so a stray write cannot start a firmware update.
  only `--allow-dfu` on `harald arm` lifts that, and working out a protocol never
  needs it. harald also blocks the classic firmware-update channels it knows, so far
  airoha's tooling channel, the same way unless you arm with `--allow-dfu`. but many
  classic gadgets take firmware over their normal control channel, where harald can't
  tell an update from any other message, so your agent must never send a message
  whose purpose it doesn't know.
- **classic channels are fenced.** harald never opens a channel whose service record
  lists hands-free, headset, dial-up networking, obex object push and file transfer,
  phonebook, messages, sim access or apple iap2. the serial port profile is allowed,
  since many gadgets' control channels use it. harald also refuses channels missing
  from the device's service list. with `harald classic send`, every message spends
  one write, a query too.
- **library drivers need no arming.** `harald drive` runs a `devices/` entry's
  standalone driver, which you start yourself, so it sits outside the gate. read a
  driver before you run it, as you would any script.
- **only app-wide keys are shared.** a `devices/` entry may carry a key only if it is
  the same on every unit and ships inside the vendor app. per-unit keys and pairing
  keys never go in the library; they stay in your own `harald-work/` folder.

## 七 contributing a device

cracked a gadget? your agent's work is in `harald-work/<slug>/`. turn it into
`devices/<slug>/` with a `device.toml`, `protocol.md` and `driver.py`, then open a pull
request. the
[contributing guide](https://github.com/k0nnect/harald/blob/main/CONTRIBUTING.md)
has the details, including the keys rule.

<sub>mit license. harald is named after king harald "bluetooth" of denmark, after whom
bluetooth is named; ᚼᛅᚱᛅᛚᛏᚱ is his name as it is carved on the jelling stone.</sub>

<!-- mcp-name: io.github.k0nnect/harald -->
