reachy-mini-cli

reachy-mini-cli is the agent and command-line tool for operating a Reachy Mini — an expressive desk robot with a movable head, two antennas, a rotating body, a mic array that hears direction, a camera, and a speaker. Every capability the robot has becomes a noun you run from a shell or drop into an agent loop: hold the hardware, feel alive, orient to sound, turn toward motion, speak, think out loud, feel a head pat, fall asleep, and survive a reboot.

If the agents family has a method, a brain, and a mind — devague is the method that works an idea backwards into a plan, lobes is the split brain that serves cognition across machines, and colleague is the swappable mind you point at a repo — then this is the body. reachy-mini-cli is the embodiment leg: the place where all of that stops being text on a screen and starts leaning its antennas toward the person who just spoke.

Everything on this page is real. The version is 0.31.0, the same one on PyPI. The captures were taken today against a live robot on the rig we call spark — its daemon answering on localhost, an arducam for eyes — and where a service was down, you will see the robot's honest fallback rather than a staged success.

  • installuv tool install 'reachy-mini-cli[daemon]'
  • repoagentculture/reachy-mini-cli
  • packagereachy-mini-cli 0.31.0 · PyPI
  • licenseMIT
  • python>=3.12
  • commandsreachy-mini-cli · reachy
  • transportssdk (in-process) · http (daemon REST)
  • hardwarehead + antennas + body · mic array · camera · speaker
  • agents familythe body — embodiment leg
the body

Everything the robot can do is a verb you can type

Start with what it is: a small desk robot that can look at you, listen for you, and move like it means it. reachy-mini-cli does not hide any of that behind an app — it hands you each capability as a plain command, so a human at a terminal and an agent in a loop reach the robot exactly the same way.

Here is the whole body, one line at a time. None of these is a promise for later; each is a noun that ships in 0.31.0.

  • hold the hardware

    daemon owns the robot process and wakes it; device and move run low-level state and one-shot goto / wake / sleep animations against it

  • feel alive

    demo-mode is the always-on idle loop — gentle breathing, glances, antenna sway — so the robot looks present even when nothing is happening

  • orient to sound

    listen leans its antennas toward a voice, then turns its head and body to face it — reading real direction-of-arrival off the mic array

  • turn toward what it sees

    vision turns toward motion or light in the camera using pure pixel math — frame differences and brightness, no machine-learning model in the loop

  • speak

    say is a dumb pipe — text in, voice out to the speaker — in either a text-to-speech voice or an offline harmonic one, with no LLM and no senses attached

  • think out loud

    think is the cognition loop: it reads the senses, asks an LLM for a first-person thought, speaks it sentence by sentence, and moves its body in step with the words

  • feel a head pat

    pat notices a hand on its head from how the pose deviates from what it commanded — no touch sensor — and leans into the touch: lean, nuzzle, settle

  • fall asleep

    sleep decays from alert to drowsy to asleep when left alone, and wakes on speech, a snap, its wake word, or a pat

  • survive a reboot

    service enrolls exactly one presence mode — demo or live — as a boot-enabled systemd unit, one presence per reboot; hardening that survival on real hardware is open work the tracker owns

fresh runwhoami — the CLI's own identityspark · reachy-mini-cli · identity probereachy-mini-cli 0.31.0 · captured 2026-07-17fresh run · spark, 2026-07-17
$ reachy-mini-cli whoami
nick: reachy-mini-cli
version: 0.31.0
backend: unknown
model: unknown
fresh rundoctor — the health reportspark · reachy-mini-cli · installed-wheel self-checkreachy-mini-cli 0.31.0 · captured 2026-07-17fresh run · spark, 2026-07-17
$ reachy-mini-cli doctor
reachy-mini-cli doctor: healthy

[ok] source_checkout: no culture.yaml found alongside the package; identity checks skipped
fresh runquickstart — the real-mode pathspark · reachy-mini-cli · onboarding, no daemon requiredreachy-mini-cli 0.31.0 · captured 2026-07-17fresh run · spark, 2026-07-17
$ reachy-mini-cli quickstart
reachy-mini-cli quickstart — install and start real mode

