{"record":{"id":"8f251569e308abec","repo":"zeroclaw-labs/zeroclaw","slug":"git-channel-reading-private-key-path-path-fai","errorCode":null,"errorMessage":"git channel: reading private_key_path `{path}` failed: {e}","messagePattern":"git channel: reading private_key_path `(.+?)` failed: (.+?)","errorType":"validation","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-channels/src/git/channel.rs","lineNumber":53,"sourceCode":"/// with configs that predate the inline field.\nfn resolve_github_private_key(cfg: &GitConfig) -> anyhow::Result<Option<String>> {\n    if cfg.private_key.is_some() {\n        return Ok(cfg.private_key.clone());\n    }\n    let Some(path) = cfg\n        .private_key_path\n        .as_deref()\n        .map(str::trim)\n        .filter(|p| !p.is_empty())\n    else {\n        return Ok(None);\n    };\n    match std::fs::read_to_string(path) {\n        Ok(pem) => {\n            warn_on_loose_permissions(path);\n            Ok(Some(pem))\n        }\n        Err(e) => anyhow::bail!(\"git channel: reading private_key_path `{path}` failed: {e}\"),\n    }\n}\n\n/// The private key is a long-lived credential: group/other access on the\n/// key file is operator error worth surfacing, but not worth refusing to\n/// start over.\n#[cfg(unix)]\nfn warn_on_loose_permissions(path: &str) {\n    use std::os::unix::fs::MetadataExt;\n    let Ok(meta) = std::fs::metadata(path) else {\n        return;\n    };\n    if meta.mode() & 0o077 != 0 {\n        ::zeroclaw_log::record!(\n            WARN,\n            ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)\n                .with_attrs(::serde_json::json!({\"path\": path})),\n            \"GitHub App private key is readable by group/other; chmod 600 recommended\"","sourceCodeStart":35,"sourceCodeEnd":71,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-channels/src/git/channel.rs#L35-L71","documentation":"The GitHub provider for the git channel authenticates as a GitHub App using a PEM private key loaded from channels.git.<alias>.private_key_path. When std::fs::read_to_string fails, build_provider bails with the OS error inline. Loose file permissions only produce a warning; an unreadable file is fatal.","triggerScenarios":"private_key_path points at a nonexistent file, the service user lacks read permission, or a relative path resolves against an unexpected working directory.","commonSituations":"Deploying with a relative key path that breaks when the cwd changes; key file owned by root in a container; secret volume not mounted; typo in the path.","solutions":["Use an absolute path in private_key_path","Verify the file exists and is readable by the service user: sudo -u <user> cat <path>","Fix ownership/permissions for the service account (chown, chmod 600)","In containers, confirm the secret mount actually provides the file at that path"],"exampleFix":"# before\n[channels.git.mygh]\nprovider = \"github\"\nprivate_key_path = \"keys/app.pem\" # relative: breaks when cwd differs\n\n# after\n[channels.git.mygh]\nprovider = \"github\"\nprivate_key_path = \"/etc/zeroclaw/secrets/app.pem\" # absolute, mounted, mode 600","handlingStrategy":"validation","validationCode":"// Rust — fail fast with a clearer diagnosis before building the provider\nlet path = std::path::Path::new(&cfg.private_key_path);\nif !path.is_absolute() {\n    anyhow::bail!(\"private_key_path should be absolute: {}\", path.display());\n}\nstd::fs::read_to_string(path).with_context(|| {\n    format!(\"private key unreadable at {} (running as the service user?)\", path.display())\n})?;","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always use absolute paths for credential files","Run a preflight read of every configured credential file at deploy time","Mount secrets read-only at fixed paths and verify the mount before start"],"tags":["git","github-app","private-key","filesystem","configuration"],"backgroundTag":"private-key-unreadable","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}