Hmbown/CodeWhale · error · anyhow::Error
sub-agent state path must stay within state root
Error message
sub-agent state path must stay within state root: {} What it means
This is the containment check for sub-agent state paths: after resolving symlinks and Windows verbatim/8.3 spellings through the deepest existing ancestor, the resulting path must remain inside the canonical state root. If `path_is_within_state_root` returns false — typically because a symlink, `..` component, or alias spelling escaped the root — the operation is refused. This is a security boundary preventing state reads/writes outside the sandbox.
Solutions
- Remove symlinks inside the state root that point outside it and use real directories or bind mounts contained in the root.
- Pass paths that are strictly under the configured state root with no `..` components.
- Re-point the configured state root to the actual canonical location of your state directory.
- Re-create state artifacts inside the current state root instead of linking to old locations.
Example fix
// before: symlink escapes the state root ln -s /etc codewhale/state/escape resolve_state_path(root, "state/escape/passwd") // after: keep state inside the root mkdir -p codewhale/state/inner resolve_state_path(root, "state/inner/session.json")
Defensive patterns
Strategy: validation
Validate before calling
use std::path::{Path, Component};
fn lexically_inside(root: &Path, p: &Path) -> bool {
p.canonicalize().ok()
.map(|c| c.starts_with(root.canonicalize().unwrap_or_else(|_| root.to_path_buf())))
.unwrap_or(false)
}
// reject user input containing `..` before it reaches the API Try / catch
match load_state(root, path) {
Ok(v) => v,
Err(e) if e.to_string().contains("must stay within state root") => {
// path escaped the root: do not retry; fix the path or symlinks
eprintln!("refusing out-of-root state path: {path:?}");
Default::default()
}
Err(e) => return Err(e),
} Prevention
- Reject `..` components in user-supplied state paths up front.
- Keep all state inside the configured root — no symlinks to outside locations.
- Re-point the state root config when the state directory moves.
When it happens
Trigger: Calling a sub-agent state API with a path that, after `resolve_path_for_containment`, does not prefix-match the state root — e.g. a symlink pointing outside the root, or `../` components surviving resolution.
Common situations: Symlinked state directories pointing to a home or tmp location outside the state root; user-supplied paths with traversal segments; migrated state roots where old symlinks now point elsewhere; Windows 8.3 short-name aliases that no longer prefix the root.
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
- sub-agent state path must not traverse symlinks
- external credential path contains reparse point
- owned skill root escapes anchor
- agent action=claim widens an enforced write scope, and the…
- audited skill path does not match owned package
AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22).
Data as JSON: /api/errors/5486c1195450cc41.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/tools/subagent/mod.rs:8662
let absolute = if path.is_absolute() {
path.to_path_buf()
} else {
state_root.join(path)
};
let file_name = absolute
.file_name()
.ok_or_else(|| anyhow!("sub-agent state path must include a file name"))?;
let parent = absolute
.parent()
.ok_or_else(|| anyhow!("sub-agent state path must include a parent directory"))?;
// Resolve through the deepest existing ancestor so Windows short/long names
// and verbatim (`\\?\`) spellings match the already-canonical state root.
// A missing parent used to fall back to lexical normalization and could keep
// an 8.3 component (for example `RUNNER~1`) that no longer prefixes the root.
let parent = resolve_path_for_containment(parent)?;
let state_path = parent.join(file_name);
if !path_is_within_state_root(&state_path, &state_root) {
return Err(anyhow!(
"sub-agent state path must stay within state root: {}",
state_path.display()
));
}
reject_root_relative_symlinks(&state_root, &state_path)?;
Ok(state_path)
}
/// Canonicalize `path` when it exists; otherwise canonicalize the deepest
/// existing ancestor and re-append the missing tail. Keeps containment checks
/// on the same identity spelling as [`normalize_subagent_workspace`].
fn resolve_path_for_containment(path: &Path) -> Result<PathBuf> {
let mut missing = Vec::new();
let mut current = path.to_path_buf();
loop {
match current.canonicalize() {
Ok(canonical) => {
let mut resolved = canonical;View on GitHub (pinned to 73e0f67d83)