NousResearch/hermes-agent · critical
${isPosix ? 'install.sh --manifest' : 'install.ps1 -Manifest
Error message
${isPosix ? 'install.sh --manifest' : 'install.ps1 -Manifest'} failed: exit ${result.code}\n${result.stderr || result.stdout} What it means
RuntimeError from load_jobs() (cron/jobs.py:1120): jobs.json parsed successfully as JSON, but the top-level value is neither an object (expected {'jobs': [...]}) nor a list (auto-repairable) — it is a string, number, bool, or null. Without this guard, data.get('jobs') would raise AttributeError and take down the cron subsystem; instead it fails with a precise type name.
Source
Thrown at apps/desktop/electron/bootstrap-runner.ts:707
return args
}
async function fetchManifest({ scriptPath, installerKind, emit, hermesHome, activeRoot, installStamp, pinCommit }) {
const isPosix = installerKind === 'posix'
const args = isPosix
? ['--manifest', ...buildPosixPinArgs({ installStamp, activeRoot, hermesHome, pinCommit })]
: ['-Manifest', ...buildPinArgs(installStamp, { pinCommit })]
const result = await (isPosix ? spawnBash : spawnPowerShell)(scriptPath, args, {
emit,
stageName: '__manifest__',
hermesHome
})
if (result.code !== 0) {
throw new Error(
`${isPosix ? 'install.sh --manifest' : 'install.ps1 -Manifest'} failed: exit ${result.code}\n${result.stderr || result.stdout}`
)
}
// The manifest is the LAST JSON line on stdout (install.ps1 may print
// banner / info lines first depending on Console.OutputEncoding effects).
// Find the last line that parses as JSON with a `stages` field.
const lines = result.stdout.split(/\r?\n/).filter(Boolean)
for (let i = lines.length - 1; i >= 0; i--) {
try {
const parsed = JSON.parse(lines[i])
if (parsed && Array.isArray(parsed.stages)) {
return parsed
}
} catch {
void 0View on GitHub (pinned to c896c09c42)
Solutions
- Look at the reported type name in the message to confirm what's actually in the file.
- Back up the file, then replace its contents with {"jobs": []}.
- Recreate needed jobs with `hermes cron add` (or restore from a backup of the old JSON).
- Ensure only Hermes writes jobs.json — no shell redirects or editors target it.
Example fix
# before: ~/.hermes/cron/jobs.json contains null
echo '{"jobs": []}' > ~/.hermes/cron/jobs.json # after
hermes cron list Defensive patterns
Strategy: validation
Validate before calling
import json
from pathlib import Path
from hermes_constants import get_hermes_home
def jobs_payload_ok() -> bool:
f = get_hermes_home() / "cron" / "jobs.json"
if not f.exists():
return True
data = json.loads(f.read_text())
return isinstance(data, (dict, list)) and not isinstance(data, (str, bytes))
# a bare string/number/null top level will fail here before Hermes raises Type guard
def is_jobs_payload(data: object) -> bool:
"""True when data is a shape load_jobs() accepts or auto-repairs."""
return isinstance(data, dict) or isinstance(data, list) Try / catch
try:
jobs = list_jobs()
except RuntimeError as e:
if "expected {'jobs": " in str(e) or "got " in str(e) and "corrupted" in str(e):
f.write_text('{"jobs": []}') # after backing up; then reload
jobs = list_jobs() Prevention
- Only Hermes writes jobs.json — no shell redirects, editors, or monitoring scripts targeting it.
- If you must inspect it, read-only via `hermes cron list` or python -m json.tool.
When it happens
Trigger: jobs.json contains e.g. "ok", 42, true, or null at top level — usually a script or human overwriting the file with something that isn't a job store at all.
Common situations: A user echoed a status string into jobs.json by mistake; a monitoring script truncated the file to empty content that parsed as null; an editor auto-saved a scratch note into the wrong file.
Related errors
- Cannot resolve ${installScriptName()}: no SOURCE_REPO_ROOT a
- Hermes backend did not become ready: ${detail}
- Preview mode — launching is disabled.
- no install root
- ${isPosix ? 'install.sh --manifest' : 'install.ps1 -Manifest
AI-assisted analysis of NousResearch/hermes-agent@c896c09c42 (2026-08-14).
Data as JSON: /api/errors/55bdce89b631bbea.
Report an issue: GitHub.