agentsclimarketplace

Hermes cron

Skill paarths-collab/hstack/skills/hermes-cron

Add, list, and manage scheduled tasks (cron) for a self-hosted Hermes agent over SSH, including the recommended reliability defaults (nightly gateway restart, nightly backup, weekly session prune). Works from any AI agent — Claude Code, Codex, Cursor, Gemini CLI, or Hermes itself.From its SKILL.md

Install
npx -y skills add paarths-collab/hstack --skill hermes-cron

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

One thing to look at

  • 13 stars13 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

SKILL.md

12.2 KB, as published. Nobody here has run it

/hermes-cron — scheduled tasks (SSH-first)

You are the engineer wiring scheduled tasks on a self-hosted Hermes agent. The agent (Hermes, Codex, Claude Code, Cursor, Gemini CLI — whichever ran you) is talking to a remote VPS over SSH as root. Every hermes cron … command in this skill runs on the VPS, not on the local machine.

Cron jobs run the agent on a timer and deliver results to the configured home channel. Schedules accept one-shot durations (30m, 2h), intervals (every 30m, every 2h, every 1d), classic cron expressions (0 9 * * 1-5), or ISO timestamps.

Do everything autonomously; stop only for things a machine cannot do (mint a token, scan a QR, send the first hello). For cron, that means: confirm the natural-language prompt + schedule with the user once, then write everything.

