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
- Give the SOP a name containing at least one letter or digit, e.g. "deploy-check"
- 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
- 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
- Require at least one alphanumeric word in SOP names at draft creation
- Never auto-generate names from symbols/punctuation placeholders
- Unit-test name generation against slugify for every locale you support
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
- candidate SOP did not validate as exactly one loadable SOP
- candidate manifest name '{}' does not match proposal target
- candidate SOP has no parsed steps
- cannot resolve SOP path '{}'
- unsafe SOP path component
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/3f18a12b54cbcb53.
Report an issue: GitHub.