Real mode — local robot (recommended)
-------------------------------------
1. Install once (CLI + daemon binary + SDK):
     uv tool install 'reachy-mini-cli[daemon]'
     # or: pip install 'reachy-mini-cli[daemon]'
     # the installed command is 'reachy-mini-cli' (short alias: 'reachy')

2. Start the daemon (wakes the robot on start):
     reachy-mini-cli daemon start

3. Verify it answers:
     reachy-mini-cli device status

4. Make it do something:
     reachy-mini-cli listen run                  # orient to sound (Ctrl-C to stop)
     reachy-mini-cli demo-mode start             # feel-alive idle loop (background)
     reachy-mini-cli move goto --z 10 --pitch -5 # one motion command

5. Put it back down when you are done:
     reachy-mini-cli daemon stop

… (the remote/HTTP-only path and the always-available no-daemon commands elided)
Exit codes: 0 success, 1 user error, 2 environment/setup error.
the noun map

One robot, fourteen nouns — and every one of them speaks JSON

This is the entire surface, nothing held back. Fourteen entries: thirteen robot nouns and one row of agent-first introspection that needs no robot at all. Every noun supports --json, and reachy-mini-cli explain <noun> prints its full flag reference.

Read the transport column closely — it is the whole operating model in one word per row. The sense nouns default to the in-process sdk transport; device, app, and move default to http, the daemon's REST API. That split is not an accident; it is the next section.

nounwhat it doestransport
daemonstart / stop / status the local reachy-mini-daemon processnone — manages the process
devicedaemon and live robot state (status, state)http (default)
applist / start / stop daemon appshttp
moveone-shot goto / wake / sleep animationshttp (default)
demo-modealways-on "feel alive" idle loop (breathe, glances, sway)sdk / http
behavior50 Hz engine that composes named behaviors per channelsdk / http
listentwo-tier sound orienting (antenna lean → head/body turn); --live folds every sense into one loopsdk default
visionturn toward motion or light (pure pixel math, no ML)sdk default
saydumb pipe: text → voice (TTS or offline harmonic) → speakersdk default
thinkLLM cognition loop: speaks and expresses; --export JSONL feedsdk default
patfeel a head pat and lean into it (no touch sensor)sdk only
sleepdecay to sleep when idle; wake on sound / wake-word / patsdk default
serviceboot-persist exactly one presence mode (demo or live) via systemd --usernone — manages systemd
whoami · quickstart · learn · explain · overview · doctor · cliagent-first introspection — no robot needed
fresh rundevice status — the daemon's view of the robotspark · reachy-mini-cli · live daemon, hardware_id 37a38ce3a26e0727reachy-mini-cli 0.31.0 · captured 2026-07-17fresh run · spark, 2026-07-17
$ reachy-mini-cli device status
type: daemon_status
robot_name: reachy_mini
state: running
wireless_version: False
desktop_app_daemon: False
simulation_enabled: False
mockup_sim_enabled: False
no_media: False
media_released: False
camera_specs_name: arducam
backend_status:
  ready: False
  motor_control_mode: enabled
  last_alive: None
  control_loop_stats:
    mean_control_loop_frequency: 49.66561043533144
    max_control_loop_interval: 0.02042984962463379
    nb_error: 0
    motor_controller: ControlLoopStats(period=~20.00ms, read_dt=~1.92 ms, write_dt=~0.19 ms)
  error: None
error: None
wlan_ip: None
version: 1.7.3
hardware_id: 37a38ce3a26e0727
fresh rundevice state — the live head posespark · reachy-mini-cli · live joint/pose telemetryreachy-mini-cli 0.31.0 · captured 2026-07-17fresh run · spark, 2026-07-17
$ reachy-mini-cli device state
control_mode: enabled
head_pose:
  x: 0.0007060431511311815
  y: 0.0005256924657005011
  z: -0.0011491551363928543
  roll: -0.005241699183951121
  pitch: -0.004407163898566546
  yaw: -0.07900591329492851
head_joints: None
body_yaw: 0.0015339807878858025
antennas_position:
  - 0.01687378866674205
  - -0.024543692606170175
