bytedance/deer-flow · error · GitHubAppAuthError
GITHUB_APP_PRIVATE_KEY_PATH points to nonexistent file: {p}
Error message
GITHUB_APP_PRIVATE_KEY_PATH points to nonexistent file: {p} What it means
GitHubAppAuthError raised by load_app_private_key() when GITHUB_APP_PRIVATE_KEY_PATH is set but the file does not exist at the expanded path. Path is expanduser()-ed, so '~' works; existence is checked before read_text().
Source
Thrown at backend/app/gateway/github/app_auth.py:113
def load_app_private_key() -> str:
"""Return the App's RSA private key as a PEM string.
Reads from ``GITHUB_APP_PRIVATE_KEY`` (inline PEM) if set, else from
the path in ``GITHUB_APP_PRIVATE_KEY_PATH``. Inline takes precedence
so operators can roll a key by setting an env var instead of moving
files around in production.
"""
inline = os.environ.get(_PRIVATE_KEY_ENV)
if inline and inline.strip():
return inline
path = os.environ.get(_PRIVATE_KEY_PATH_ENV)
if not path:
raise GitHubAppAuthError(f"Neither {_PRIVATE_KEY_ENV} nor {_PRIVATE_KEY_PATH_ENV} is set")
p = Path(path).expanduser()
if not p.exists():
raise GitHubAppAuthError(f"{_PRIVATE_KEY_PATH_ENV} points to nonexistent file: {p}")
return p.read_text(encoding="utf-8")
def mint_app_jwt(*, now: float | None = None) -> str:
"""Sign a short-lived JWT identifying this App to GitHub.
Args:
now: Optional override for ``time.time()`` — tests use this.
Returns:
Signed RS256 JWT suitable for ``Authorization: Bearer <jwt>``.
"""
issued_at = int(now if now is not None else time.time())
payload = {
# GitHub recommends iat 60s in the past to tolerate clock skew.
"iat": issued_at - 60,
"exp": issued_at + _APP_JWT_TTL_SECONDS,
# iss must be a string in current pyjwt; GitHub accepts theView on GitHub (pinned to 1dd6ba1acb)
Solutions
- Check the path from the process's point of view: `docker exec <gateway> ls -l "$GITHUB_APP_PRIVATE_KEY_PATH"`
- Fix the mount: compose volumes: - ./secrets/gh.pem:/run/secrets/gh.pem:ro and point _PATH at /run/secrets/gh.pem; in K8s verify the secret and volumeMount agree
- Strip stray whitespace/newlines from the env value (common with .env files edited on Windows)
- Remember ~ expands to the container user's home, not yours — prefer absolute paths in containers
Example fix
# before (compose) environment: GITHUB_APP_PRIVATE_KEY_PATH: ~/secrets/gh.pem # not mounted -> GitHubAppAuthError # after volumes: - ./secrets/gh.pem:/run/secrets/gh.pem:ro environment: GITHUB_APP_PRIVATE_KEY_PATH: /run/secrets/gh.pem
Defensive patterns
Strategy: validation
Validate before calling
import os, pathlib
p = pathlib.Path(os.environ['GITHUB_APP_PRIVATE_KEY_PATH']).expanduser()
if not p.is_file():
raise ConfigError(f'private key file missing: {p}')
key = p.read_text()
assert 'PRIVATE KEY' in key, 'file is not a PEM' Type guard
null
Try / catch
except GitHubAppAuthError as e:
if 'nonexistent file' in str(e):
fix_mount_and_restart() # mount check, then redeploy
raise Prevention
- Use absolute container paths for secrets, never ~ or relative paths
- Smoke-test secret mounts (`ls -l $PATH`) in the container after deploy
- Trim whitespace from env values in .env files
When it happens
Trigger: Calling load_app_private_key() with a _PATH value pointing to a missing file — secret not mounted, wrong mount path in compose/K8s, file only present on the host but the Gateway runs in a container, or a typo in the path.
Common situations: Docker bind-mount uses a relative path resolved against a different workdir; Kubernetes secret volume name mismatch; the pem was never copied to the server; container runs as a different user so ~ expands elsewhere; trailing whitespace/newline in the env var corrupting the path.
Related errors
- Neither GITHUB_APP_PRIVATE_KEY nor GITHUB_APP_PRIVATE_KEY_PA
- GITHUB_APP_ID is not set
- GITHUB_APP_ID={raw!r} is not an integer
- Failed to mint installation token (status={resp.status_code}
- Webhook signature verification not configured. Set {_SECRET_
AI-assisted analysis of bytedance/deer-flow@1dd6ba1acb (2026-08-14).
Data as JSON: /api/errors/5e30516031ba4406.
Report an issue: GitHub.