Honest picture (verified 2026-06): Hermes cron sessions run in a fresh session each firing — they cannot recursively create more crons, and they cannot read prior session memory unless you store it explicitly. Scripts live in ~/.hermes/scripts/. Self-hosted Hermes also has a known memory-leak / OOM (#25315) which is exactly why the nightly gateway-restart default exists.


Before you start — gather (ask once, in one batch)

VariableWhat it isHow to get it
$VPS_IPIP or hostname of the VPS running HermesTheir hosting dashboard
$VPS_USERSSH user, usually rootTheir hosting dashboard
$CRON_SCHEDULENatural-language or cron-expr schedule (e.g. every 1d, 0 9 * * 1-5, 30m)Ask the user
$CRON_PROMPTWhat the agent should do each firing, in plain EnglishAsk the user
$CRON_TZ(optional) IANA timezone for human-readable schedules; defaults to UTCAsk the user if "8am" is mentioned

Confirm SSH access before doing anything:

ssh -o StrictHostKeyChecking=accept-new -o BatchMode=yes \
    "$VPS_USER@$VPS_IP" "echo ok" 2>&1 | grep -q '^ok$' \
  || { echo "ABORT: SSH to $VPS_USER@$VPS_IP failed. Run /setup-ssh-keys first."; exit 1; }

Step 1 — verify Hermes is reachable on the VPS

ssh "$VPS_USER@$VPS_IP" '
  set -e
  if command -v hermes >/dev/null 2>&1; then
    hermes --version
  elif docker ps --format "{{.Names}}" | grep -q hermes; then
    AGENT=$(docker ps --filter name=hermes --format "{{.Names}}" | head -1)
    docker exec "$AGENT" hermes --version
  else
    echo "FAIL: hermes not found on host or in container"; exit 1
  fi
' || { echo "ABORT: Hermes is not installed/running. Run /hermes-install first."; exit 1; }

Expected: a version string like 0.15.x or 0.17.x.

If you got root@<hex-string> from hostname, you are inside a container — type exit first. Crons added inside a container disappear when the container restarts.


Step 2 — idempotency check (skip if already wired)

Hermes does not dedupe crons by prompt — if you run this twice you get two crons firing in parallel. Always grep the existing list first.

# Slugify the prompt for matching (first 40 chars, lowercase, alnum only)
SLUG=$(printf '%s' "$CRON_PROMPT" | tr '[:upper:]' '[:lower:]' \
       | tr -c 'a-z0-9' ' ' | tr -s ' ' | cut -c1-40)
ALREADY=$(ssh "$VPS_USER@$VPS_IP" "hermes cron list 2>/dev/null" \
          | grep -cFi "$SLUG" || echo 0)
if [ "$ALREADY" -gt 0 ] && [ "${FORCE:-0}" != "1" ]; then
  echo "A cron matching this prompt is already registered. Set FORCE=1 to add anyway."
  ssh "$VPS_USER@$VPS_IP" "hermes cron list"
  exit 0
fi

Step 3 — DRY RUN preview (always show before writing)

cat <<EOF
DRY RUN — the following will happen on $VPS_USER@$VPS_IP:
  1. hermes cron create "$CRON_SCHEDULE" "$CRON_PROMPT"
     (timezone: ${CRON_TZ:-UTC}; human-readable schedules are converted to UTC server-side)
  2. hermes cron list (to capture the new cron ID for rollback)
  3. Verify the new cron appears in 'hermes cron list'
  4. Verify the gateway is still healthy after the write

No secrets are written by this skill. The prompt above WILL be visible in
'hermes cron list' output — don't embed tokens in it.
EOF

If the user confirms (or AUTO_APPROVE=1), proceed to Step 4.


Step 4 — create the cron (no echo, no logging of internals)

ssh "$VPS_USER@$VPS_IP" "hermes cron create '$CRON_SCHEDULE' '$CRON_PROMPT'" \
  || { echo "FAIL: hermes cron create returned non-zero. Rolling back."; rollback; exit 1; }

Capture the new cron ID so rollback can target it exactly:

NEW_CRON_ID=$(ssh "$VPS_USER@$VPS_IP" "hermes cron list 2>/dev/null" \
              | grep -F "$CRON_PROMPT" | head -1 | awk '{print $1}')
[ -n "$NEW_CRON_ID" ] || { echo "FAIL: could not locate new cron ID. Rolling back."; rollback; exit 1; }
echo "OK: created cron $NEW_CRON_ID"

If your Hermes build emits the ID on stdout from cron create, capture it directly:

NEW_CRON_ID=$(ssh "$VPS_USER@$VPS_IP" "hermes cron create '$CRON_SCHEDULE' '$CRON_PROMPT'" \
              | awk '/^cron/{print $2; exit}')

Step 5 — install the three recommended reliability defaults (idempotent)

These are safe to run on every invocation — each one self-skips if already present. Skip this step entirely with SKIP_DEFAULTS=1.

if [ "${SKIP_DEFAULTS:-0}" != "1" ]; then
  ssh "$VPS_USER@$VPS_IP" 'bash -s' <<'REMOTE'
    HERMES="$HOME/.local/bin/hermes"
    [ -x "$HERMES" ] || HERMES="hermes"

    install_if_absent() {
      local schedule="$1" prompt="$2" slug="$3"
      if "$HERMES" cron list 2>/dev/null | grep -qFi "$slug"; then
        echo "skip: '$slug' already present"
      else
        "$HERMES" cron create "$schedule" "$prompt" \
          && echo "added: $slug" \
          || echo "warn: could not add '$slug'"
      fi
    }

    # 1) Nightly gateway restart — mitigates OOM (#25315) before it crashes.
    install_if_absent "every 1d" \
      "Run hermes gateway stop then hermes gateway run --daemon to recycle memory" \
      "gateway restart"

    # 2) Nightly backup — push ~/.hermes to a private backup repo.
    install_if_absent "every 1d" \
      "Push changes in ~/.hermes to the backup GitHub repo" \
      "backup github"

    # 3) Weekly session prune — ~/.hermes bloats to multi-GB with ~3000 session files.
    install_if_absent "every 7d" \
      "Prune session files in ~/.hermes older than 14 days" \
      "session prune"
REMOTE
fi

Step 6 — reload the gateway (stop + run, NOT restart)

Cron writes don't strictly require a gateway reload, but the gateway must be healthy for the cron daemon inside Hermes to actually fire. Use stop + run, never gateway restart.

ssh "$VPS_USER@$VPS_IP" "hermes gateway stop || true"
sleep 2
ssh "$VPS_USER@$VPS_IP" "hermes gateway run --daemon"
sleep 5

Step 7 — verify the cron appears in hermes cron list (poll up to 30s)

FOUND=0
for i in $(seq 1 6); do
  if ssh "$VPS_USER@$VPS_IP" "hermes cron list 2>&1" \
       | grep -qF "$CRON_PROMPT"; then
    FOUND=1
    echo "OK: cron is listed."
    break
  fi
  sleep 5
done
[ "$FOUND" = "1" ] || { echo "FAIL: cron not in list after 30s. Rolling back."; rollback; exit 1; }

Also confirm the gateway came back up and there are no fatal errors in the last 100 log lines (cron daemon failures show up here):

ssh "$VPS_USER@$VPS_IP" "hermes logs 2>&1 | tail -100" \
  | grep -qiE "cron.*(loaded|scheduled|started)" \
  || echo "WARN: did not see cron scheduler confirmation in logs — verify manually."

Step 8 — live trigger smoke test (run the cron once, immediately)

For non-destructive prompts, run the new cron once to confirm the agent actually picks it up. Skip with SKIP_SMOKE=1 if the prompt has side effects (sends email, posts to a channel, etc.).

if [ "${SKIP_SMOKE:-0}" != "1" ]; then
  HTTP_OK=$(ssh "$VPS_USER@$VPS_IP" "hermes cron run '$NEW_CRON_ID' 2>&1" \
            | grep -ciE "queued|started|ok|running" || echo 0)
  case "$HTTP_OK" in
    0)  echo "WARN: cron run did not report queued/started. Inspect 'hermes logs'." ;;
    *)  echo "OK: cron $NEW_CRON_ID fired manually." ;;
  esac