timestamp: 2026-07-17T00:25:15.449314Z
passive_joints: None
doa: None
the one model that matters

One robot serves one mind at a time — learn that or fight it

If you take one thing from this page, take this. The CLI will happily let you launch any two nouns at once, but the hardware underneath has two single resources, each with room for exactly one owner. This trips up humans and agents alike, over and over.

The first single resource is the SDK client and its media session: on the sdk transport, listen, think, and sleep each open a single-consumer mic session, and vision and pat read camera frames and head pose through the same one in-process client. So only one sdk-sense noun can own the robot at a time. The second is the head itself — every move, from an idle sway to a sound-orienting turn to an expression, flows through one serial motion queue, one move at a time, so motion is always smooth and never fights itself. Two motion drivers still fight over the same head.

Because both resources are single-owner, you cannot run two sdk-sense nouns as separate processes against one robot. The second one is starved — a stray pat process next to listen gets throttled to about one hertz, far too slow to feel a hand. There are exactly two ways to compose behaviors anyway, and the diagram beside this traces both.

USB mic arrayone audio devicehead · antennas · bodyone motor setsdk transportone live owner at a time — these five are mutually exclusiveone SDK clientin-process ReachyMinimedia_sessionsingle consumerserial MotionQueueone move at a timedriveslistenmic DoA + RMSthinkmic DoA + RMSsleepmic DoA + RMSvisioncamera framespathead-pose read-backopens media_session — listen · think · sleeprides the same SDK client, no media_session — vision · pat
Two single resources, one owner each: the in-process SDK client (fed by the mic array) and the serial motion queue (driving the head). The five sdk-sense nouns fan out of one for sense and converge into the other for motion — which is why only one can own the robot at a time, and why the two composition escapes exist.
  • two single resources

    one in-process SDK client with a single-consumer media session, and one serial motion queue for the head — each serves exactly one owner

  • five nouns, one seat

    listen, think, sleep, vision, and pat are mutually exclusive on the sdk transport — run one sdk-sense owner per robot, never two

  • escape one — fold into one loop

    listen run --live folds think, vision, and sleep into listen's single loop alongside the head-pat hook: every live sense on one media session, in one process

  • escape two — put the second noun on http

    an http-transport noun polls the daemon's DoA route and opens no SDK client at all, so it never competes — the way to layer a second behavior onto the one local owner

  • the head is arbitrated by priority

    when senses are folded together, think, pat, and sleep drop flag files the always-alive idle layer reads, yielding the motion channel by rank: sleep over pat over think

fresh runmove wake — one queued motionspark · reachy-mini-cli · motion queue, uuid-trackedreachy-mini-cli 0.31.0 · captured 2026-07-17fresh run · spark, 2026-07-17
$ reachy-mini-cli move wake
uuid: f020512e-0567-4a76-a894-8cc5e3d521e6
fresh runlisten run — the two-tier reactionspark · reachy-mini-cli · sdk transport, orient-to-sound loopreachy-mini-cli 0.31.0 · captured 2026-07-17fresh run · spark, 2026-07-17
$ reachy-mini-cli listen run --max-ticks 120
… (SDK/daemon version-mismatch RuntimeWarning elided — kept verbatim in the say · harmonic pane above)
[listen] orienting to sound via sdk: dwell=1.5s hold=3s speed=18deg/s; Ctrl-C to stop
[listen] antenna lean +22 (0.3s)
[listen] look +17 (1.5s)
[listen] idle +17 (2.2s)
[listen] idle +7 (2.2s)
[listen] escalate body -45 head -8 (3.8s)
[listen] idle -8 (2.2s)
[listen] idle -8 (2.2s)
[listen] escalate body +44 head +0 (4.0s)
[listen] idle +0 (2.2s)
[listen] idle +0 (2.2s)
[listen] escalate body -35 head +0 (4.0s)
[listen] idle +0 (2.2s)
[listen] idle +0 (2.2s)
[listen] antenna lean +15 (0.3s)
[listen] antenna lean +10 (0.3s)
[listen] escalate body -45 head -9 (1.6s)
[listen] idle -9 (2.2s)
[listen] idle -9 (2.2s)
[listen] antenna lean +16 (0.3s)
[listen] antenna lean -21 (0.3s)
[listen] stopped after 79 tick(s)
the senses, live

