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
- 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.
- 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.
- 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.
- After editing, verify with :theme <name> inside Helix or by launching hx; a successful switch means the chain now terminates.
- 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
- Never let a theme's inherits key equal the theme's own file name when only one file of that name exists.
- When splitting a theme into parent + child files, keep the parent chain acyclic: draw the graph once (child -> parent) and check no arrow points back.
- Keep user overrides in ~/.config/helix/themes/ and treat the runtime themes/ dir as read-only base themes; same-directory shadowing is the main source of accidental loops.
- Run :theme <name> after every inherits edit — it exercises the full chain immediately.
- If you embed Helix's loader in an app, run validate_inherits_chain on all shipped themes in CI so a loop fails the build, not the user's editor.
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
- Expected 'inherits' to be a string: {}
- File not found for: {}
- Failed to load config: {}
- Could not load theme: {}
- Command not provided
AI-assisted analysis of helix-editor/helix@079a789e8c (2026-08-16).
Data as JSON: /api/errors/a2f6e5130c4653ef.
Report an issue: GitHub.