zeroclaw-labs/zeroclaw · error · anyhow::Error

invalid SOP name '{name}': must be a single path component (

Error message

invalid SOP name '{name}': must be a single path component (no separators, '.', '..', or absolute paths)

What it means

resolve_sop_dir validates that an SOP name is exactly one normal path component: it must not contain '/', '\\', or NUL, and Path::components() must yield a single Normal component. This rejects '.', '..', absolute paths, Windows prefixes, and any nested path before the name is joined onto sops_dir, preventing path traversal outside the SOP store.

Source

Thrown at crates/zeroclaw-runtime/src/sop/mod.rs:233

            let expanded = shellexpand::tilde(dir);
            install_root.join(expanded.as_ref())
        }
        _ => default_sops_dir(install_root),
    }
}

/// Resolve `<sops_dir>/<name>`, accepting only a single normal path
/// component so caller-controlled names cannot escape the SOP root.
fn resolve_sop_dir(sops_dir: &Path, name: &str) -> Result<PathBuf> {
    let mut components = Path::new(name).components();
    let single_normal = matches!(
        (components.next(), components.next()),
        (Some(std::path::Component::Normal(_)), None)
    );
    if single_normal && !name.contains(['/', '\\', '\0']) {
        Ok(sops_dir.join(name))
    } else {
        anyhow::bail!(
            "invalid SOP name '{name}': must be a single path component (no separators, '.', '..', or absolute paths)"
        )
    }
}

// ── SOP loading ─────────────────────────────────────────────────

/// Load all SOPs from the configured directory, resolved against `install_root`.
pub fn load_sops(
    install_root: &Path,
    config_dir: Option<&str>,
    default_execution_mode: SopExecutionMode,
) -> Vec<Sop> {
    let dir = resolve_sops_dir(install_root, config_dir);
    load_sops_from_directory(&dir, default_execution_mode)
}

/// Load a single SOP by directory name from the SOPs root. Errors if the

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Pass a single-component slug: sanitize user input by replacing separators with '-' (or reject it) before calling any SOP API.
  2. Validate early at the trust boundary with your own single-component check so the bad name never reaches the store layer.
  3. Never construct SOP names by joining directory paths; map hierarchical names to flat slugs.

Example fix

// before: name built from a user path segment
let sop_name = format!("{team}/{name}"); // 'platform/backup' -> rejected
create_sop(&sops_dir, &sop)?;

// after: flatten to one component
let sop_name = format!("{}-{}", team, name); // 'platform-backup'
create_sop(&sops_dir, &sop)?;
Defensive patterns

Strategy: validation

Validate before calling

use std::path::Path;

fn is_valid_sop_name(name: &str) -> bool {
    !name.is_empty()
        && name == name.trim()
        && !name.contains('/')
        && !name.contains('\\')
        && !name.contains('\0')
        && name != "."
        && name != ".."
        && Path::new(name).components().count() == 1
}

assert!(is_valid_sop_name(&sop_name), "SOP name must be one path component");

Type guard

fn is_valid_sop_name(name: &str) -> bool {
    !name.is_empty()
        && name == name.trim()
        && !name.contains('/')
        && !name.contains('\\')
        && !name.contains('\0')
        && name != "."
        && name != ".."
        && std::path::Path::new(name).components().count() == 1
}

Try / catch

match save_sop(&sops_dir, &sop) {
    Err(e) if e.to_string().contains("must be a single path component") => {
        // sanitize: replace separators with '-' and retry with the flattened slug
    }
    other => other?,
}

Prevention

When it happens

Trigger: Calling load/save/create/delete SOP APIs with a name like "team/backup", "..", ".", "/etc/passwd", "C:\\sop", a name containing '\0', or any name built by joining user input with path separators.

Common situations: Names derived from file paths or URLs; user-supplied names passed unchecked from a CLI or web form; tools that assume Windows separators are safe because the server runs on Linux; empty or dot-only names.

Related errors


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