Control your Matter smart home from the command line — pure Rust, pure JSON.
mat is a command-line Matter controller built for scripts and AI agents,
not apps. It speaks Matter directly — TLV, CASE, the Interaction Model,
groupcast, mDNS discovery, and commissioning are implemented from scratch in
Rust and run in-process — and prints exactly one JSON object per command on
stdout.
The demo runs against matv, the virtual Matter device that ships in this
workspace — the whole Quickstart works with no hardware at all.
For the design background, the mat / matd split, and what mat does and
does not do, see ARCHITECTURE.md.
Most ways to script Matter go through a hub, a cloud API, or a heavyweight
controller SDK. mat is the other extreme: one small binary that is the
controller.
- Built for scripts and AI agents. stdout carries exactly one JSON object
per command; diagnostics stay on stderr as structured logs. Error
kinds and exit codes are stable, so a program (or an LLM) can decide recovery without parsing prose. - The whole Matter stack, in-process. TLV, CASE, the Interaction Model,
groupcast, mDNS, and commissioning (on-network and BLE+Thread) are a
from-scratch pure-Rust implementation (crate
mat-controller). No chip-tool, no Python matter-server, no vendor SDK, no controller subprocess. (The one exception:mat diag --deepshells out toping6for its IP-liveness probe.) - One-shot by design.
matholds no state except the credential store — run it from cron, a shell pipeline, or an agent loop. When you want warm sessions and live subscriptions, the optionalmatddaemon adds them (mat listen). - Names at the edge, numbers on the wire. An optional local
aliases.tomlmaps node / group / endpoint names to numbers right after arg parsing; the protocol layer stays numeric. - Test without hardware. The workspace ships
matv, a virtual Matter device you can commission and control end-to-end on a laptop.
# install from crates.io -> ~/.cargo/bin/{mat,matd,matv}
# (the CLI package is named `matterctl`; the binary it installs is `mat`)
cargo install matterctl matd matv
# create your first fabric (writes the credential store; no network I/O)
mat fabric init
# commission a device (all values here are dummy)
mat commission --setup-code "MT:Y.K9042C00KA0648G00" --node 5
# on a shared host, keep the setup code out of the process list instead:
# MAT_SETUP_CODE="MT:..." mat commission --node 5
# control and read it
mat on --node 5
mat read --node 5 --cluster onoff --attribute on-off
# when you are done with it: remove it from the fabric and the ledger
mat unpair --node 5Every command prints exactly one JSON object on stdout:
// mat commission
{ "timestamp": "2026-06-06T12:34:56+09:00", "node_id": 5, "status": "success" }
// mat on — control is an invoke of the OnOff cluster's on command
{ "timestamp": "2026-06-06T12:34:57+09:00", "node_id": 5, "endpoint": 1, "cluster": "onoff", "command": "on", "status": "success" }
// mat read — the attribute's TLV value, normalized
{ "timestamp": "2026-06-06T12:34:58+09:00", "node_id": 5, "endpoint": 1, "cluster": "onoff", "attribute": "on-off", "value": true }Errors are structured the same way (stderr, stable kind + exit code — see
Errors and exit codes).
matv hosts a virtual Matter device (a bridge with one on/off light), so the
whole flow above runs end-to-end on a single machine:
cat > matv.toml <<'EOF'
passcode = 20202021
discriminator = 3840
vendor_id = 0xFFF1
product_id = 0x8000
port = 0
store = "./device-store"
iface = "eth0" # an interface with an IPv6 link-local address
[[device]]
id = "demo-light"
kind = "onoff-light"
name = "Demo Light"
EOF
matv --config matv.toml & # prints the setup payload (qr_payload) as JSON
export MAT_STORE=./mat-store
# trust the virtual device's self-generated dev attestation root
export MAT_PAA_TRUST_STORE=./device-store/paa
mat fabric init
mat commission --setup-code "MT:Y.K90AFN00KA0648G00" --node 1
mat on --node 1 --endpoint 2 # endpoint 2 = the bridged light
mat read --node 1 --endpoint 2 --cluster onoff --attribute on-offstore holds the device's fabric keys: matv creates a missing store
directory owner-only (0700; an existing one keeps its mode) and writes its
state files 0600. paa/paa.der is the public dev attestation root, rewritten
on every start for the commissioner to trust.
kind is one of onoff-light, switch (a momentary Generic Switch — short /
long / multi press events) or contact-sensor (Boolean State — StateChange
events). Virtual buttons and sensors are driven from stdin with
matv --config matv.toml --stdin-control, one JSON line per stimulus:
{"device": "btn1", "press": "short"}
{"device": "btn1", "press": "multi", "count": 2}
{"device": "door", "state": true}Each applied stimulus prints {"device":..,"applied":..,"event_numbers":[..]}
on stdout; a bad line prints a {"error":{"kind":..}} line on stderr and the
hook keeps reading. Events reach a subscriber through the subscription's
EventReports — run mat listen --event in another terminal to stream them:
{"timestamp":"2026-09-06T21:00:00+09:00","node_id":1,"endpoint":3,"cluster":"switch","event":"initial-press","event_number":3,"priority":"info","data":{"new-position":1},"priming":false}matv also receives groupcast: it binds a second UDP socket on 5540
(the Matter groupcast multicast destination port; SO_REUSEPORT so several
matv processes can share it) — override with group_port in matv.toml,
or set it to 0 for an ephemeral port in tests; port must differ from
group_port, and matv refuses to start if they are equal. Once a group has a
KeySetWrite / GroupKeyMap / AddGroup / ACL on the device (mat group provision sets all four up), a mat group invoke sent to that group
multicasts to the member endpoints. Keys and membership persist under
<store>/group_keys.json / groups.json; replay protection is the spec's
32-wide (fabric, source) counter window. Privacy-flagged groupcast (the form
the chip SDK, chip-tool and Apple Home send) is decrypted the same way
(derived from the SDK's DeriveGroupPrivacyKey / BuildPrivacyNonce /
AES_CTR_crypt definitions; not yet verified against a real chip-SDK
controller), and KeySetRead / KeySetRemove / KeySetReadAllIndices are
served, so
mat group remove fully tears a group down on matv. Membership rows of a
[[device]] removed from matv.toml are pruned at startup (re-adding the
device restores its endpoint, not its groups). Group-addressed Read/Write
is not implemented.
- Rust (stable) and Task to build. No external Matter controller is needed — the backend is native and pure Rust.
- Matter uses mDNS / IPv6 multicast, so on a real network the host must be able
to send and receive these.
matauto-detects the network interface (override withMAT_IFACE; see Backend). Groupcast also sends directly on a Thread TUN interface when one is available, in addition to the LAN path (override withMAT_THREAD_IFACE; see Groupcast egress). - BLE commissioning (BLE+Thread) is an opt-in
blecargo feature (pulls inlibdbus); the default build and localtask checkdo not need it. Deploy builds enable it — see Backend.
From crates.io — the CLI package is matterctl (the mat name was taken);
the binary it installs is mat:
cargo install matterctl # the mat CLI (binary: mat)
cargo install matd # optional: the resident daemon
cargo install matv # optional: the virtual device hostBLE commissioning is an opt-in cargo feature (pulls in libdbus):
cargo install matterctl --features ble.
From a checkout:
task build # release build -> target/release/{mat,matd,matv}
task install # install all three binaries into ~/.cargo/binThe full reference lives at https://nogu3.github.io/mat/ (also browsable
in docs/):
| Page | Contents |
|---|---|
| Commands | Every command with its JSON output: discover / commissioning, state operations, diagnostics, listen, multi-admin share, groupcast, routing through matd |
| Configuration | The credential store, aliases.toml, subscriptions.toml |
| Errors and exit codes | Error schema, kind table, exit codes, stderr logs |
| Backend | The native backend, interface auto-detection, environment variables |
| Development | Build / test tasks, manual E2E with real devices |
For the design background and roadmap, see ARCHITECTURE.md.
Everything documented is implemented on the native backend and passes the fake-connection / binary integration tests; real-device E2E has confirmed the full op sweep runs natively with no fallback. Group delivery is unacknowledged multicast by design, so per-device actuation cannot be confirmed from the controller side (see Groupcast).
Issues and pull requests are welcome. Before sending a PR, run task check
(format check + clippy with -D warnings + tests); it needs no real devices.
Please keep stdout pure JSON and follow the design rules in
ARCHITECTURE.md.
MIT.
