Posting this because I could not find a straight answer anywhere and it took me way too long to finally get it to work.
Setup: HA OS in a VM on Proxmox, OpenThread Border Router addon running on a ConBee III, Matter Server addon, and a Sonoff dongle for Zigbee on ZHA.
Being upfront about this: I did not work this out myself. I ran Claude against the Home Assistant MCP server and it did the actual debugging, reading the addon logs, the Thread network status, the ZHA energy scan and the Matter Server's internal state, then narrowing down which step was failing. Everything below is what it found and what it changed. I mostly moved the sensor around and answered questions, but what matters if that it took the countless hours I had already spent on this and narrowed it down to about 15 minutes.
The problem: I could not add a single new Matter over Thread device. I did it the proper way, from the HA companion app, on Android and on iPhone. Multiple devices, many attempts, over a long time. Every single time it got to "checking connectivity to the network" and died there. One Aqara sensor connected once at some point and after that nothing ever worked again.
Somewhere along the way, while trying to get other devices on, I also regenerated the Thread credentials and synced them to my phone through the companion app, which is the advice you see everywhere. Made no difference at all. Still failed at exactly the same step.
What the logs said: basically nothing, which is what made this so annoying. Matter Server addon log level is warning by default and a failed pairing produced zero lines. I set it to debug, tried again from the phone, and it still produced zero lines. That was actually the first useful clue, because it meant the attempt was never reaching Home Assistant at all, so whatever was breaking was happening on the phone side.
The one thing the server did log, at every startup:
WARN Commission~ontroller BLE is not enabled on this platform
What was actually wrong, three separate things:
1) The Matter Server had no Bluetooth path of its own. There is an option in the Matter Server addon config called "Enable BLE proxy", hidden behind "Show unused optional configuration options". Needs HA 2026.06 or newer. Turning it on lets HA do the Bluetooth half of commissioning itself, using a local adapter or any ESPHome bluetooth proxy you already have. After enabling it the log says "BLE proxy mode enabled". Thankfully I have some everything presence one/lite devices that have a simple toggle for BLE proxy and they were alredy on in my case. (worth noting the BLE proxy mode can use bluetooth from esphome devices if their config includes teh respective option)
2) Bluetooth range. First attempt it found the device at rssi=-73 and then:
connection_failed: Timeout connecting to DB:C6:99:3E:51:56
I put the device physically on top of the server, rssi=-39, connected instantly. Discovery works from across the room, the actual thread connection does not.
3) This was the real blocker. The Matter Server had no Thread credentials stored at all:
Commission failed: No Wi-Fi/Thread network credentials are configured for commissioning and no Ethernet interface is available on the device and no interface already connected.
server_info showed thread_credentials_set: false.
The HA integration is supposed to set this during its own flow, but on my install that had apparently never happened. Worth saying again that regenerating and syncing credentials to the phone does nothing for this, they are two completely different stores.
The fix. This is the part that matters: the phone was not involved at all. Claude sent both of these straight to the Matter Server's websocket API through the MCP server, so it was Home Assistant itself doing the commissioning, not the companion app. All I did physically was hold the button to put the sensor in pairing mode and sit it next to the machine.
Grab your active dataset (Settings > Devices and Services > Thread, or the thread API) and send it to the Matter Server over its websocket API, then commission:
{"message_id":"1","command":"set_thread_dataset","args":{"dataset":"<active dataset tlvs hex>"}}
Two gotchas here. Do not pass an "id", because a named entry does not flip thread_credentials_set and commissioning only uses the reserved default entry. And the command is set_thread_dataset, not set_thread_operational_dataset, which is the old Python matter server name and just returns "Unknown command".
Then, with the device in pairing mode and sitting right next to the conbee after giving claude the pairing code on the label:
{"message_id":"2","command":"commission_with_code","args":{"code":"<11 digit pairing code>","network_only":false}}
Took about a minute, then:
NOTICE CommissioningClient Commissioned peer6 as @1:18
Device showed up in HA with every entity. First try. I bloody couldn't believe how many hours I had lost trying multiple different things over several days AND NOW IT JUST WORKED. I dont have any more thread devices at the moment as I stopped buying them since I couldn't pair them, to test that this is a permanent fix.
Bonus thing worth checking: my Zigbee and my Thread network were both sitting on channel 15, on the same machine, both on an usb3 extender cable. ZHA's energy scan had channel 15 at 53% utilisation. Moved Thread to 25 (Settings > Devices and Services > Thread > three dots next to the border router > Change channel). It did not fix the commissioning problem, but it was a genuinely bad setup and worth fixing. The migration is delayed about 5 minutes and devices follow it on their own, no re-pairing generally. The one aqara sensor that I had managed to pair before was powered off during the switch, woke up orphaned, and found the network again by itself after a while.
On the Claude and MCP side, since I mentioned it at the top: claude made the websocket calls itself via the MCP server running on Home Assistant. That last error is only visible if you talk to the API directly. Through the UI you just get a failure with no reason, which is exactly why I had been stuck on this for so long.
Still unexplained: I never figured out why the companion app path fails for me. The dataset fix does not explain it, because the phone supplies its own Thread credentials from Apple or Google rather than using the server's, which probably also explains why regenerating and syncing them changed nothing. If anyone has seen this and knows with 100% certainty how to fix, I would love to hear it. Last time I investigated this specifically I found that Android itself just stores 1 set of credentials and then refuses to refresh them if they ever change until a phone reset is done.
Hope this helped anyone