zeroclaw-labs/zeroclaw · error

Serial path not allowed: {}. Allowed: {}

Error message

Serial path not allowed: {}. Allowed: {}

What it means

SerialPeripheral::connect enforces a hardcoded device-path allowlist: /dev/ttyACM*, /dev/ttyUSB*, /dev/tty.usbmodem*, /dev/cu.usbmodem*, /dev/tty.usbserial*, /dev/cu.usbserial*, COM* (plus /tmp/zc-sim-* when built with the dev-sim feature). Any configured path that does not start with one of these prefixes is rejected before the port is opened; the message lists the accepted prefixes.

Source

Thrown at crates/zeroclaw-hardware/src/peripherals/serial.rs:148

}

impl SerialPeripheral {
    /// Create and connect to a serial peripheral.
    #[allow(clippy::unused_async)]
    pub async fn connect(config: &PeripheralBoardConfig) -> anyhow::Result<Self> {
        let path = config.path.as_deref().ok_or_else(|| {
            ::zeroclaw_log::record!(
                WARN,
                ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Reject)
                    .with_outcome(::zeroclaw_log::EventOutcome::Failure)
                    .with_attrs(::serde_json::json!({"board": config.board})),
                "serial peripheral connect refused: config missing 'path'"
            );
            anyhow::Error::msg("Serial peripheral requires path")
        })?;

        if !is_serial_path_allowed(path) {
            anyhow::bail!(
                "Serial path not allowed: {}. Allowed: {}",
                path,
                serial_path_allowlist_hint()
            );
        }

        let builder = tokio_serial::new(path, serial_open_baud(path, config.baud));
        #[cfg(unix)]
        let builder = if should_open_serial_nonexclusive(path) {
            builder.exclusive(false)
        } else {
            builder
        };
        let port = builder.open_native_async().map_err(|e| {
            ::zeroclaw_log::record!(
                ERROR,
                ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Fail)
                    .with_outcome(::zeroclaw_log::EventOutcome::Failure)

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Point path at the device's real node under an allowed prefix: /dev/ttyACM* or /dev/ttyUSB* on Linux, /dev/cu.usbmodem* on macOS, COM<n> on Windows
  2. Do not use /dev/serial/by-id or /dev/ttyS* paths — the allowlist matches prefixes literally; use the underlying ttyACM/ttyUSB node instead
  3. Fix format details: uppercase COM with a number; the leading /dev/ is required on Unix

Example fix

# before
[[peripherals.boards]]
board = "nucleo-f401re"
transport = "serial"
path = "/dev/serial/by-id/usb-STMicroelectronics-CDC-1234"

# after
[[peripherals.boards]]
board = "nucleo-f401re"
transport = "serial"
path = "/dev/ttyACM0"
Defensive patterns

Strategy: validation

Validate before calling

const ALLOWED: [&str; 7] = [
    "/dev/ttyACM", "/dev/ttyUSB", "/dev/tty.usbmodem", "/dev/cu.usbmodem",
    "/dev/tty.usbserial", "/dev/cu.usbserial", "COM",
];
fn serial_path_allowed(path: &str) -> bool {
    ALLOWED.iter().any(|p| path.starts_with(p))
}

if !serial_path_allowed(&config.path.clone().unwrap_or_default()) {
    anyhow::bail!("serial path rejected by allowlist; use /dev/ttyACM* or COM*");
}
SerialPeripheral::connect(&config).await?;

Type guard

fn is_allowed_serial_path(path: &str) -> bool {
    ["/dev/ttyACM", "/dev/ttyUSB", "/dev/tty.usbmodem", "/dev/cu.usbmodem",
     "/dev/tty.usbserial", "/dev/cu.usbserial", "COM"]
        .iter().any(|p| path.starts_with(p))
}

Prevention

When it happens

Trigger: Configuring a [[peripherals.boards]] entry with path = "/dev/ttyS0" (legacy UART), a stable symlink like /dev/serial/by-id/usb-... (fails: not an allowed prefix), a relative path like ttyACM0, or a typo such as /dev/ttyACN0.

Common situations: Boards exposing legacy ttyS* nodes; users preferring /dev/serial/by-id paths for stability; lowercase com3 on Windows; configs ported from tools that accept any device path.

Related errors


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/350f766669b52258. Report an issue: GitHub.