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

unsafe SOP path component

Error message

unsafe SOP path component

What it means

ensure_relative_component checks that a single path component (the slug produced by slugify(sop_name)) consists only of Normal components. It runs after the empty-slug check in contained_sop_dir, so it fires when the slug is non-empty but is itself a special component - '.' or '..' (CurDir/ParentDir). It is the last guard before sops_root.join(slug).

Source

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

                    .any(|c| !matches!(c, Component::Normal(_)))
                {
                    bail!("unsafe SOP path component '{}'", name.to_string_lossy());
                }
                remainder.push(name);
                current = parent;
            }
            _ => bail!("cannot resolve SOP path '{}'", path.display()),
        }
    }
}

fn ensure_relative_component(component: &str) -> Result<()> {
    let path = Path::new(component);
    if path
        .components()
        .any(|c| !matches!(c, Component::Normal(_)))
    {
        bail!("unsafe SOP path component");
    }
    Ok(())
}

fn hash_sop_dir(dir: &Path) -> Result<String> {
    let mut hasher = Sha256::new();
    for name in ["SOP.toml", "SOP.md"] {
        let path = dir.join(name);
        hasher.update(name.as_bytes());
        hasher.update([0]);
        if path.exists() {
            hasher.update(fs::read(path)?);
        }
        hasher.update([0]);
    }
    Ok(format!("sha256:{}", hex::encode(hasher.finalize())))
}

View on GitHub (pinned to 88bb9c8533)

Solutions

  1. Use a SOP name with letters/digits so the slug is a normal component, e.g. 'deploy-check'
  2. Pre-validate in your draft-creation code: reject names where Path::new(&slugify(name)).components() is not a single Normal component
  3. If it fires during apply_proposal, fix the stored proposal's sop_name and re-propose

Example fix

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

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

Strategy: validation

Validate before calling

use std::path::{Path, Component};
fn is_normal_component(slug: &str) -> bool {
    Path::new(slug).components().all(|c| matches!(c, Component::Normal(_)))
}
// gate: valid_sop_name(name) = is_normal_component(&slugify(name)) && !slug.is_empty()

Type guard

fn slug_is_normal(slug: &str) -> bool {
    std::path::Path::new(slug)
        .components()
        .all(|c| matches!(c, std::path::Component::Normal(_)))
}

Try / catch

Err(e) if e.to_string().contains("unsafe SOP path component") => {
    // reject dot-only names upstream; do not sanitize by appending characters blindly
}

Prevention

When it happens

Trigger: create_proposal/apply_proposal with a sop_name that slugifies to '.' or '..' - names consisting of dots such as '.', '..', or names whose only surviving characters after slugification are dots (e.g. '.-.' if hyphens are stripped).

Common situations: Placeholder or joke SOP names made of dots, model-generated drafts with '.' names, or names relying on characters the slugifier removes, leaving only dots behind.

Related errors


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