Safety

September 18, 2026 · View on GitHub

A biped falls in 0.3 s; an LLM answers in 3 s. Everything here follows from that.

Layers

LayerOwnerWhat it guarantees
Bodythe robot's own controllerWhatever that particular body actually offers, which is not the same everywhere. The Microduck's robotd gives joint and thermal clamps, fall detection and a deadman: velocity goes to zero when robot.move notifications stop. An Open Duck Mini v2 gives none of those: its deadman is quackd's own daemon on the Pi and the human watching is its only fall detector (details under "On hardware"). The body is still the sole safety authority: clients send intents, never motor writes. What each body offers is declared in its manifest's safety_authority, and quackd doctor prints what the robot itself reported (see "On other bodies").
Judgementthe pilot, held to it by quackd ExecutorNothing that moves the body runs until the model has said, against the robot's datasheet, whether the task fits the body at all. It is the model's own judgement; what the executor guarantees is that it was made, recorded, and made before the first leg moved. A coordinator flock's member is a state machine with no pilot to ask, so it is never asked: a role's needs against a datasheet is what that kind of flock has instead. A pilot flock's member is a whole pilot and is asked exactly as a solo run is, about its own part of the task rather than all of it (flock.md).
Conversationquackd ExecutorThe LLM and MCP clients can only do what the .duck allows, as often as the budget allows, with a human in the loop where the contract says so.
Sessionquackd Heartbeat + KillSwitchA dead transport or a worried human ends in a stop intent. In a flock one kill switch reaches every member's executor, so Ctrl-C stops every body rather than the one in front.

Note

The optional discrete stepper changes none of this. With --jev on (jev.md) some turns are answered by a classifier instead of the model, and every one of those calls goes through the same Executor.run_verb as every other, so the allowlist, the budgets, the confirm gates, the preconditions and the body's own safety authority bind it exactly as they bind the model. Two things about it are structural rather than enforced, which is stronger: it can never author a number, because a verb with a free number in its schema is never offered to it at all, and it can never author a sentence, so it cannot record a feasibility verdict, cannot declare an outcome, and cannot end a run. The judgement layer above is untouched: nothing that moves the body runs until the model has said whether the task fits it.

