{"record":{"id":"4ee4d527829d77a8","repo":"Hmbown/CodeWhale","slug":"sub-agent-state-path-must-include-a-file-name","errorCode":null,"errorMessage":"sub-agent state path must include a file name","messagePattern":"sub-agent state path must include a file name","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/tui/src/tools/subagent/mod.rs","lineNumber":7680,"sourceCode":"    // is migrated on load (see load_state).\n    checked_subagent_state_path(\n        &state_root,\n        &Path::new(\".codewhale\")\n            .join(\"state\")\n            .join(SUBAGENT_STATE_FILE),\n    )\n}\n\nfn checked_subagent_state_path(state_root: &Path, path: &Path) -> Result<PathBuf> {\n    let state_root = normalize_subagent_workspace(state_root);\n    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    let parent = match parent.canonicalize() {\n        Ok(parent) => parent,\n        Err(err) if err.kind() == std::io::ErrorKind::NotFound => normalize_path_components(parent),\n        Err(err) => return Err(err.into()),\n    };\n    let state_path = parent.join(file_name);\n    if !state_path.starts_with(&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}","sourceCodeStart":7662,"sourceCodeEnd":7698,"githubUrl":"https://github.com/Hmbown/CodeWhale/blob/0c42157ee52f9d55af2b506d71b46249910f77d3/crates/tui/src/tools/subagent/mod.rs#L7662-L7698","documentation":"`checked_subagent_state_path` requires the final path component to be a file name before joining it back onto the canonicalized parent. Paths that end in \"..\" or normalize to a bare root (\"/\") have no file_name(), and the guard rejects them before any filesystem access.","triggerScenarios":"A model- or user-supplied relative path whose tail is \"..\" (e.g. state/agents/..), or a constructed path that collapses to the root; typically input that was never sanitized into file.ext form.","commonSituations":"Agent tool calls echoing unsanitized paths; string-built paths from config with trailing separators or dot segments; templating that leaves an empty file component.","solutions":["Validate that the path's last non-dot component is a real file name before passing it in.","Strip trailing separators and '..' tails, and reject paths with no file component outright.","Build state paths as state_root.join(dir).join(file_name) with a non-empty file_name."],"exampleFix":"// before\nlet p = format!(\"{}/..\", agent_dir); // tail is '..', no file name\nchecked_subagent_state_path(root, Path::new(&p))?; // Err 1210\n\n// after\nlet p = agent_dir.join(format!(\"{agent_id}.jsonl\"));\nchecked_subagent_state_path(root, &p)?;","handlingStrategy":"validation","validationCode":"fn has_file_name(rel: &str) -> bool {\n    std::path::Path::new(rel).file_name().is_some()\n        && std::path::Path::new(rel).components().last()\n            .map_or(false, |c| matches!(c, std::path::Component::Normal(_)))\n}","typeGuard":"fn is_wellformed_state_path(rel: &str) -> bool {\n    has_file_name(rel) && !rel.ends_with(\"..\")\n}","tryCatchPattern":"match checked_subagent_state_path(&state_root, Path::new(rel)) {\n    Err(e) if e.to_string().contains(\"must include a file name\") => { /* reject input, request a concrete file path */ }\n    r => r?,\n}","preventionTips":["Build state paths as root.join(dir).join(file_name) with non-empty parts.","Sanitize model-supplied paths at the tool boundary before they reach state APIs."],"tags":["subagent","path-validation","rust"],"backgroundTag":"invalid-file-path","analyzedSha":"0c42157ee52f9d55af2b506d71b46249910f77d3","analyzedAt":"2026-08-20T21:50:45.477Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}