nikivdev/code · error · anyhow::Error

cycle detected while loading config includes: {}

Error message

cycle detected while loading config includes: {}

What it means

Raised by `load_discovery_config` (src/discover.rs:315) while recursively loading config includes: it canonicalizes each path and, if that canonical path is already in the `visited` list, a cycle exists (e.g. a includes b which includes a). The cycle would otherwise cause infinite recursion, so loading aborts and reports the offending path.

Source

Thrown at src/discover.rs:315

        .cloned()
        .unwrap_or_else(|| "root".to_string());
    (primary, aliases)
}

fn push_watched_path(paths: &mut Vec<PathBuf>, path: &Path) {
    if !paths.iter().any(|existing| existing == path) {
        paths.push(path.to_path_buf());
    }
}

fn load_discovery_config(
    path: &Path,
    visited: &mut Vec<PathBuf>,
    watched_paths: &mut Vec<PathBuf>,
) -> Result<LoadedDiscoveryConfig> {
    let canonical = path.canonicalize()?;
    if visited.contains(&canonical) {
        anyhow::bail!(
            "cycle detected while loading config includes: {}",
            path.display()
        );
    }
    visited.push(canonical.clone());
    push_watched_path(watched_paths, &canonical);

    let contents = fs::read_to_string(&canonical)?;
    let mut cfg = parse_discovery_config(&canonical, &contents)?;

    let mut project_name = cfg.project_name.take();
    let mut tasks = cfg.tasks;
    let mut task_resolution = cfg.task_resolution.take();

    for include in cfg.command_files {
        let include_path = config::resolve_include_path(&canonical, &include.path);
        let included = load_discovery_config(&include_path, visited, watched_paths)?;
        if project_name.is_none() {

View on GitHub (pinned to a747e741ae)

Solutions

  1. Trace the include chain and remove the include that points back to an already-included file
  2. Break the cycle by extracting shared settings into a common file included once
  3. Replace symlink loops with a single canonical shared config path
  4. The message names the path where the cycle was detected — start pruning there

Example fix

// before (a.toml)
include = ["b.toml"]
# b.toml
include = ["a.toml"]
// after
# a.toml
include = ["b.toml"]
# b.toml — remove the back-reference
include = []
Defensive patterns

Strategy: validation

Validate before calling

fn has_include_cycle(entry: &Path) -> Result<bool> {
    let mut seen = std::collections::HashSet::new();
    let mut stack = vec![entry.canonicalize()?];
    while let Some(p) = stack.pop() {
        if !seen.insert(p.clone()) { return Ok(true); }
        for inc in parse_includes(&p)? {
            stack.push(inc.canonicalize()?);
        }
    }
    Ok(false)
}
// call before loading; skip load if it returns Ok(true)

Type guard

fn acyclic(entry: &Path) -> bool { has_include_cycle(entry).unwrap_or(false) }

Try / catch

match load_discovery_config(path, &mut visited, &mut watched) {
    Err(e) if e.to_string().contains("cycle detected while loading config includes") => {
        eprintln!("fix the include chain at {} and retry", e.to_string());
    }
    other => other?,
}

Prevention

When it happens

Trigger: A discovery config include chain that revisits any file: `a.toml` includes `b.toml` which includes `a.toml`, a self-include (`a.toml` includes `a.toml`), or symlinked directories whose canonicalized paths resolve back into an already-visited file.

Common situations: Copying an include block into a nested config without removing it from the parent, symlinking a shared config directory back onto itself, or merge tooling duplicating includes.

Related errors


AI-assisted analysis of nikivdev/code@a747e741ae (2026-09-01). Data as JSON: /api/errors/5f4607b493c058d1. Report an issue: GitHub.