AprilNEA/OpenLogi · error

output must not be a symlink

Error message

{asset} output must not be a symlink

What it means

`reject_symlink` ensures an output path that is about to be published is not a symlink; unlike the require_* helpers it tolerates a nonexistent path (NotFound is OK, since publishing will create it) but bails if the path already exists as a symlink, and wraps other inspection errors with `could not inspect {asset} output`.

Solutions

  1. Remove the symlink at the output path (`rm <path>`) and re-run finish so a real file is written.
  2. If the link was intentional (e.g. sync tooling), move the actual data to the output location and delete the link first.
  3. Inspect with `ls -la <path>` to confirm what exists before re-running.

Example fix

// before (state.json -> ~/sync/state.json)
// after
rm out/mx-master/state.json && openlogi fixture contribute finish --id mx-master --name "MX Master" ...
Defensive patterns

Strategy: validation

Validate before calling

[ ! -L "$OUT_PATH" ] || { echo "$OUT_PATH must not be a symlink" >&2; exit 1; }

Prevention

When it happens

Trigger: publish_manifest_and_finish finds an existing symlink at an output location (manifest or state output) while finalizing the contribution. Also fires on any non-NotFound I/O error while inspecting the path.

Common situations: A previous tool or setup linked the output file to elsewhere (e.g. symlinked state.json into a synced folder); leftover links from an older workflow.

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


AI-assisted analysis of AprilNEA/OpenLogi@e846e6f4b4 (2026-09-13). Data as JSON: /api/errors/9de9e88719f8f738. Report an issue: GitHub.

Appendix: source

Thrown at crates/openlogi-cli/src/cmd/fixture/contribute.rs:503

    if metadata.file_type().is_symlink() || !metadata.is_dir() {
        bail!("{asset} must be a non-symlink directory");
    }
    Ok(())
}

fn require_regular_file(path: &Path, asset: &str) -> Result<()> {
    let metadata =
        fs::symlink_metadata(path).with_context(|| format!("could not inspect {asset}"))?;
    if metadata.file_type().is_symlink() || !metadata.is_file() {
        bail!("{asset} must be a non-symlink regular file");
    }
    Ok(())
}

fn reject_symlink(path: &Path, asset: &str) -> Result<()> {
    match fs::symlink_metadata(path) {
        Ok(metadata) if metadata.file_type().is_symlink() => {
            bail!("{asset} output must not be a symlink")
        }
        Ok(_) => Ok(()),
        Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
        Err(error) => Err(error).with_context(|| format!("could not inspect {asset} output")),
    }
}

#[cfg(test)]
mod tests {
    use openlogi_fixture::CANONICAL_DEVICE_PROFILE_JSON;

    use super::*;

    #[test]
    fn output_directory_must_match_the_synthetic_id() {
        let args = ContributeArgs {
            id: "fixture-001".to_string(),
            name: "Synthetic mouse".to_string(),

View on GitHub (pinned to e846e6f4b4)