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

SOP name does not contain a safe path component

Error message

SOP name does not contain a safe path component

What it means

contained_sop_dir derives the on-disk SOP directory as sops_root.join(slugify(sop_name)). slugify strips everything that is not a safe path character; if the result is the empty string, the SOP name consisted entirely of unsafe characters and there is no directory name to write to. The bail fires before any path is joined, so nothing is written.

Source

Thrown at crates/zeroclaw-runtime/src/sop/procedural_memory.rs:380

    procedure_markdown: &str,
) -> Result<()> {
    fs::create_dir_all(target_dir)?;
    atomic_write_file(&target_dir.join("SOP.toml"), manifest_toml)?;
    atomic_write_file(&target_dir.join("SOP.md"), procedure_markdown)?;
    Ok(())
}

fn atomic_write_file(path: &Path, content: &str) -> Result<()> {
    let tmp = path.with_extension("tmp");
    fs::write(&tmp, content)?;
    fs::rename(&tmp, path)?;
    Ok(())
}

fn contained_sop_dir(sops_root: &Path, sop_name: &str) -> Result<PathBuf> {
    let slug = slugify(sop_name);
    if slug.is_empty() {
        bail!("SOP name does not contain a safe path component");
    }
    ensure_relative_component(&slug)?;
    let target = sops_root.join(slug);
    ensure_within_root(sops_root, &target)?;
    Ok(target)
}

/// Validate that an already-existing SOP directory (taken from the loaded
/// `Sop.location`) stays within `sops_root`, rejecting `..` and symlink escapes.
fn contained_existing_dir(sops_root: &Path, location: &Path) -> Result<PathBuf> {
    let target = location.to_path_buf();
    ensure_within_root(sops_root, &target)?;
    Ok(target)
}

fn ensure_within_root(sops_root: &Path, target: &Path) -> Result<()> {
    let root = resolve_existing_ancestor(sops_root)?;
    let resolved_target = resolve_existing_ancestor(target)?;

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Give the SOP a name containing at least one letter or digit, e.g. "deploy-check"
  2. If this fires on apply_proposal for an Update, inspect the stored proposal's sop_name and the loaded SOP's name - fix the store record or the SOP's manifest name
  3. Add a pre-check in your proposal-creation UI: slugify(name).is_empty() means reject before calling create_proposal

Example fix

// before
create_proposal(&engine, ProposalDraft { sop_name: "???".into(), .. })?;

// after
create_proposal(&engine, ProposalDraft { sop_name: "deploy check".into(), .. })?;
Defensive patterns

Strategy: validation

Validate before calling

fn valid_sop_name(name: &str) -> bool {
    !slugify(name).is_empty()
}

Type guard

fn is_safe_sop_name(name: &str) -> bool { !slugify(name).is_empty() }

Try / catch

Err(e) if e.to_string().contains("does not contain a safe path component") => {
    // reject the draft at the UI/model boundary and ask for a word-based name
}

Prevention

When it happens

Trigger: apply_proposal or create_proposal with a sop_name made only of characters slugify removes - punctuation/symbols/whitespace-only strings like "???", "---", "...", " "; or the Update path where a previously loaded SOP's stored name slugifies to empty (data corrupted or hand-edited store).

Common situations: Model-generated proposal drafts with placeholder names ('###', '...'), SOP stores edited by hand, or names in a non-Latin script that the slugifier does not transliterate.

Related errors


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