JuliusBrussee/caveman · critical
githubapp: private key is not valid PEM
Error message
githubapp: private key is not valid PEM
What it means
parseRSAPrivateKey only accepts keys wrapped in a valid PEM block (PKCS#1 or PKCS#8). pem.Decode returned nil, meaning the input bytes contain no '-----BEGIN ...-----' armor at all. New() calls this when constructing a GitHub App client, so the app credentials could not be parsed.
Solutions
- Re-download the GitHub App private key (.pem) and use the full file including the '-----BEGIN RSA PRIVATE KEY-----' and '-----END RSA PRIVATE KEY-----' lines.
- Load with os.ReadFile instead of pasting through env/config, or base64 the whole file and decode before use.
- Verify before calling New() that the value is non-empty and starts with '-----BEGIN'.
- If you have only DER bytes, wrap them in PEM (openssl rsa -inform DER -in key.der -out key.pem or pem.EncodeToMemory).
Example fix
// before
key := os.Getenv("GH_APP_KEY") // raw base64 body, no PEM armor
client := githubapp.New(key)
// after
pemBytes, _ := os.ReadFile("app.private-key.pem") // full PEM with BEGIN/END lines
client := githubapp.New(string(pemBytes)) Defensive patterns
Strategy: validation
Validate before calling
func looksLikePEM(b []byte) bool {
return bytes.HasPrefix(bytes.TrimSpace(b), []byte("-----BEGIN"))
}
if !looksLikePEM([]byte(key)) {
return errors.New("GitHub App key is not PEM-encoded; missing -----BEGIN armor")
} Type guard
func isPEMPrivateKey(s string) bool {
block, _ := pem.Decode([]byte(s))
return block != nil && strings.Contains(block.Type, "PRIVATE KEY")
} Try / catch
client, err := githubapp.New(rawKey)
if err != nil {
if strings.Contains(err.Error(), "not valid PEM") {
return fmt.Errorf("GH_APP_KEY missing PEM armor: %w", err)
}
return err
} Prevention
- Store the key as a full PEM file and read it with os.ReadFile, never paste fragments.
- Use base64 on the whole file and decode before use when passing through env/config.
- Add a startup assertion that the key starts with '-----BEGIN'.
- Keep BEGIN/END lines intact across shell quoting and secret-manager round-trips.
When it happens
Trigger: Calling New() with a private key that is raw base64 DER bytes without PEM armor, an empty/whitespace value, a key with stripped BEGIN/END lines, or a file path/placeholder instead of key contents.
Common situations: Env var contents lost their header lines through shell/secret-manager quoting; copying only the base64 body from the downloaded .pem; GITHUB_APP_KEY empty because the env var was never set; passing a filename instead of the file contents.
Understand the failure class
Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.
Related errors
- githubapp: app id is required
- githubapp: private key is neither PKCS#1 nor PKCS#8 RSA
- githubapp: private key is not RSA
- : MCP ownership journal failed; native config may already…
- MCP transaction failed and rolled back
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/03201b539879d6f8.
Report an issue: GitHub.
Appendix: source
Thrown at shared/platform/githubapp/githubapp.go:400
resp, err := a.httpClient.Do(req)
if err != nil {
return 0, nil, fmt.Errorf("githubapp: request failed: %w", err)
}
defer resp.Body.Close()
raw, err := io.ReadAll(io.LimitReader(resp.Body, 4<<20))
if err != nil {
return resp.StatusCode, nil, fmt.Errorf("githubapp: read response: %w", err)
}
return resp.StatusCode, raw, nil
}
// parseRSAPrivateKey accepts a PKCS#1 ("RSA PRIVATE KEY") or PKCS#8
// ("PRIVATE KEY") PEM — GitHub Apps download PKCS#1, but Cloud KMS / openssl
// conversions emit PKCS#8, so we accept both.
func parseRSAPrivateKey(pemBytes []byte) (*rsa.PrivateKey, error) {
block, _ := pem.Decode(pemBytes)
if block == nil {
return nil, fmt.Errorf("githubapp: private key is not valid PEM")
}
if key, err := x509.ParsePKCS1PrivateKey(block.Bytes); err == nil {
return key, nil
}
parsed, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("githubapp: private key is neither PKCS#1 nor PKCS#8 RSA: %w", err)
}
key, ok := parsed.(*rsa.PrivateKey)
if !ok {
return nil, fmt.Errorf("githubapp: private key is not RSA")
}
return key, nil
}
// snippet trims an error body so we never echo a large/secret-bearing response.
func snippet(b []byte) string {
const max = 256View on GitHub (pinned to 3ee70a1026)