fi

A successful manual run proves: (a) the schedule parsed, (b) the prompt is valid, and (c) the home channel delivery works. If it fails, the scheduled firing will fail too.


Rollback (auto-runs on any failure above)

rollback() {
  if [ -n "${NEW_CRON_ID:-}" ]; then
    ssh "$VPS_USER@$VPS_IP" "hermes cron remove '$NEW_CRON_ID' 2>/dev/null || true"
  fi
  ssh "$VPS_USER@$VPS_IP" "hermes gateway stop; sleep 2; hermes gateway run --daemon"
  echo "Rolled back. Cron $NEW_CRON_ID (if created) is no longer scheduled."
}

Managing existing crons

All hermes cron subcommands run identically over SSH:

ssh "$VPS_USER@$VPS_IP" "hermes cron list"
ssh "$VPS_USER@$VPS_IP" "hermes cron pause '<id>'"
ssh "$VPS_USER@$VPS_IP" "hermes cron resume '<id>'"
ssh "$VPS_USER@$VPS_IP" "hermes cron run '<id>'"
ssh "$VPS_USER@$VPS_IP" "hermes cron remove '<id>'"

In-chat shortcut for end users: tell the agent in conversation "every morning at 8am, summarize my unread emails" — Hermes writes the cron itself and converts to UTC. This skill is for the SSH-first, idempotent, agent-driven path.


Pitfalls

#PitfallWhy it bitesPrevention
1Cron-run sessions trying to create more cronsEach firing is a fresh session and cannot recurseGenerate the cron up-front; have the firing call a script in ~/.hermes/scripts/
2Human-readable schedule with no timezoneDefaults to UTC — "8am" fires at the wrong hourSpecify $CRON_TZ or convert before passing
3Duplicate crons from re-running this skillcron create does NOT dedupe by promptIdempotency check in Step 2
4gateway restart to pick up envRestart does NOT reliably re-read .envUse stop + run
5echo >> .env instead of hermes config setCan merge onto a prior line without trailing newlineAlways hermes config set (this skill writes no secrets, but the rule still applies if your prompt loads one)
6Secrets in config.yaml or in the cron promptcron list is plaintext, world-readableReference env vars only; secrets stay in ~/.hermes/.env, chmod 600
7sed with / delimiter on tokensTokens contain /+=; sed breaksAlways use | delimiter
8Adding the cron inside a containerDisappears on container restartwhoami; hostname check first; exit the container if you see a hex name
9~/.hermes ballooning to multi-GB~3000 session files accumulate over monthsInstall the weekly session-prune default (Step 5)
10Gateway OOM mid-week (#25315)Known memory leak in self-hosted HermesInstall the nightly gateway-restart default (Step 5)
11No backup of ~/.hermesHermes does not auto-backup; one bad disk = total loss of memory, crons, configInstall the nightly backup default (Step 5)
12Skipping the verify stepSilent success — cron written but scheduler not loadedAlways poll cron list + grep logs

Definition of done

  • SSH to $VPS_USER@$VPS_IP confirmed working
  • Hermes version verified on VPS (0.15.x or 0.17.x), confirmed running on host not container
  • Idempotency check passed (or FORCE=1 overrode)
  • Dry-run shown to user; user approved (or AUTO_APPROVE=1)
  • New cron created via hermes cron create; ID captured for rollback
  • Three reliability defaults installed or skipped via SKIP_DEFAULTS=1
  • Gateway reloaded with stop + run (NOT restart)
  • hermes cron list shows the new cron within 30s
  • Smoke test: manual hermes cron run <id> reported queued/started (or SKIP_SMOKE=1)
  • No secrets in the cron prompt, in config.yaml, or echoed in chat
  • Rollback function defined and proven (re-run with FORCE=1 rewires cleanly)

See reference/TROUBLESHOOTING.md for gateway, memory-leak, and cron failure modes.

Keep looking

Skills are one crate of 628,853. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.