One process, every sense — this is the presence the robot boots into

Because only one sdk media owner can run at a time, the supported way to run all the senses at once is a single command — listen run --live — that folds think, vision, and sleep into listen's loop beside the head-pat hook. Every live sense rides the one media session and the one motion queue, in one process, arbitrated by those priority flags. This is exactly the loop the live boot presence runs.

Below is each behavior the folded loop carries. Where a sense is honest about its limits, the card says so — because the robot does too.

  • listen — two tiers of reaction

    tier one leans the antennas toward every sound; tier two turns the head and then the body on speech or a snap — reading real direction-of-arrival, not a guess

  • vision — sight without a model

    turns toward motion by frame difference and toward light by brightness centroid — pure pixel math, no ML and no GPU; on http it can report its specs but not move

  • pat — touch without a sensor

    there is no touch sensor; pat infers a hand from how the actual head pose deviates from the commanded one, then leans, nuzzles, and settles into it

  • sleep — a real drowsiness curve

    decays alert to drowsy to asleep when nothing is happening, breathing slower as it fades, and wakes on speech, a snap, its wake word, or a pat

  • demo-mode — the feel-alive idle

    the motion-only presence for a robot that just needs to look present: continuous gentle breathing, glances, and antenna sway, with no senses attached

  • service — one presence per reboot

    boots exactly one presence — the idle demo loop or the folded live loop — through systemd; enabling one disables the sibling, so two presences never fight for the robot

fresh runservice status — the rig, restoredspark · reachy-mini-cli · presence unit, post-batchreachy-mini-cli 0.31.0 · captured 2026-07-17fresh run · spark, 2026-07-17
$ reachy-mini-cli service status
mode: live
presence_unit: reachy-live.service
daemon_healthy: True
units:
  reachy-daemon.service:
    enabled: enabled
    active: active
  reachy-demo-mode.service:
    enabled: disabled
    active: inactive
  reachy-live.service:
    enabled: enabled
    active: active
thinking out loud

It reasons, speaks a sentence at a time, and moves as it thinks

think is the noun where the body and the mind meet. Each turn snapshots what the robot just perceived — direction-of-arrival, mic loudness, speech — and asks an LLM for one or two short first-person sentences. The reply is sentence-streamed: the first sentence is synthesized and played while the later ones are still being generated, so thinking and speaking overlap the way they do in a person. Emoji markers in that stream drive calm body poses that arrive timed to the words, and each move goes onto the one serial motion queue. The captured demo runs three gestures against three spoken phrases so you can watch the timing without an LLM in the loop.

It has a second voice for when the first one is unreachable. The default is text-to-speech over an HTTP service, but that service can be down — and when it is, live cognition would otherwise go silent. So the robot also has a harmonic voice: fully offline, deterministic, in-process. It does not read the words; it renders their meaning as a short note-melody in the robot's own identity signature. Today's captures show both halves, separately and honestly: the default TTS run failed clean — an error line, a hint, no crash — and a second run that asked for the harmonic engine up front played its melody with no TTS service anywhere. The engine is chosen per run, with --voice-engine or REACHY_VOICE_ENGINE; when the speech service is down, the harmonic voice is the one you reach for.

And you can watch it think from outside. think run --export - streams a newline-delimited JSON feed of what the robot is thinking, saying, and feeling — one object per line, tagged thinking, message, or emotion, each with a timestamp. The renderer lives out of this repo by design: the CLI emits a documented contract, and a separate consumer renders it. Be precise about what ships, though — in 0.31.0 only the stdout sink, spelled '-', is wired. Point --export at a file or an HTTP URL and the CLI says so itself: only '-' (stdout) is supported in this version; HTTP and file sinks are future work. A clean exit-one error, not a broken promise.

One last honesty: the loop does nothing gracefully. When the sense-event buffer is empty, the turn is a no-op — no LLM call, no audio. A bounded think run against a quiet room can finish with zero spoken turns and that is the design, not a failure. It is the same discipline everywhere in this tool: the robot would rather do nothing legibly than fake being busy.

