helix-editor/helix · error · anyhow::Error

Cycle found in inheriting: {}

Error message

Cycle found in inheriting: {}

What it means

Helix's theme Loader resolves the 'inherits' key recursively: each parent theme name is looked up as <name>.toml across the theme directories (highest priority first), and every visited file path is recorded in a HashSet. When the chain arrives at a theme file whose exact path was already visited, the loader refuses to re-enter it (marking cycle_found), and if no lower-priority directory supplies a fresh file it raises 'Cycle found in inheriting: {name}'. This is a hard stop: the merged theme TOML is never produced and load() returns an Err.

Source

Thrown at helix-view/src/theme.rs:245

        let mut cycle_found = false; // track if there was a path, but it was in a cycle
        self.theme_dirs
            .iter()
            .find_map(|dir| {
                let path = dir.join(&filename);
                if !path.exists() {
                    None
                } else if visited_paths.contains(&path) {
                    // Avoiding cycle, continuing to look in lower priority directories
                    cycle_found = true;
                    None
                } else {
                    visited_paths.insert(path.clone());
                    Some(path)
                }
            })
            .ok_or_else(|| {
                if cycle_found {
                    anyhow!("Cycle found in inheriting: {}", name)
                } else {
                    anyhow!("File not found for: {}", name)
                }
            })
    }

    pub fn default_theme(&self, true_color: bool) -> Theme {
        if true_color {
            self.default()
        } else {
            self.base16_default()
        }
    }

    /// Returns the default theme
    pub fn default(&self) -> Theme {
        DEFAULT_THEME.clone()
    }

View on GitHub (pinned to 079a789e8c)

Solutions

  1. Trace the loop: start at the theme named in the message, read its inherits key, and follow the chain file by file until you return to a file you already saw; that back-edge is the bug.
  2. Fix the self/mutual reference: in the offending toml, change inherits to a real ancestor (usually "default") or delete the inherits key entirely if the theme is standalone.
  3. If you intentionally inherit from a same-named theme, make sure the parent file lives in a lower-priority themes directory (e.g. the runtime themes/ dir while your override sits in the user config dir) — same directory shadowing is what closes the loop.
  4. After editing, verify with :theme <name> inside Helix or by launching hx; a successful switch means the chain now terminates.
  5. If you are a library consumer calling Loader::load, validate the inherits graph yourself before loading (see defense section) so a user-authored loop cannot abort startup.

Example fix

# before — ~/.config/helix/themes/mytheme.toml (file is named mytheme.toml)
inherits = "mytheme"

# after
inherits = "default"
Defensive patterns

Strategy: validation

Validate before calling

// Walk the inherits chain before calling Loader::load,
// using the same dirs you passed to Loader::new (priority order).
use std::collections::HashSet;
use std::path::PathBuf;

fn validate_inherits_chain(mut name: &str, dirs: &[PathBuf]) -> Result<(), String> {
    let theme_dirs: Vec<PathBuf> = dirs.iter().map(|d| d.join("themes")).collect();
    let mut visited: HashSet<PathBuf> = HashSet::new();
    loop {
        if name == "default" || name == "base16_default" {
            return Ok(()); // built-ins are served from embedded data
        }
        let filename = format!("{name}.toml");
        let path = theme_dirs
            .iter()
            .map(|d| d.join(&filename))
            .find(|p| p.exists())
            .ok_or_else(|| format!("File not found for: {name}"))?;
        if !visited.insert(path.clone()) {
            return Err(format!("Cycle found in inheriting: {name}"));
        }
        let raw = std::fs::read_to_string(&path).map_err(|e| e.to_string())?;
        let value: toml::Value = toml::from_str(&raw).map_err(|e| e.to_string())?;
        match value.get("inherits").and_then(|v| v.as_str()) {
            Some(parent) => name = parent,
            None => return Ok(()),
        }
    }
}

Type guard

fn has_inherits_cycle(dirs: &[PathBuf], name: &str) -> bool {
    validate_inherits_chain(name, dirs).is_err()
        && validate_inherits_chain(name, dirs)
            .unwrap_err()
            .starts_with("Cycle found")
}

Try / catch

// anyhow errors are untyped here, so match on the message prefix:
match loader.load(theme_name) {
    Ok(theme) => theme,
    Err(err) if err.to_string().starts_with("Cycle found in inheriting") => {
        log::warn!("theme '{theme_name}' has an inherits loop, using default");
        loader.default_theme(true_color)
    }
    Err(err) => return Err(err),
}

Prevention

When it happens

Trigger: Calling Loader::load / load_with_warnings with a theme whose inheritance chain loops back on itself: (1) a theme file containing inherits pointing to its own name (mytheme.toml with inherits = "mytheme") when only one file of that name exists; (2) mutual recursion, A.toml inherits "B" and B.toml inherits "A"; (3) a longer transitive loop A -> B -> C -> A. Note that inheriting a same-named file in a LOWER-priority themes directory is legal (different path), so the error strictly means the same path was re-entered.

Common situations: Users editing custom themes in ~/.config/helix/themes/ who copy a theme file and rename the file but forget to update the inherits key (so it inherits itself); hand-built theme families where a parent/child pair reference each other; refactoring a inherits chain (e.g. base -> common -> base) and introducing a back-edge; shipping a theme that inherits a user's local override of the same name, turning an intended shadowing into a loop.

Related errors


AI-assisted analysis of helix-editor/helix@079a789e8c (2026-08-16). Data as JSON: /api/errors/a2f6e5130c4653ef. Report an issue: GitHub.