Hmbown/CodeWhale · error · anyhow::Error
{kind} must be a single path component
Error message
{kind} must be a single path component What it means
The second ensure_safe_storage_id check requires the id to be exactly one Normal path component. It rejects ids containing '/', parent segments ('..'), current-directory segments ('.'), absolute prefixes, and any other multi-component or non-normal path shape. This is the path-traversal guard that keeps storage writes inside the automation/task directories.
Source
Thrown at crates/tui/src/automation_manager.rs:1837
let run: AutomationRunRecord = serde_json::from_str(&raw)
.with_context(|| format!("Failed to parse {}", path.display()))?;
if run.schema_version > CURRENT_RUN_SCHEMA_VERSION {
bail!(
"Automation run schema v{} is newer than supported v{}",
run.schema_version,
CURRENT_RUN_SCHEMA_VERSION
);
}
Ok(run)
}
fn ensure_safe_storage_id(kind: &str, value: &str) -> Result<()> {
let mut components = Path::new(value).components();
let Some(component) = components.next() else {
bail!("{kind} must not be empty");
};
if components.next().is_some() || !matches!(component, std::path::Component::Normal(_)) {
bail!("{kind} must be a single path component");
}
Ok(())
}
fn validate_name_and_prompt(name: &str, prompt: &str) -> Result<()> {
if name.trim().is_empty() {
bail!("Automation name is required");
}
if prompt.trim().is_empty() {
bail!("Automation prompt is required");
}
Ok(())
}
fn normalize_optional_string(value: Option<String>) -> Option<String> {
value
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())View on GitHub (pinned to 0c42157ee5)
Solutions
- Use a flat, opaque id (uuid or slugified name) instead of a path-like string.
- Sanitize before saving: replace '/' and other separators with '-' and strip leading dots.
- Enforce an id pattern such as ^[A-Za-z0-9_-]+$ at the input boundary.
Example fix
// before
let automation_id = format!("{}/{}", team, name); // contains '/' -> bails
// after
let automation_id = format!("{}-{}", team, name)
.chars()
.map(|c| if c.is_ascii_alphanumeric() || c == '-' || c == '_' { c } else { '-' })
.collect::<String>(); Defensive patterns
Strategy: validation
Validate before calling
use std::path::{Component, Path};
fn is_safe_storage_id(id: &str) -> bool {
let mut components = Path::new(id).components();
matches!(components.next(), Some(Component::Normal(_))) && components.next().is_none()
} Prevention
- Never accept raw paths or URLs as storage ids.
- Slugify human-readable names before persisting them as ids.
- Add a unit test asserting generated ids pass the single-component check.
When it happens
Trigger: Passing an id like 'a/b', '../secrets', '.', '..', or '/etc/passwd' where an automation id, trigger id, run id, or task id is expected; ids assembled by joining user input with '/'.
Common situations: Reusing a file path or URL slug as the storage id; concatenating team/name into an id with separators; ids built from free-form model or task names that contain slashes.
Related errors
- persistent path allow rules must stay within the workspace
- persistent allow rules must be scoped to a workspace
- persistent command allow rules must use exact matching
- persistent allow rules must match an exact command or path
- persistent command allow rules must not be empty
AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20).
Data as JSON: /api/errors/ca8961ad6ed492b5.
Report an issue: GitHub.