hermes - 💡(How to fix) Fix feat(cron): support prompt_file field for loading cron job prompts from disk [1 pull requests]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Cron jobs in ~/.hermes/cron/jobs.json currently embed their LLM prompt as a JSON string field on the job object. As prompt count and length grow this becomes painful:

  • Multi-paragraph prompts live as escaped one-liners with no syntax highlighting, soft-wrap, or markdown rendering.
  • git diff cron/jobs.json shows prompt edits and runtime field churn (last_run_at, next_run_at, last_status, repeat.completed) in the same blob, making prompt iteration unreadable.
  • Per-job context — schedule, script binding, delivery target, prompt — is fragmented across jobs.json (prompt + admin) and ~/.hermes/scripts/ (the script), with no folder where they cluster.

A small additive feature — letting a job point at a file for its prompt — would resolve the bulk of this without changing any other behavior.

Error Message

  • Return clear error text on missing / unreadable file so the LLM call can report the problem instead of silently noping.

Root Cause

Cron jobs in ~/.hermes/cron/jobs.json currently embed their LLM prompt as a JSON string field on the job object. As prompt count and length grow this becomes painful:

  • Multi-paragraph prompts live as escaped one-liners with no syntax highlighting, soft-wrap, or markdown rendering.
  • git diff cron/jobs.json shows prompt edits and runtime field churn (last_run_at, next_run_at, last_status, repeat.completed) in the same blob, making prompt iteration unreadable.
  • Per-job context — schedule, script binding, delivery target, prompt — is fragmented across jobs.json (prompt + admin) and ~/.hermes/scripts/ (the script), with no folder where they cluster.

A small additive feature — letting a job point at a file for its prompt — would resolve the bulk of this without changing any other behavior.

Fix Action

Fixed

Code Example

def _build_job_prompt(job: dict, prerun_script: Optional[tuple] = None) -> str:
    prompt = str(job.get("prompt") or "")
    ...

---

def _build_job_prompt(job: dict, ...) -> str:
    prompt_file = job.get("prompt_file")
    if prompt_file:
        prompt = _load_prompt_file(prompt_file)
    else:
        prompt = str(job.get("prompt") or "")
    ...

---

~/.hermes/cron-jobs/
├── morning-kanban-dump/
│   ├── prompt.md
│   ├── run.sh         # symlinked from scripts/ or kept here pending #<scripts-dir-relaxation issue>
│   ├── schedule.cron
│   └── README.md
├── briefing-judge/
│   └── ...
RAW_BUFFERClick to expand / collapse

Summary

Cron jobs in ~/.hermes/cron/jobs.json currently embed their LLM prompt as a JSON string field on the job object. As prompt count and length grow this becomes painful:

  • Multi-paragraph prompts live as escaped one-liners with no syntax highlighting, soft-wrap, or markdown rendering.
  • git diff cron/jobs.json shows prompt edits and runtime field churn (last_run_at, next_run_at, last_status, repeat.completed) in the same blob, making prompt iteration unreadable.
  • Per-job context — schedule, script binding, delivery target, prompt — is fragmented across jobs.json (prompt + admin) and ~/.hermes/scripts/ (the script), with no folder where they cluster.

A small additive feature — letting a job point at a file for its prompt — would resolve the bulk of this without changing any other behavior.

Current code

cron/scheduler.py::_build_job_prompt reads the prompt unconditionally from job["prompt"]:

def _build_job_prompt(job: dict, prerun_script: Optional[tuple] = None) -> str:
    prompt = str(job.get("prompt") or "")
    ...

There is no prompt_file / prompt_path / @file: indirection. The only way to keep a prompt outside jobs.json today is to leave the field empty and have the script emit the entire prompt as stdout — which collapses the script-output / prompt distinction and forces every prompt to be templated by a shell script.

Proposed change

Add a prompt_file field on the job dict, parallel to the existing script field, with the same security guards:

def _build_job_prompt(job: dict, ...) -> str:
    prompt_file = job.get("prompt_file")
    if prompt_file:
        prompt = _load_prompt_file(prompt_file)
    else:
        prompt = str(job.get("prompt") or "")
    ...

_load_prompt_file would mirror _run_job_script's posture:

  • Resolve relative paths against a safe root (e.g. HERMES_HOME/cron-jobs/, or reuse HERMES_HOME/scripts/).
  • Reject paths that resolve outside that root (path-traversal guard).
  • Return clear error text on missing / unreadable file so the LLM call can report the problem instead of silently noping.

If both prompt and prompt_file are set, prompt_file wins (or raise — author's call). If neither is set, current behavior (empty prompt) holds.

Why it matters

Prompt engineering is iterative. JSON-embedded strings make iteration painful at exactly the moment you want to be fast:

  • Editing a 500-character prompt in a JSON string field invariably means re-escaping quotes and newlines.
  • Reviewing a prompt change in git diff requires reading through unrelated runtime-field churn on the same line of the diff (Hermes mutates last_run_at and similar fields every run).
  • Sharing a prompt with another person (or another agent) means extracting it from JSON; sharing a file is cat.

This is also load-bearing for letting users organize cron jobs as per-job folders — the conventional layout

~/.hermes/cron-jobs/
├── morning-kanban-dump/
│   ├── prompt.md
│   ├── run.sh         # symlinked from scripts/ or kept here pending #<scripts-dir-relaxation issue>
│   ├── schedule.cron
│   └── README.md
├── briefing-judge/
│   └── ...

— is only useful if prompt.md is what Hermes actually reads. Otherwise the folder is documentation that drifts from the real source of truth in jobs.json.

Suggested implementation order

  1. prompt_file field, resolved against HERMES_HOME/cron-jobs/ (or a configurable prompt_root). Same path-traversal guards as _run_job_script. ~30 lines of code + a test.
  2. Migration path. Optional: a hermes cron extract-prompts CLI that walks jobs.json, writes each non-empty prompt to cron-jobs/<job-name>/prompt.md, and rewrites the job to use prompt_file. Round-trips the existing file losslessly.
  3. Companion change (separate issue, optional): relax _run_job_script so symlinks within cron-jobs/ that point into scripts/ resolve cleanly — enables the full folder-per-job layout without forcing users to keep script bodies in scripts/.

(1) on its own is sufficient to unblock the per-job folder convention; (2) and (3) are quality-of-life follow-ups.

Environment

  • Hermes Agent — current main, post-2026-05-19 sync
  • macOS 25.4.0 (Darwin)
  • Python 3.11.14
  • 24 active cron jobs, 19 with a script field, ~2000 chars of prompt content in jobs.json

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

hermes - 💡(How to fix) Fix feat(cron): support prompt_file field for loading cron job prompts from disk [1 pull requests]