fresh runthink demo — the scripted 3-gesture scriptspark · reachy-mini-cli · built-in demo, sdk transportreachy-mini-cli 0.31.0 · captured 2026-07-17fresh run · spark, 2026-07-17
$ reachy-mini-cli think demo --voice-engine harmonic
/home/spark/.local/share/uv/tools/reachy-mini-cli/lib/python3.12/site-packages/reachy_mini/reachy_mini.py:164: RuntimeWarning: Reachy Mini SDK and daemon versions do not match: SDK=1.9.0, daemon=1.7.3. Running different versions can create issues. Install matching reachy_mini versions for the SDK and daemon.
  self.media_manager = self._configure_mediamanager(media_backend, log_level)
[think demo] done — expressed 3 gesture(s), spoke 3 phrase(s)
fresh runthink expressions — the gesture vocabularyspark · reachy-mini-cli · expression-marker → joint-offset tablereachy-mini-cli 0.31.0 · captured 2026-07-17fresh run · spark, 2026-07-17
$ reachy-mini-cli think expressions
🤔  head_z+2, head_roll+8, head_pitch+6, head_yaw-5, antenna_right-10, antenna_left-10
😮  head_x-3, head_z+5, head_pitch-8, antenna_right+30, antenna_left+30
🙂  head_pitch+5, antenna_right+12, antenna_left+12
👂  head_y+2, head_roll-5, head_pitch+3, head_yaw+15, antenna_right+20, antenna_left-5, body_yaw+8
😐  head_z+3, head_pitch-3, antenna_right+5, antenna_left+5
🎉  head_z+6, head_pitch-10, antenna_right+40, antenna_left-40, body_yaw+10
😔  head_x+2, head_z-3, head_pitch+12, antenna_right-25, antenna_left-25
fresh runthink run — the bounded, zero-turn resultspark · reachy-mini-cli · live cognition loop, --max-turns 1reachy-mini-cli 0.31.0 · captured 2026-07-17fresh run · spark, 2026-07-17
$ reachy-mini-cli think run --max-turns 1 --max-ticks 40 --voice-engine harmonic
/home/spark/.local/share/uv/tools/reachy-mini-cli/lib/python3.12/site-packages/reachy_mini/reachy_mini.py:164: RuntimeWarning: Reachy Mini SDK and daemon versions do not match: SDK=1.9.0, daemon=1.7.3. Running different versions can create issues. Install matching reachy_mini versions for the SDK and daemon.
  self.media_manager = self._configure_mediamanager(media_backend, log_level)
