Hmbown/CodeWhale · error

sub-agent state path must include a file name

Error message

sub-agent state path must include a file name

What it means

`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.

Source

Thrown at crates/tui/src/tools/subagent/mod.rs:7680

    // is migrated on load (see load_state).
    checked_subagent_state_path(
        &state_root,
        &Path::new(".codewhale")
            .join("state")
            .join(SUBAGENT_STATE_FILE),
    )
}

fn checked_subagent_state_path(state_root: &Path, path: &Path) -> Result<PathBuf> {
    let state_root = normalize_subagent_workspace(state_root);
    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"))?;
    let parent = match parent.canonicalize() {
        Ok(parent) => parent,
        Err(err) if err.kind() == std::io::ErrorKind::NotFound => normalize_path_components(parent),
        Err(err) => return Err(err.into()),
    };
    let state_path = parent.join(file_name);
    if !state_path.starts_with(&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)
}

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Validate that the path's last non-dot component is a real file name before passing it in.
  2. Strip trailing separators and '..' tails, and reject paths with no file component outright.
  3. Build state paths as state_root.join(dir).join(file_name) with a non-empty file_name.

Example fix

// before
let p = format!("{}/..", agent_dir); // tail is '..', no file name
checked_subagent_state_path(root, Path::new(&p))?; // Err 1210

// after
let p = agent_dir.join(format!("{agent_id}.jsonl"));
checked_subagent_state_path(root, &p)?;
Defensive patterns

Strategy: validation

Validate before calling

fn has_file_name(rel: &str) -> bool {
    std::path::Path::new(rel).file_name().is_some()
        && std::path::Path::new(rel).components().last()
            .map_or(false, |c| matches!(c, std::path::Component::Normal(_)))
}

Type guard

fn is_wellformed_state_path(rel: &str) -> bool {
    has_file_name(rel) && !rel.ends_with("..")
}

Try / catch

match checked_subagent_state_path(&state_root, Path::new(rel)) {
    Err(e) if e.to_string().contains("must include a file name") => { /* reject input, request a concrete file path */ }
    r => r?,
}

Prevention

When it happens

Trigger: 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.

Common situations: Agent tool calls echoing unsanitized paths; string-built paths from config with trailing separators or dot segments; templating that leaves an empty file component.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20). Data as JSON: /api/errors/4ee4d527829d77a8. Report an issue: GitHub.