{"record":{"id":"2c11db66fe6a6835","repo":"tinyhumansai/openhuman","slug":"config-path-is-a-directory-not-a-file","errorCode":null,"errorMessage":"Config path is a directory, not a file: {}","messagePattern":"Config path is a directory, not a file: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"src/openhuman/config/schema/load/impl_load.rs","lineNumber":373,"sourceCode":"                                        error = %e,\n                                        \"[config] failed to auto-fix config file permissions to 600\",\n                                    );\n                                }\n                            }\n                        }\n                    }\n                }\n            }\n\n            // A directory (or other non-regular file) at the config path is a\n            // bad-install / corruption signal, not a transient read failure. On\n            // Windows `read_to_string` of a directory returns the same\n            // `Access is denied. (os error 5)` shape as a real ACL denial, which\n            // the observability classifier would otherwise demote. Fail fast with\n            // distinct wording so it keeps paging instead of being suppressed as\n            // an expected user-state config-read failure (#3962, Codex P2).\n            if config_path.is_dir() {\n                anyhow::bail!(\n                    \"Config path is a directory, not a file: {}\",\n                    config_path.display()\n                );\n            }\n\n            // Use the recovery-aware read path. If the file cannot be read\n            // (e.g. non-UTF-8 bytes), the corrupted file is renamed to\n            // `.corrupted.<timestamp>` and backup/defaults are attempted,\n            // with rate-limited error logging (#5167).\n            let (contents, read_was_recovered) =\n                read_config_with_recovery_or_default(&config_path).await?;\n\n            // When `read_config_with_recovery_or_default` returned an empty\n            // string (both primary and backup were unreadable), skip the TOML\n            // parse and use `Config::default()` directly.  An empty TOML would\n            // otherwise parse successfully with serde defaults (all fields at\n            // their `Option::None` / `vec![]` / `false` values) instead of\n            // the richer `Default` impl (issue #5167).","sourceCodeStart":355,"sourceCodeEnd":391,"githubUrl":"https://github.com/tinyhumansai/openhuman/blob/749120085864ce16e0f273c7b86fac7740b39c5b/src/openhuman/config/schema/load/impl_load.rs#L355-L391","documentation":"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).","triggerScenarios":"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.","commonSituations":"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.","solutions":["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.","Restore config.toml from your backup or a `*.corrupted.*`/defaults copy if the directory replaced a real file.","Fix the provisioning script/installer that created a directory at a file path."],"exampleFix":"# before — provisioning created a directory at the config path\n$ ls -la ~/.openhuman/config.toml\ndrwxr-xr-x 2 user user 4096 config.toml/\n\n# after\n$ mv ~/.openhuman/config.toml ~/config.toml.dir-bad\n# core now creates a fresh config.toml, or restore yours from backup","handlingStrategy":"validation","validationCode":"let cfg = home.join(\".openhuman\").join(\"config.toml\");\nif cfg.is_dir() {\n    // bad install — quarantine the directory so the core can recreate a default file\n    std::fs::rename(&cfg, cfg.with_extension(\"toml.dir-quarantined\"))?;\n}","typeGuard":"fn config_path_is_loadable(p: &std::path::Path) -> bool {\n    !p.exists() || p.is_file()\n}","tryCatchPattern":null,"preventionTips":["Provisioning scripts must create the config path as a file, never mkdir it","Health-check installs: config.toml must be a regular file","Restore configs from backups that preserve the file type"],"tags":["config","boot","install-corruption","filesystem","observability"],"backgroundTag":"invalid-config-path","analyzedSha":"749120085864ce16e0f273c7b86fac7740b39c5b","analyzedAt":"2026-08-17T21:21:45.363Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}