[think] thinking out loud via sdk; voice engine: harmonic; turn-interval=1s mute-after-speak=2.5s; Ctrl-C to stop
[think] stopped after 0 spoken turn(s)
portedthink demo — the real cognition, portedreachy-mini-cli repo · think-body-expression verification docreachy-mini-cli v0.14.0 (as documented) · captured 2026-06-10ported · docs/verification/think-body-expression.md (documents 2026-06-10, v0.14.0 — the artifact's date, not the port date)
## Gate 2 — Demo run (scripted verification)
Run `reachy-mini-cli think demo` and observe:
### c1 / h1 — Movement is timed to thoughts, not random
- [ ] The **first gesture** (`🤔`) fires **before or with** the first spoken
phrase ("I wonder what that sound was."), not after it is finished.
- [ ] The **second gesture** (`👂`) fires **before or with** the second phrase
("There it is again, to my left."), not before the first phrase starts.
- [ ] The **third gesture** (`🙂`) fires **before or with** the third phrase
("Ah — it's just the fan.").
- [ ] No gesture fires **during silence** between phrases.
… (Gate 2's remaining c5/h5 calm-body and c7/h7 distinct-expression checklist items elided — same three gestures, judged for stillness and visual distinctness)
## Notes
… (the coalescing-behaviour and rubric-path notes elided)
- **JSON result:** `reachy-mini-cli think demo --json` should output a JSON
object with `"status": "ok"`, `"expressed"` (list of 3 emojis), and
`"spoken"` (list of 3 phrase strings).
fresh runsay run — the TTS refusalspark · reachy-mini-cli · error:/hint: contract, no TTS serverreachy-mini-cli 0.31.0 · captured 2026-07-17fresh run · spark, 2026-07-17
$ reachy-mini-cli say run "Hello I am the page material now."
[say] synthesizing 35 char(s) …
error: TTS endpoint unreachable: [Errno 111] Connection refused
hint: start the TTS server or point REACHY_TTS_URL at a reachable host (tried http://localhost:9000/v1/audio/synthesize)
fresh runsay run — the harmonic voice, livespark · reachy-mini-cli · sdk transport, audio playedreachy-mini-cli 0.31.0 · captured 2026-07-17fresh run · spark, 2026-07-17
$ reachy-mini-cli say run "Hello from the capture batch" --voice-engine harmonic
[say] synthesizing 28 char(s) …
[say] playing 44640 PCM bytes …
/home/spark/.local/share/uv/tools/reachy-mini-cli/lib/python3.12/site-packages/reachy_mini/reachy_mini.py:164: RuntimeWarning: Reachy Mini SDK and daemon versions do not match: SDK=1.9.0, daemon=1.7.3. Running different versions can create issues. Install matching reachy_mini versions for the SDK and daemon.
  self.media_manager = self._configure_mediamanager(media_backend, log_level)
agent-first, all the way down

Built for an agent to drive — every answer is structured, every failure is a contract

This is not a robot toy with a scripting hook bolted on. It is an agent-first CLI, cited from teken's agent-first foundation, and the same design that makes it pleasant for a person makes it drivable by a model. The introspection verbs — whoami, quickstart, learn, explain, overview, doctor — all work with no robot attached, so an agent can learn the whole surface before it touches hardware.

The contracts underneath are the point. Every command takes --json. Results go to stdout and diagnostics to stderr, never mixed, so a program can read the answer without parsing around chatter. Exit codes mean one thing each: zero for success, one for a user-input error, two for an environment error, three and up reserved. And a failure is never a Python traceback — it is a structured error: line with a hint: line telling you what to do next.

You can see the contract hold on a bare install. Run an sdk-transport noun without the SDK extra and you do not get an ImportError — you get exit two and a hint to install [sdk] or use --transport http. In today's captures the same shape appears for real: the TTS service was down, and the robot returned its clean error-and-hint pair rather than crashing; a second run asked for the offline harmonic voice explicitly and played through the same speaker. The failures on this page are the CLI keeping its promises.

  • introspection with no robot

    whoami, quickstart, learn, explain, overview, and doctor run on any install — an agent reads its own identity, the bring-up sequence, and per-noun docs before a robot is even attached

  • --json on every verb

    structured output on every command, results on stdout and diagnostics on stderr and never mixed, so a program reads the answer cleanly and a human reads the chatter separately

  • exit codes that mean one thing

    zero success, one user-input error, two environment error, three and up reserved — the same policy across every noun, so a caller branches on the number without scraping text

  • error: and hint:, never a traceback

    every failure is a structured pair — the TTS-down capture shows it live: a clean error line and a hint naming the fix, the exit code intact, and not a stack trace in sight

  • a bare install fails cleanly

    the SDK is an optional extra, lazily imported; run an sdk noun without it and you get exit two with a remediation to install [sdk] or use --transport http — never an ImportError from deep in a library

what's next

The tracker is public too — this is direction, not a promise

The body is still growing, and its direction is checkable the same way its facts are: on the open tracker. Four currents stand out — and note that everything in this section is open work, not something 0.31.0 ships.

reachy-mini-cli is open source, MIT, and running for real — the captures on this page are a live robot's own sessions, taken today on the rig we call spark. Every fact above traces to the public repo, the packaging metadata, the export schema, or the CLI's own output, and where a service was down you saw the honest fallback, not a staged success.

That is the whole thesis of the body: the same agent-first discipline the rest of the family runs on — structured output, honest failures, one clear owner of a shared resource — made physical, in a robot that leans toward you when you speak and answers in its own voice even when the network does not. Install it, bring the daemon up, and say something near it.

Read the code →uv tool install 'reachy-mini-cli[daemon]'