astral-sh/ruff · error
Circular configuration detected: {chain}
Error message
Circular configuration detected: {chain} What it means
Ruff resolves configuration by following `extends` chains from an initial config path. If walking the chain revisits a configuration file already processed, extending is circular and cannot converge, so resolution bails with the visited chain (paths joined by ' extends ').
Source
Thrown at crates/ruff_workspace/src/resolver.rs:361
initial_config_path: &Path,
transformer: &dyn ConfigurationTransformer,
origin: ConfigurationOrigin,
) -> Result<Configuration> {
resolve_configuration_with_cache(initial_config_path, transformer, origin, None)
}
fn resolve_configuration_with_cache(
initial_config_path: &Path,
transformer: &dyn ConfigurationTransformer,
origin: ConfigurationOrigin,
configuration_cache: Option<&ConfigurationCache>,
) -> Result<Configuration> {
let relativity = Relativity::from(origin);
let mut configurations = indexmap::IndexMap::new();
let mut next = Some(fs::normalize_path(initial_config_path));
while let Some(path) = next {
if configurations.contains_key(&path) {
bail!(format!(
"Circular configuration detected: {chain}",
chain = configurations
.keys()
.chain([&path])
.map(|p| format!("`{}`", p.display()))
.join(" extends "),
));
}
let project_root = relativity.resolve(&path);
let load = || {
let options = pyproject::load_options(&path).with_context(|| {
if configurations.is_empty() {
format!(
"Failed to load configuration `{path}`",
path = path.display()
)
} else {View on GitHub (pinned to 26f38c119c)
Solutions
- Inspect the printed chain and remove the `extends` entry that closes the loop
- Move shared settings into a single base config that all others extend (a DAG, not cycles)
- Resolve symlink loops between config files, then re-run Ruff
Example fix
# before (base.toml extends a.toml, a.toml extends base.toml) # a.toml [extends] path = "base.toml" # after: only the child extends the base # a.toml — remove the extends block referencing base.toml's ancestor chain [lint] select = ["E", "F"]
Defensive patterns
Strategy: validation
Validate before calling
import tomllib
from pathlib import Path
def check_no_cycles(path: Path, seen=None) -> None:
seen = seen or set()
p = path.resolve()
if p in seen:
raise SystemExit(f"Circular configuration detected at {p}")
cfg = tomllib.loads(p.read_text())
seen.add(p)
ext = cfg.get("extends")
if isinstance(ext, str):
check_no_cycles((p.parent / ext).resolve(), seen) Try / catch
import subprocess
p = subprocess.run(["ruff", "check", "."], capture_output=True, text=True)
if p.returncode != 0 and "Circular configuration detected" in p.stderr:
fix_extends_chain(); retry() Prevention
- Keep the extends graph acyclic: shared settings go in one base config that children extend
- Never let a base config extend a config that (transitively) extends it
- Check for symlink loops when configs live in linked directories
- Run `ruff check` locally after editing any extends entry
When it happens
Trigger: Configuration A extends B and B (directly or transitively) extends A; a config file extends itself; symlinked or normalized paths (`fs::normalize_path`) that loop back to an ancestor config.
Common situations: Shared team configs that extend each other for convenience; monorepo setups where a base config accidentally re-extends a child config; symlink loops between config directories.
Related errors
- `python_extension` is not expected to be combined with the i
- No files found under the given path
- Working directory does not exist
- Expected {}
- Unrecognized language: `{s}`. Expected one of `python`, `pyi
AI-assisted analysis of astral-sh/ruff@26f38c119c (2026-09-05).
Data as JSON: /api/errors/3225537946e267d7.
Report an issue: GitHub.