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 theView on GitHub (pinned to 88bb9c8533)
Solutions
- Pass a single-component slug: sanitize user input by replacing separators with '-' (or reject it) before calling any SOP API.
- Validate early at the trust boundary with your own single-component check so the bad name never reaches the store layer.
- 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
- Reject multi-component names at the API/CLI boundary, before they reach the store.
- Flatten hierarchical names into slugs ('team-x' not 'team/x').
- Never build SOP names from filesystem paths or URLs.
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
- unsafe SOP 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
- Lark/Feishu marker target resolves outside workspace_dir
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/daff6579cc98b66f.
Report an issue: GitHub.