Hmbown/CodeWhale · error · anyhow::Error

{kind} must be a single path component

Error message

{kind} must be a single path component

What it means

The second ensure_safe_storage_id check requires the id to be exactly one Normal path component. It rejects ids containing '/', parent segments ('..'), current-directory segments ('.'), absolute prefixes, and any other multi-component or non-normal path shape. This is the path-traversal guard that keeps storage writes inside the automation/task directories.

Source

Thrown at crates/tui/src/automation_manager.rs:1837

    let run: AutomationRunRecord = serde_json::from_str(&raw)
        .with_context(|| format!("Failed to parse {}", path.display()))?;
    if run.schema_version > CURRENT_RUN_SCHEMA_VERSION {
        bail!(
            "Automation run schema v{} is newer than supported v{}",
            run.schema_version,
            CURRENT_RUN_SCHEMA_VERSION
        );
    }
    Ok(run)
}

fn ensure_safe_storage_id(kind: &str, value: &str) -> Result<()> {
    let mut components = Path::new(value).components();
    let Some(component) = components.next() else {
        bail!("{kind} must not be empty");
    };
    if components.next().is_some() || !matches!(component, std::path::Component::Normal(_)) {
        bail!("{kind} must be a single path component");
    }
    Ok(())
}

fn validate_name_and_prompt(name: &str, prompt: &str) -> Result<()> {
    if name.trim().is_empty() {
        bail!("Automation name is required");
    }
    if prompt.trim().is_empty() {
        bail!("Automation prompt is required");
    }
    Ok(())
}

fn normalize_optional_string(value: Option<String>) -> Option<String> {
    value
        .map(|value| value.trim().to_string())
        .filter(|value| !value.is_empty())

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Use a flat, opaque id (uuid or slugified name) instead of a path-like string.
  2. Sanitize before saving: replace '/' and other separators with '-' and strip leading dots.
  3. Enforce an id pattern such as ^[A-Za-z0-9_-]+$ at the input boundary.

Example fix

// before
let automation_id = format!("{}/{}", team, name); // contains '/' -> bails

// after
let automation_id = format!("{}-{}", team, name)
    .chars()
    .map(|c| if c.is_ascii_alphanumeric() || c == '-' || c == '_' { c } else { '-' })
    .collect::<String>();
Defensive patterns

Strategy: validation

Validate before calling

use std::path::{Component, Path};

fn is_safe_storage_id(id: &str) -> bool {
    let mut components = Path::new(id).components();
    matches!(components.next(), Some(Component::Normal(_))) && components.next().is_none()
}

Prevention

When it happens

Trigger: Passing an id like 'a/b', '../secrets', '.', '..', or '/etc/passwd' where an automation id, trigger id, run id, or task id is expected; ids assembled by joining user input with '/'.

Common situations: Reusing a file path or URL slug as the storage id; concatenating team/name into an id with separators; ids built from free-form model or task names that contain slashes.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20). Data as JSON: /api/errors/ca8961ad6ed492b5. Report an issue: GitHub.