The executor (mirrors upstream's own rules)

Every verb call — from the agent loop, an MCP session or the discrete stepper — passes Executor.run_verb, which applies these in order and stops at the first one that refuses. The gate column is the word the trace and the transcript print, so a refusal tells you which row you are on.

GateWhat it checksWhat you change
abortthe run has been aborted. stop is exempt, so the brake still worksnothing: the run is over
allowlistthe verb is in verbs.allow. stop is always allowedthe .duck's verbs.allow
unknownquackd has never heard of this verbthe spelling, or the robot (quackd list-verbs)
verdictthe pilot has judged the task feasible against the datasheet. stop, observe, report_state, say, quack, express, gaze, look and introspect run before it, and so does any verb its adapter declared read_only, because a pilot has to look at a thing before judging whether it can lift itnothing: the model calls assess_task
paramsthe arguments fit the verb's schemathe call. This is feedback to the model, not a crash
confirmverbs.confirm, or a safety_class of confirm or dangerous. y/N in the terminal; over MCP it refuses unless --yesanswer y, or pass --yes
budgetmax_steps and max_minutes. These cap an MCP session too, which has no loop of its own; max_llm_calls is the loop'sthe .duck's budgets, or --max-steps
abort_whenthe battery threshold, and consecutive failures once the result is inthe .duck's abort_when, or the robot
preconditionwhat the manifest says this verb needs: not fallen, not sitting, torque onthe robot's state
dry_run--dry-run is on, so nothing is sentdrop --dry-run
cancelledthe caller went away mid-verb: an MCP client dropped the request, or a second Ctrl-Cnothing: a stop went out

Execution races a timeout against the abort, so a kill switch cancels the verb that is running. A verb that times out or raises stops the robot and comes back as a failed result rather than an abort.

The cancelled row is the one worth knowing about: that path used to return at once and leave the legs moving with nothing to halt them, which is the failure this page exists to rule out.

Heartbeat

A task pings transport.heartbeat() every 500 ms (robot.health on a Microduck, each backend's own health call elsewhere, a liveness check in sim). One failure → stop intent → abort flag → the loop ends with outcome: aborted. Upstream's own rationale: "LLMs stall mid-inference".

Kill switch

Ctrl-C and q (when stdin is a terminal) set the same abort flag; the loop's finally always sends stop, puts a body that has a recorded rest pose back into it, and closes the transport. Works on Windows (signal handler, not loop.add_signal_handler).

A second Ctrl-C raises straight through that teardown. If it lands during the rest move the close is skipped, so the process exits without disconnecting and a LeRobot arm is left holding rather than sagging. That is the safe direction, and it is written up under "A LeRobot arm" below because it is not a tidy exit.

There is one window where that is not what happens. A --by-hand run ends by asking whether to open the gripper before the arm folds up, and a second press landing on that wait is caught rather than let through, because it means "skip the question" and not "abandon the arm energised with no record written". The gripper is left exactly as the run left it, the arm is still folded to its recorded rest pose, the transport is still closed, and the record still ends with a run_end and a summary. It buys one press and no more: the guard is on that wait alone, so a third press lands somewhere without one and quits at once, on the terms of the paragraph above.

When the pilot is unsure

assess_task has a third answer. uncertain means the verdict itself turns on a figure the pilot cannot judge from where it is: the mass or size of a thing that decides a limit, or a limit the maker never published. Not having found the target is not by itself one of those, because finding the thing is the task. It becomes one when a limit turns on that unseen thing's mass or size, which is why the verbs that look run before the verdict: the pilot can take a frame from where it stands. What it cannot do is go to the thing first. search_scan, go_to and move all wait for the verdict, so when the deciding figure is on something out of frame, uncertain is the honest answer and the person is the way past it. At a terminal that is a y/N question with no as the default. A no ends the run aborted and exits 1, not infeasible and 3, because a person stopping a robot is the kill switch's kind of act rather than a statement about the body.

--yes answers that question with go, the same way it answers a confirm gate, and quackd record always passes it. So a --yes in a script no longer only skips confirmations: it also clears the pilot's own doubt about whether the task suits the body. Over MCP there is no terminal and nothing clears it, so the verdict stays pending and the model is told to ask the person it is chatting with (mcp.md).

When a feasible verdict contradicts itself

A verdict says two things: whether the body can do the task, and, in needs, what the task would require of a body. They can disagree. A pilot on a Microduck answered feasible to a 45 minute patrol and wrote endurance_min: 45 in the same record, on a body whose endurance nobody has ever published, and the duck walked until its step budget ran out.

So a feasible is now held to the body's own datasheet before it is recorded, by the same function that holds another robot's bid at the coordinator. A need the sheet does not meet, or does not publish, is refused and named, with two exceptions that would otherwise refuse an honest answer: a minimum of zero asks for nothing, and an unpublished terrain meets indoor_flat, which is what the prompt tells such a body to assume about itself. and the pilot is told the three ways on: infeasible if that need decides the task, uncertain if a person could know the figure, or a corrected need if it asked for more than the task turns on. An uncertain and an infeasible are left alone, because one asks a person and the other ends the run anyway.

On the one model measured so far it turns a silent feasible into an uncertain, which is then the question above, so under --yes the check costs one LLM call and leaves the contradiction in the transcript rather than stopping the run.

Two things it cannot do. It cannot catch silence, because it reads what the pilot declared: a pilot that never mentions the figure its plan hinges on passes exactly as it did before. And it asks more of a pilot that answers fully, which is the same fact from the other side. A duck asked to nudge a 60 g ball has no published payload to compare against, so a pilot that honestly writes payload_kg: 0.06 is refused where one that writes nothing is not. That is refuse by default doing what ADR-0032 says it should, and the way to answer it once rather than every run is a duck: 2 datasheet: block in the task file: a figure given there replaces the adapter's and is rendered as coming from you, so the pilot is comparing against a number somebody stands behind.

Dry run

--dry-run sends nothing, and the trace names every verb a model would have run, with the parameters it chose:

⚠  gate    dry_run: skipped would run walk_to, sent nothing (target='ball', stop_distance=0.22, timeout_s=20)

A parameter the model left unset shows as null rather than being dropped, because on a dry run the omission is the thing you are checking. The verbs whose adapter declared them read_only still run: observe (alias get_frame), report_state, the rosbridge base's introspect, and whatever a body quackd never shipped flags for itself. Use it the first time you point a new .duck at hardware.

It does not get you past the verdict, because that gate runs before this one. A task the pilot judges infeasible ends with nothing logged rather than with the list of verbs it would have sent, which is the one case where --dry-run tells you less than you asked for.

A dry run does not move the arm into its rest pose either, because it moves nothing. On a LeRobot arm with a pose recorded that is a change of behaviour worth knowing before you press Ctrl-C and walk away: the arm ends the dry run holding itself up, with a line saying so, instead of going limp.

On hardware

One of the seven bodies has run on hardware, once. On 2026-09-15 a LeRobot SO-101 follower arm ran lerobot-lookout and then a series of free-form --goal runs, piloted by OpenAI gpt-6-astra on Windows 11 with lerobot 0.6.1: it waved the wrist about 27 degrees either way, waved again from a raised pose, and opened and closed the gripper. It also fell at the end of every one of those runs, because LeRobot's disconnect() drops torque, which is the whole reason the rest pose below exists. lerobot-first-run.md is the account, including what that day did not measure, and most of what this page would want to know is on that list: whether the holding band is right, what a joint reads after ten minutes of work, and whether a stall is caught on purpose rather than by luck. The other six bodies have not been on hardware at all.

If the body is a Microduck, run the contract in the physics simulator first (--robot microduck:mujoco): it is the only place quackd can show you a body that undershoots, refuses and falls over, and nothing there can be hurt. When you do reach the robot, start with --dry-run every time, then a .duck whose allow list is the smallest thing that could work, then widen it. You are responsible for your robot.

There is now one place where quackd takes torque off a robot, and only a person can ask for it. quackd run --by-hand releases a LeRobot arm at the start of a run so you can lift it, put whatever the task needs into the gripper, and set the pose the run begins from. Every word of that is a limit. Only that arm: the flag is refused on the other six bodies by name. Only at the pose recorded with quackd robot rest-pose, because an arm held up by torque alone falls the instant torque goes, so the arm refuses to be let go of anywhere else and a run on an arm with no pose recorded is refused before it starts. Only when a person typed the flag at a terminal, which is also checked before anything moves. And never by anything else: releasing is not a verb, it is in no allow list, and it is deliberately not on the RobotAdapter protocol, so there is nothing for a model or an MCP client to call. When you press Enter, quackd writes the pose you left the arm in as the goal before it re-enables torque, writes it again, reads it back, and ends the run rather than starting it if a joint moved more than five degrees while your hand was still on it.

If a run ends while the arm is still limp in your hands, quackd picks it up before it folds it. That state takes a Ctrl-C during the wait, a heartbeat that died, or a take_hold the arm refused, and the way out of it is the stop every teardown opens with: on an arm that is in somebody's hand that stop takes hold first, at wherever your hand has it, and the rest move then puts it down from there. Sending a goal to a limp servo would have been a stop that stopped nothing. If even that did not take, the close says so in the one line that is worth reading:

the arm is limp and in your hands (...): put it down before you let go of it, because nothing is holding it up

That line and the "torque was left on" one under "A LeRobot arm" below are opposites on purpose, and quackd has to pick the right one. Whoever reads this one is holding the arm, and being told instead that it is holding itself up is the sentence that gets an arm dropped.

An XLeRobot (a 12 kg dual-arm cart):

  • The watchdog stops the wheels and nothing else. Upstream's 500 ms deadman calls stop_base(), so the fourteen arm and head servos keep holding their last goal under torque. deadman_scope says base_only, and that is the robot's entire safety authority.
  • Nothing reports a battery, so a battery abort can never fire. The power station's switch is the only e-stop and it is not on the network.
  • The host exits by itself after an hour with no supervisor anywhere upstream, so a long session ends as a heartbeat failure rather than an error.
  • Blocks under the wheels until you have checked the turn direction: quackd converts rad/s to the deg/s the wire wants, and a wrong conversion is a 57x error.
  • Work through xlerobot-hardware-checklist.md.

An AlohaMini (two arms on a 600 mm motorised lift):

  • As upstream ships it the arms are limp, so the safest bring-up is on the stock host, where the base and the lift can be exercised with no arm risk. quackd's own host wrapper is what turns torque on, and upstream's disconnect() turns it off again, so a loaded arm falls when that host exits.
  • The watchdog covers the base and the lift, never the arms (base_and_lift_only).
  • home() leaves the lift travelling at full speed, because the write that would zero that register is commented out upstream. quackd sends stop as its first command after connecting for exactly this reason.
  • Clear the lift's whole travel before powering it. How fast it moves in mm/s is not stated anywhere upstream, so quackd's duration estimate for lift is an assumption.
  • Work through alohamini-hardware-checklist.md.

A ToddlerBot (a 56 cm, 3 kg humanoid):

  • It cannot get up. There is no get-up policy for this body at the pinned commit, so a fall ends the run and needs a human. Every moving verb refuses once it is down.
  • Work through toddlerbot-hardware-checklist.md in order. It keeps the feet off the ground until step 13, and steps 11 and 12 (pull the network cable mid-move, then send SIGTERM) are the two that matter most.
  • The deadman is a slew, not a stop. There is no velocity at this hardware boundary: the command is an absolute pose. On silence the daemon quackd ships slews to the safe pose at upstream's own rate, waist first, and holds. It never goes limp, because on this body torque off is a fall.
  • quackd owns the control loop here, which is true of no other body. Upstream's own step() is a no-op, so nothing times out and nothing re-arms without the daemon.
  • A good first contract is the shipped toddlerbot-lookout: it moves no leg, no arm and no waist.

A Microduck (a 25 cm biped):

  • Run on the floor, not a table. A 25 cm biped and a table edge do not mix.
  • Keep pets and kids clear of kick (and grab, and roulade).
  • The gamepad preempts remote control. Upstream arbitrates authority; there is no stop button because releasing the sticks stops the robot via the deadman. quackd does not try to out-rank the pad.
  • quackd never sends robot.relax (torque off — the robot collapses) or robot.init (moves every joint). Use robotctl for those, with the robot on its stand.
  • A good first contract: allow: [quack, gaze, stop], then add walking.

A LeRobot arm (an SO-101 class arm on a desk):

  • An arm sweeps a volume. Clear it before move_joints, and keep hands out of the path. A gripper is a pinch hazard even at the 50 % torque cap LeRobot writes at configure().

  • That cap is on the gripper and on nothing else, so the five body joints have no protection but their own firmware and quackd's heat gate: a joint at or above 60 °C refuses to move.

  • There is no e-stop and quackd cannot give it one. Cutting the servo supply is the only thing that stops this arm in every case, the one where quackd itself has died included.

  • pick hands the whole arm to a learned policy for up to a minute. It is confirm-gated for that reason. Watch it, and keep stop within reach.

  • stop holds position and never releases, and it leaves the gripper's goal alone so a failed verb never drops what is held.

  • The arm falls when a session ends, unless you have recorded a rest pose. LeRobot's own disconnect() disables torque by its default and quackd keeps that default, which is why the arm fell at the end of every run on 2026-09-15. Fold the arm by hand while nothing is connected, record where it sits with quackd robot rest-pose <name>, and quackd drives it back there between the stop and the close, on every exit path there is: success, failure, infeasible, a spent budget, an abort, an error and Ctrl-C. It drives the arm there at the start of a run too, before the pilot gets control, and a run that cannot reach it aborts before the first LLM call. Only the five body joints are ever driven: the gripper is recorded and never commanded, for the same reason stop leaves it alone. With no pose recorded, the old behaviour stands and the arm sags where it stopped, which is also what quackd robot rest-pose <name> --clear returns you to. An MCP session does the same at both ends and refuses to open at all if it cannot get there, because a client is about to drive a body nobody has established the pose of (mcp.md).

  • The rest move is sent unclipped, so it is the one move that ignores the joint limits. A folded arm often sits outside the travel its own calibration recorded (the bench arm folded to shoulder_lift -113.5 degrees against a calibrated ±84.2), and the refusal that keeps move_joints inside those limits would otherwise refuse to put the arm down. Record a pose you are willing to have the arm driven into from wherever a run ends, and watch the first one.

  • An arm that did not reach that pose keeps torque instead of letting go. quackd turns LeRobot's flag off for that one case, leaves the arm holding itself up, and prints one line:

    the arm is not at its rest pose (...), so torque was left on and it will not fall: hold the arm and cut its power, or run again
    

    The brackets name the joint furthest from where it was asked to be, what it reads, and why nothing put it there. Do what the line says. The arm is energised, the run is over, and nothing is going to put it down on its own.

  • A probe and a dry run now leave torque on where they used to drop it. A dry run never moves the arm and quackd robot list --probe never moves it either, so on an arm away from its recorded rest pose both end with torque on: the dry run prints the line above, and the probe says ok, torque left on: not at its rest pose in its reachable column. quackd doctor is the other way round: it drives a probed arm back to the pose and says which it got in a rest pose row, because a doctor probe disconnects like anything else, and that is one of the ways the arm fell.

  • A second Ctrl-C during the end-of-run rest move skips the close entirely. The move is not shielded from a KeyboardInterrupt, so the process exits with disconnect() never called, torque never disabled, and the arm holding wherever the move had got to. That is the safe direction and it is not a clean exit: the port closes with the arm still energised, so hold the arm and cut its power.

  • A good first contract is the shipped lerobot-lookout, which moves no joint. The order to bring one up in, nothing moving until step 10: lerobot-hardware-checklist.md.

A wheeled base over rosbridge:

  • No deadman was verified anywhere in that stack, so if quackd dies mid-verb the base keeps its last Twist until its own driver times out, if it does at all. Test on blocks with the wheels off the ground before testing on the floor.
  • quackd re-sends the Twist at 10 Hz while a verb runs and publishes a zero Twist on stop, on close, and when the heartbeat fails. That is the entire stop authority.
  • The speed limits are quackd's caution (limits.max_vx, max_wz in the manifest), not the base's capability. Lower them before the first real drive.
  • A good first contract: allow: [observe, report_state, introspect, stop]. Nothing there publishes a Twist, so you can read what the bridge says the robot is before you drive it.

An Open Duck Mini v2:

  • If it falls, quackd cannot pick it up, and on hardware it cannot tell that it has. There is no get-up policy, so stand_up does not exist for it, and nothing on the bridge backend detects a fall: no verb refuses because the duck is down, and every observation says fall-blind. You are the fall detector: keep it on a stand until you trust the link.
  • The deadman is quackd's own, and it runs on the robot. quackd's bridge daemon zeroes the velocity after 300 ms of silence, inside the call the control loop makes every tick, so a server thread that is starved, wedged or dead still stops the duck. Test it by pulling your laptop's Wi-Fi mid-walk before you rely on it.
  • Going limp is unreachable rather than forbidden: the only channel from the network to the body is seven floats and a few buttons, so no message reaches a torque register.
  • Head control is off unless you start the daemon with it on, and then it is clamped to 80 percent of the runtime's own range and rate limited. Upstream warns that head control can break the head, and the four head values are offsets added to wherever the walk policy is holding the head, not absolute angles, so that clamp bounds an offset rather than a joint.
  • The Feetech serial bus has exactly one owner. The bridge is the walk loop, so do not run it and upstream's script at the same time.
  • The bridge binds loopback and wants a token, because a port that walks a robot on a shared network is a hazard. Prefer ssh -L 9871:127.0.0.1:9871 your-pi.
  • The camera is a second process (quackd_duck_camd.py) serving one JPEG over HTTP with no authentication at all. It binds loopback and warns if you bind it wider, because it shows whatever the robot can see. Tunnel it rather than exposing it.
  • The only e-stop is the power switch.
  • A good first contract is the shipped open-duck-lookout: it looks and speaks, and moves no leg.
  • The order to bring one up in, feet off the ground until step 10, with an abort condition at every step: open-duck-hardware-checklist.md.

On other bodies

quackd drives more than the duck, and the honest answer to "what stops it when quackd goes quiet" differs per body. Each manifest says so (safety_authority: {native, deadman}), and stop always means stop, never collapse:

BodyNative authorityWhat stop doesNever sent
Microduck (microduck:*)robotd_deadman: velocity zeroes when intents stoprobot.stoprobot.relax, robot.init
LeRobot arm (lerobot:*)torque_limit: the gripper's torque and current caps and nothing on the five body joints (extras.torque_limit_scope is gripper_only), plus a capped step per action that quackd sets; no deadman, a position-controlled arm holds its goalre-sends the present position as the goal (hold) for the five body joints and leaves the gripper's goal alone. The rest move that follows a run's last stop is the only thing that puts the arm downdisable_torque. LeRobot's own disconnect() still does, by its default, at the end of a session, but only once the arm has been driven back to its recorded rest pose: an arm that did not get there has that default turned off and is left holding itself up, with one line saying so. With no rest pose recorded, the session ends the way it always did and the arm sags
rosbridge base (rosbridge:*)none: neither rosbridge nor the driver has a deadman we verifiedpublishes a zero Twist; quackd also re-sends the Twist at 10 Hz while a verb runssilence
Open Duck Mini v2 (open_duck:*)none in the robot, but quackd's own bridge daemon runs on it and zeroes the velocity after 300 ms of silence, inside the 50 Hz loopzero velocity, head held, torque still onanything that reaches torque, the head-control mode button, any direct servo or IMU read
XLeRobot (xlerobot:*)none: the host's own 500 ms watchdog is real but calls stop_base(), which zeroes the three wheels and nothing else, so the 14 arm and head servos keep holding under torque. deadman_scope says base_onlyzeroes the three velocity keys and leaves every arm goal exactly where it was, deliberately not rebuilding a hold from an unstamped reading that may be cycles olddisconnect(), which is upstream's torque-off, and any enable(on=False)
AlohaMini (alohamini:*)none: the host's 1 s watchdog calls stop_motion(), which is the base and the lift and never the arms. deadman_scope says base_and_lift_onlyone payload carrying all three velocity zeros and a lift velocity zero, because omitting either leaves the robot travellinganything that disables arm torque. As shipped the arms are already limp, which is why the arm verbs need quackd's own host wrapper and refuse without it
ToddlerBot (toddlerbot:*)none in the robot, and nothing upstream has a watchdog, timeout or e-stop at all. quackd's own daemon runs on it and after 500 ms of silence slews to the safe pose at upstream's own 0.3 rad/s, waist first, and holdsholds the last verified-good measured pose. There is no velocity at this hardware boundary, so stop cannot mean zero velocitytorque off, ever. Silence on this body means hold forever and torque off means fall, so the deadman is a trajectory rather than a message

The verbs a body lacks are not gated, they do not exist: an arm cannot move, a base cannot say, and validate --robot says so before a run starts.

What each body can carry, reach and survive is its datasheet (manifest-spec.md): every number with how sure quackd is of it and who says so, and a figure the maker never published listed as not published rather than guessed at. The pilot is shown it and told to judge the task against it before anything moves, which is what assess_task is for, and a .duck file can correct it for the build in front of you (ADR-0032).

What quackd does not protect against

A model that is allowed to walk can walk into a wall; the sim has walls, your living room has stairs. The allowlist is your tool: a .duck for a new space should start small.

The feasibility verdict is the model's own guess about the world, weighed against numbers that carry confidence labels of their own, and neither is a measurement. A wrong feasible buys nothing past the gates below it: the allowlist, the budgets, the confirm gates and the body's own safety authority all still apply, which is why the verdict is a layer above them and not a replacement for any. A wrong infeasible costs one run. A robot that reports a URDF that does not match the robot is believed, and that assumption is recorded as one.

There is one more thing to know about. A robot's memory (memory.md) is text a model wrote, kept on disk, and handed to the next model as part of its system prompt. The executor never reads it, so a note cannot widen an allowlist, lift a budget or open a confirm gate: none of the guarantees above depend on it being true. What a note can do is persuade a later run, including a later run of a different task on the same body. A model that concludes something wrong ("the sofa is safe to walk under") will keep telling itself so until somebody deletes the line. That is the whole point of the feature and also its whole risk, which is why the file is plain text you can read, quackd memory show prints exactly what the pilot was told, quackd memory clear forgets it, and --no-memory runs as if it were never there.

There is a second thing that is not about the body. --robot microduck:mujoco downloads an MJCF, 38 meshes and two ONNX policies from GitHub and the Hugging Face Hub the first time it runs, into ~/.quackd/cache, and then runs those policies in quackd's own process. What guards that: the commit and the revision are pinned in adapters/microduck/src/quackd_microduck/sim3d/upstream_api.py, every file is checked against a sha256 recorded when it was read and a mismatch is a refusal, and the tarball is unpacked by name against a fixed list rather than by whatever it contains. What does not: QUACKD_MICRODUCK_ASSETS points quackd at a checkout of your own, and there a file that differs from the pin is a warning and the run continues, which is deliberate, because a newer export is what somebody with a checkout usually wants. The state says which you got (extras.model_pinned), so the transcript records it. Nothing in this path reaches a robot: the physics backend has no address and drives nothing outside the process.

Report anything that lets a model bypass the executor — see SECURITY.md.