tinyhumansai/openhuman · critical

Config path is a directory, not a file: {}

Error message

Config path is a directory, not a file: {}

What it means

During config load (`load_or_init` read branch), a directory at the config path is a bad-install/corruption signal, not a transient read failure — the loader fails fast with this distinct wording before attempting any read. The comment is explicit about why the wording matters: on Windows, `read_to_string` on a directory returns the same `Access is denied. (os error 5)` shape as a real ACL denial, which the observability classifier would otherwise demote as an expected user-state config-read failure; distinct wording keeps it paging (#3962, Codex P2).

Source

Thrown at src/openhuman/config/schema/load/impl_load.rs:373

                                        error = %e,
                                        "[config] failed to auto-fix config file permissions to 600",
                                    );
                                }
                            }
                        }
                    }
                }
            }

            // A directory (or other non-regular file) at the config path is a
            // bad-install / corruption signal, not a transient read failure. On
            // Windows `read_to_string` of a directory returns the same
            // `Access is denied. (os error 5)` shape as a real ACL denial, which
            // the observability classifier would otherwise demote. Fail fast with
            // distinct wording so it keeps paging instead of being suppressed as
            // an expected user-state config-read failure (#3962, Codex P2).
            if config_path.is_dir() {
                anyhow::bail!(
                    "Config path is a directory, not a file: {}",
                    config_path.display()
                );
            }

            // Use the recovery-aware read path. If the file cannot be read
            // (e.g. non-UTF-8 bytes), the corrupted file is renamed to
            // `.corrupted.<timestamp>` and backup/defaults are attempted,
            // with rate-limited error logging (#5167).
            let (contents, read_was_recovered) =
                read_config_with_recovery_or_default(&config_path).await?;

            // When `read_config_with_recovery_or_default` returned an empty
            // string (both primary and backup were unreadable), skip the TOML
            // parse and use `Config::default()` directly.  An empty TOML would
            // otherwise parse successfully with serde defaults (all fields at
            // their `Option::None` / `vec![]` / `false` values) instead of
            // the richer `Default` impl (issue #5167).

View on GitHub (pinned to 7491200858)

Solutions

  1. Inspect and remove the directory at the reported path (its contents may explain how it got there or hold a lost config), then let the core recreate a default `config.toml` or restore the file from backup.
  2. Restore config.toml from your backup or a `*.corrupted.*`/defaults copy if the directory replaced a real file.
  3. Fix the provisioning script/installer that created a directory at a file path.

Example fix

# before — provisioning created a directory at the config path
$ ls -la ~/.openhuman/config.toml
drwxr-xr-x 2 user user 4096 config.toml/

# after
$ mv ~/.openhuman/config.toml ~/config.toml.dir-bad
# core now creates a fresh config.toml, or restore yours from backup
Defensive patterns

Strategy: validation

Validate before calling

let cfg = home.join(".openhuman").join("config.toml");
if cfg.is_dir() {
    // bad install — quarantine the directory so the core can recreate a default file
    std::fs::rename(&cfg, cfg.with_extension("toml.dir-quarantined"))?;
}

Type guard

fn config_path_is_loadable(p: &std::path::Path) -> bool {
    !p.exists() || p.is_file()
}

Prevention

When it happens

Trigger: Something created a directory where `config.toml` should be — a mistaken `mkdir` in provisioning, an installer/restore bug, or a sync tool that materialized the path as a folder. `config_path.is_dir()` then trips before the recovery-aware read path runs.

Common situations: Failed or partially-restored installs; provisioning scripts that pre-create the config path as a directory; cloud-sync folders turning the file into a folder; misconfigured HOME resolving somewhere unexpected.

Related errors


AI-assisted analysis of tinyhumansai/openhuman@7491200858 (2026-08-17). Data as JSON: /api/errors/2c11db66fe6a6835. Report an issue: GitHub.