{"record":{"id":"5486c1195450cc41","repo":"Hmbown/CodeWhale","slug":"sub-agent-state-path-must-stay-within-state-root","errorCode":null,"errorMessage":"sub-agent state path must stay within state root: {}","messagePattern":"sub-agent state path must stay within state root: (.+?)","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/tui/src/tools/subagent/mod.rs","lineNumber":8662,"sourceCode":"    let absolute = if path.is_absolute() {\n        path.to_path_buf()\n    } else {\n        state_root.join(path)\n    };\n    let file_name = absolute\n        .file_name()\n        .ok_or_else(|| anyhow!(\"sub-agent state path must include a file name\"))?;\n    let parent = absolute\n        .parent()\n        .ok_or_else(|| anyhow!(\"sub-agent state path must include a parent directory\"))?;\n    // Resolve through the deepest existing ancestor so Windows short/long names\n    // and verbatim (`\\\\?\\`) spellings match the already-canonical state root.\n    // A missing parent used to fall back to lexical normalization and could keep\n    // an 8.3 component (for example `RUNNER~1`) that no longer prefixes the root.\n    let parent = resolve_path_for_containment(parent)?;\n    let state_path = parent.join(file_name);\n    if !path_is_within_state_root(&state_path, &state_root) {\n        return Err(anyhow!(\n            \"sub-agent state path must stay within state root: {}\",\n            state_path.display()\n        ));\n    }\n    reject_root_relative_symlinks(&state_root, &state_path)?;\n    Ok(state_path)\n}\n\n/// Canonicalize `path` when it exists; otherwise canonicalize the deepest\n/// existing ancestor and re-append the missing tail. Keeps containment checks\n/// on the same identity spelling as [`normalize_subagent_workspace`].\nfn resolve_path_for_containment(path: &Path) -> Result<PathBuf> {\n    let mut missing = Vec::new();\n    let mut current = path.to_path_buf();\n    loop {\n        match current.canonicalize() {\n            Ok(canonical) => {\n                let mut resolved = canonical;","sourceCodeStart":8644,"sourceCodeEnd":8680,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/73e0f67d83c59909b571efdfc88c4bc28c309cb1/crates/tui/src/tools/subagent/mod.rs#L8644-L8680","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: symlink escapes the state root\nln -s /etc codewhale/state/escape\nresolve_state_path(root, \"state/escape/passwd\")\n// after: keep state inside the root\nmkdir -p codewhale/state/inner\nresolve_state_path(root, \"state/inner/session.json\")","handlingStrategy":"validation","validationCode":"use std::path::{Path, Component};\nfn lexically_inside(root: &Path, p: &Path) -> bool {\n    p.canonicalize().ok()\n        .map(|c| c.starts_with(root.canonicalize().unwrap_or_else(|_| root.to_path_buf())))\n        .unwrap_or(false)\n}\n// reject user input containing `..` before it reaches the API","typeGuard":null,"tryCatchPattern":"match load_state(root, path) {\n    Ok(v) => v,\n    Err(e) if e.to_string().contains(\"must stay within state root\") => {\n        // path escaped the root: do not retry; fix the path or symlinks\n        eprintln!(\"refusing out-of-root state path: {path:?}\");\n        Default::default()\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["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."],"tags":["subagent","path-traversal","security","symlink"],"backgroundTag":"path-traversal-blocked","analyzedSha":"73e0f67d83c59909b571efdfc88c4bc28c309cb1","analyzedAt":"2026-09-22T01:30:00.501Z","contentChangedAt":"2026-09-22T01:30:00.501Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}