Hmbown/CodeWhale · error

sub-agent state path must not traverse symlinks: {}

Error message

sub-agent state path must not traverse symlinks: {}

What it means

Walking the path component by component from the state root, `reject_root_relative_symlinks` found an existing component that is a symlink. The guard refuses to traverse any symlink inside a state path so a tampered link cannot redirect reads or writes outside the root, even when the final target looks contained.

Source

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

        normalized
    }
}

fn reject_root_relative_symlinks(root: &Path, path: &Path) -> Result<()> {
    let relative = path.strip_prefix(root).map_err(|_| {
        anyhow!(
            "sub-agent state path must stay within state root: {}",
            path.display()
        )
    })?;
    let mut current = root.to_path_buf();
    for component in relative.components() {
        current.push(component.as_os_str());
        let Ok(metadata) = fs::symlink_metadata(&current) else {
            continue;
        };
        if metadata.file_type().is_symlink() {
            return Err(anyhow!(
                "sub-agent state path must not traverse symlinks: {}",
                current.display()
            ));
        }
    }
    Ok(())
}

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() {
        return Err(anyhow!(
            "sub-agent state path must be a regular file: {}",
            path.display()
        ));

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Find the offending component (the error names the exact path) and replace the symlink with a real directory.
  2. Move the state root to a path with no symlinked components (e.g. a direct path under ~/.local/share or an explicit non-linked dir).
  3. When unpacking state archives, use flags that don't preserve symlinks for internal components.

Example fix

# before: state root contains a symlink
ln -s /tmp/escape ~/.codewhale/agents
# -> Err 1214 on next artifact read

# after
rm ~/.codewhale/agents && mkdir ~/.codewhale/agents
Defensive patterns

Strategy: validation

Validate before calling

fn path_traverses_symlink(root: &Path, rel: &Path) -> bool {
    let mut cur = root.to_path_buf();
    for c in rel.components() {
        cur.push(c.as_os_str());
        if let Ok(m) = std::fs::symlink_metadata(&cur) {
            if m.file_type().is_symlink() { return true; }
        }
    }
    false
}

Type guard

fn is_symlink_free(root: &Path, rel: &Path) -> bool {
    !path_traverses_symlink(root, rel)
}

Try / catch

match read_subagent_state_file(&state_root, &path) {
    Err(e) if e.to_string().contains("must not traverse symlinks") => { /* error names the component: replace it with a real dir */ }
    r => r?,
}

Prevention

When it happens

Trigger: Any directory component of the state path replaced by a symlink: an attacker- or child-created link inside the state dir, a symlinked projects folder, or sync tools (Dropbox-style) materializing links.

Common situations: State directory inside a symlinked home or workspace; symlink-based version switching of state dirs; adversarial model output planting links; restoring state from archives that preserve links.

Related errors


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