zeroclaw-labs/zeroclaw · error · anyhow::Error
target SOP directory '{}' escapes SOPs root
Error message
target SOP directory '{}' escapes SOPs root What it means
ensure_within_root canonicalizes both the SOPs root and the target directory by resolving existing ancestors, then requires the resolved target to still start with the resolved root. This error means that after symlink resolution the target lives outside the SOPs root - a containment violation. The guard protects apply_proposal from writing SOP files through a symlink that escapes the sops directory.
Source
Thrown at crates/zeroclaw-runtime/src/sop/procedural_memory.rs:400
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)?;
if !resolved_target.starts_with(&root) {
bail!(
"target SOP directory '{}' escapes SOPs root",
target.display()
);
}
Ok(())
}
/// Canonicalize the nearest existing ancestor of `path` and re-append the
/// not-yet-created trailing components verbatim. A `..` or symlink that escapes
/// is caught because the existing portion is canonicalized; a trailing
/// component is rejected unless it is a plain name.
fn resolve_existing_ancestor(path: &Path) -> Result<PathBuf> {
let mut remainder: Vec<&std::ffi::OsStr> = Vec::new();
let mut current = path;
loop {
if current.exists() {
let mut resolved = fs::canonicalize(current)
.with_context(|| format!("canonicalize '{}'", current.display()))?;View on GitHub (pinned to 88bb9c8533)
Solutions
- Make the sops directory a real directory: point sops_dir at the actual path (or bind-mount) instead of a symlink, then re-run apply_proposal
- Audit the sops root for symlinks: find <sops_root> -type l -ls and remove or replace any that resolve outside the root
- If the escape was not intentional, treat it as a security incident - inspect what created the symlink before deleting it
Example fix
# before ln -s /mnt/shared/sops ~/.zeroclaw/sops # after sudo mount --bind /mnt/shared/sops ~/.zeroclaw/sops # or move the directory and update sops_dir in config to the real path
Defensive patterns
Strategy: validation
Validate before calling
// Before apply_proposal, assert containment the same way the guard does:
fn dir_within_root(sops_root: &Path, target: &Path) -> bool {
let r = std::fs::canonicalize(sops_root).unwrap_or_else(|_| sops_root.to_path_buf());
let t = std::fs::canonicalize(target).unwrap_or_else(|_| target.to_path_buf());
t.starts_with(&r)
} Try / catch
Err(e) if e.to_string().contains("escapes SOPs root") => {
// stop and audit the sops dir for symlinks; never catch-and-continue here
} Prevention
- Keep the sops directory a real directory (no symlinks, no bind tricks on the path components)
- Run 'find <sops_root> -type l' as part of deployment sanity checks
- Pin sops_dir in config to an absolute real path
When it happens
Trigger: apply_proposal when the configured sops directory (resolve_sops_dir(install_root, config.sops_dir)) or the computed target dir contains a symlink whose resolution points outside the root - e.g. sops_dir set to a symlinked path, or a symlinked SOP directory inside the sops root pointing elsewhere; also a TOCTOU-style swap of a directory component for a symlink between checks.
Common situations: Users 'relocating' the sops folder with a symlink to another disk or a dotfiles-managed directory, container setups where the sops dir is a symlink into a volume mount, or an adversarial filesystem actor planting symlinks.
Related errors
- attachment path {} canonicalizes to {} which escapes workspa
- skill '{$skill}' in {$url} is a symlink; catalog skills must
- invalid SOP name '{name}': must be a single path component (
- unsafe SOP path component '{}'
- unsafe SOP path component
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/cfff5f2e973ddcd3.
Report an issue: GitHub.