Hmbown/CodeWhale · error · anyhow::Error
sub-agent state path must not traverse symlinks
Error message
sub-agent state path must not traverse symlinks: {} What it means
Even after top-level containment passes, the library walks each component of the path relative to the state root with `fs::symlink_metadata`, rejecting the operation if any intermediate component is itself a symlink. This closes the TOCTOU-style hole where an in-root symlink redirects deeper segments outside the state root.
Solutions
- Replace the intermediate symlink with a real directory (copy contents back into the state root).
- If using dotfile managers (stow, chezmoi, GNU stow layouts), configure them not to symlink inside the state root.
- Bind-mount or copy the external directory into the state root instead of linking.
- Identify the offending component from the error's displayed path and remove that link.
Example fix
// before: symlinked subdirectory inside state root ln -s /var/lib/shared state/sessions // after mkdir state/sessions && cp /var/lib/shared/* state/sessions/
Defensive patterns
Strategy: validation
Validate before calling
use std::fs;
fn no_symlink_components(root: &std::path::Path, p: &std::path::Path) -> bool {
let mut cur = root.to_path_buf();
for comp in p.strip_prefix(root).unwrap_or(p).components() {
cur.push(comp.as_os_str());
if fs::symlink_metadata(&cur).map(|m| m.file_type().is_symlink()).unwrap_or(false) {
return false;
}
}
true
}
// guard: no_symlink_components(&state_root, &path) Try / catch
match load_state(root, path) {
Ok(v) => v,
Err(e) if e.to_string().contains("must not traverse symlinks") => {
eprintln!("symlinked component in state path: {e}");
// replace the symlink with a real directory, then retry once
Default::default()
}
Err(e) => return Err(e),
} Prevention
- Configure dotfile managers (stow/chezmoi) to copy, not symlink, under the state root.
- Bind-mount external directories instead of linking them into the state root.
- Scan the state root periodically for symlinked directories.
When it happens
Trigger: Any sub-agent state read/write where a directory component between the state root and the final file is a symlink — e.g. `state/link/session.json` where `link` is a symlink.
Common situations: Users symlink state subdirectories into dotfile-managed or shared locations; container images replace state dirs with symlinks to volumes; backup/restore tooling replaced directories with links.
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 stay within state root
- 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/5a82ee2297408c84.
Report an issue: GitHub.
Appendix: source
Thrown at crates/tui/src/tools/subagent/mod.rs:8899
{
return Err(anyhow!(
"sub-agent state path must stay within state root: {}",
path.display()
));
}
}
};
(resolved_root, relative)
}
};
let mut current = walk_root;
for component in relative.components() {
current.push(component.as_os_str());
let Ok(metadata) = fs::symlink_metadata(¤t) else {
continue;
};
if metadata.file_type().is_symlink() {
return Err(anyhow!(
"sub-agent state path must not traverse symlinks: {}",
current.display()
));
}
}
Ok(())
}
/// Maximum bytes read from a sub-agent state or transcript artifact file.
/// Generous for long session histories; anything larger is corrupt.
const MAX_SUBAGENT_STATE_BYTES: u64 = 16 * 1024 * 1024;
fn read_subagent_state_file(state_root: &Path, path: &Path) -> Result<String> {
let state_root = normalize_subagent_workspace(state_root);
reject_root_relative_symlinks(&state_root, path)?;
let metadata = fs::symlink_metadata(path)?;
let file_type = metadata.file_type();
if file_type.is_symlink() || !file_type.is_file() {View on GitHub (pinned to 73e0f67d83)