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
- Use a SOP name with letters/digits so the slug is a normal component, e.g. 'deploy-check'
- Pre-validate in your draft-creation code: reject names where Path::new(&slugify(name)).components() is not a single Normal component
- 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
- Include dot-only names in your SOP-name validation test matrix
- Treat slugify output as untrusted until checked with the same Normal-component rule
- Prefer strict slug regexes for generated names
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
- invalid SOP name '{name}': must be a single path component (
- SOP name does not contain a safe path component
- target SOP directory '{}' escapes SOPs root
- Path not allowed: parent-directory traversal is not allowed
- Lark/Feishu marker target uses a disallowed scheme
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/b9800da36869fbdd.
Report an issue: GitHub.