All-my-ai-needs hermes-cron-local-script-notify
Create lightweight Hermes cron jobs that offload work into a local pre-run script, avoid chat-context overhead, and send macOS notifications for success/failure.
git clone https://github.com/codingSamss/all-my-ai-needs
T=$(mktemp -d) && git clone --depth=1 https://github.com/codingSamss/all-my-ai-needs "$T" && mkdir -p ~/.claude/skills && cp -r "$T/platforms/hermes/skills/autonomous-ai-agents/hermes-cron-local-script-notify" ~/.claude/skills/codingsamss-all-my-ai-needs-hermes-cron-local-script-notify && rm -rf "$T"
platforms/hermes/skills/autonomous-ai-agents/hermes-cron-local-script-notify/SKILL.mdHermes cron jobs with local script + macOS notifications
Use this when the user wants a scheduled task that should be as lightweight and deterministic as possible, especially for local keepalive pings, one-shot Codex CLI calls, or simple background automations.
When to use
- User wants a cron task but does not want the future run to carry a lot of chat/tool context
- User wants the real work done by a local shell/Python script instead of a full agent workflow
- User wants macOS desktop notifications on success/failure
- User wants to keep the Hermes cron schedule, but make execution minimal
Key idea
Do the real work in a cron
script, and make the cron prompt trivial.
This keeps the autonomous run lighter because:
- the script executes locally before the cron prompt
- the prompt only needs to echo the script result
- attached
can be cleared (skills
) if not neededskills=[]
Recommended pattern
- Write a local script under
~/.hermes/scripts/ - Put the real logic there
- Use
cronjob(action='create' or 'update', script='...') - Keep the cron prompt tiny, e.g. "只把脚本 stdout 的最后一行原样输出"
- If minimizing overhead matters, clear skills with
skills=[]
Example: lightweight Codex keepalive on macOS
Why this approach
If the goal is just "poke Codex every few hours" or "refresh a 5h window", don't load a Codex skill and don't make the cron agent reason about how to call Codex. Instead, call Codex directly from a local script.
Important findings
can run outside a git repo withcodex exec--skip-git-repo-check- For simple chat-like one-shots,
is optional and usually unnecessarygit init - Add
if you want a lighter one-shot that does not persist session files--ephemeral - If you immediately
a newly created recurring job,cronjob(action='run')
may temporarily reflect the manual run rather than the next clean scheduled slot. If the user wants a clean schedule display, avoid immediate test-runs and test the script directly instead.next_run_at
Example script
Save as
~/.hermes/scripts/codex_keepalive_notify.py
#!/usr/bin/env python3 import shutil import subprocess import tempfile from pathlib import Path PROMPT = "只回复:你好,不要输出任何其他内容。" TITLE = "Codex Cron" SUCCESS_MSG = "Codex keepalive 成功:你好" def shorten(text: str, limit: int = 120) -> str: text = " ".join(text.replace("\r", " ").replace("\n", " ").split()) return text if len(text) <= limit else text[: limit - 1] + "…" def notify(message: str) -> bool: notifier = shutil.which("terminal-notifier") if notifier: res = subprocess.run([notifier, "-title", TITLE, "-message", message], capture_output=True, text=True) if res.returncode == 0: return True osa = shutil.which("osascript") if osa: safe_message = message.replace('\\', '\\\\').replace('"', '\\"') safe_title = TITLE.replace('\\', '\\\\').replace('"', '\\"') script = f'display notification "{safe_message}" with title "{safe_title}"' res = subprocess.run([osa, "-e", script], capture_output=True, text=True) if res.returncode == 0: return True return False def main() -> int: with tempfile.TemporaryDirectory(prefix="codex-keepalive-") as tmp: tmp_path = Path(tmp) out_file = tmp_path / "last_message.txt" cmd = [ "codex", "exec", "--skip-git-repo-check", "--ephemeral", "--color", "never", "--cd", str(tmp_path), "--output-last-message", str(out_file), PROMPT, ] res = subprocess.run(cmd, capture_output=True, text=True) msg = out_file.read_text(encoding="utf-8", errors="replace").strip() if out_file.exists() else "" if res.returncode == 0 and msg == "你好": print("OK 你好" if notify(SUCCESS_MSG) else "OK 你好 | notify-unavailable") return 0 combined = "\n".join(part for part in [msg, res.stderr, res.stdout] if part).strip() reason = shorten(combined or f"exit {res.returncode}") notify(f"Codex keepalive 失败:{reason}") print(f"ERR {reason}") return 0 if __name__ == "__main__": raise SystemExit(main())
Validate it first:
terminal(command="python3 -m py_compile ~/.hermes/scripts/codex_keepalive_notify.py") terminal(command="python3 ~/.hermes/scripts/codex_keepalive_notify.py", timeout=240)
Update/create cron job
cronjob( action="update", job_id="<job_id>", schedule="30 8,13,18,23 * * *", deliver="origin", skills=[], script="codex_keepalive_notify.py", prompt="预运行脚本已经完成工作。只把脚本 stdout 的最后一行原样输出,不要添加解释。" )
Making it actually fire automatically
Creating/updating the cron job is not sufficient by itself. Hermes cron depends on the Hermes gateway scheduler.
Recommended checks:
- Install/start the gateway if the user expects jobs to run after closing the current Hermes chat/CLI:
on macOS user sessionshermes gateway installhermes gateway statushermes cron status
- Treat
as the most authoritative quick check for whether jobs will actually fire.hermes cron status - On macOS, also verify persistence with:
launchctl list | grep 'ai.hermes.gateway'
(look for Gateway Service loaded/not loaded)hermes status --all
Important macOS behavior:
creates a LaunchAgent athermes gateway install~/Library/LaunchAgents/ai.hermes.gateway.plist- This is an Aqua user-session service: closing the Hermes CLI is fine, but local scripts/notifications will not run while the machine is asleep or the user is fully logged out of the GUI session
- If the gateway comes up late and misses a scheduled time by more than its grace window, Hermes may fast-forward to the next slot instead of backfilling the missed run; check
~/.hermes/logs/gateway.log
Verifying success
Check all three:
- The local script works when run directly
- The user receives the macOS notification
shows sane values for:cronjob(action='list')next_run_atlast_run_atlast_statuslast_delivery_error
Notification icon pitfall on macOS
If you use
terminal-notifier without extra flags, the notification icon is the terminal-notifier app icon, not the title text and not the app you are conceptually automating.
In Homebrew terminal-notifier 2.0.0 this can look like an orange icon with a white starburst/flower.
To change it, use one of:
for Terminal icon-sender com.apple.Terminal
for another app icon-sender <bundle-id>
for a custom icon-appIcon <png-or-url>
Do not assume a title like
Codex Cron will change the icon automatically.
Timeout troubleshooting and recovering the script's last stdout line
Hermes runs cron
scripts with subprocess.run(..., capture_output=True, timeout=_SCRIPT_TIMEOUT). If the script times out, the cron prompt only receives a generic error like:
Script timed out after 120s: /Users/<user>/.hermes/scripts/your_script.py
Important consequence:
- the LLM does not automatically receive the script's partial stdout/stderr on timeout
- if the prompt asks to repeat the script's last stdout line, you may need to recover it manually from side effects the script left behind
Recovery pattern
Use this when the timed-out script itself is designed to write its meaningful result to a file before exiting or hanging.
For the Codex keepalive pattern above:
- Read the script source and identify the tempdir prefix and output file path pattern
- here:
tempfile.TemporaryDirectory(prefix="codex-keepalive-") - and
out_file = tmp_path / "last_message.txt"
- here:
- Search temp locations for surviving directories/files
- on macOS commonly under
/var/folders/.../T/ - search for
or the known prefixlast_message.txt
- on macOS commonly under
- Check file mtimes against the cron run time from
or the cron session timestamp~/.hermes/logs/gateway.log - Read the recovered file and use its final line as the best-grounded answer
Why this works
If the child command (for example
codex exec --output-last-message <file>) already wrote its last message file before the parent Python script timed out, that file can still survive briefly in the temp directory even though Hermes only reports the generic timeout string.
Freshness check before trusting recovered output
Do not assume every leftover temp artifact belongs to the current failed run.
Use this checklist:
- Compare the artifact mtime with the current cron run time (
, current session timestamp, or nearby gateway/agent log timestamps)last_run_at - Prefer temp directories created at or very near the current scheduled run
- If the only recoverable file is clearly from an older run, treat it as stale evidence
- In that stale-evidence case, prefer the exact script error injected into the cron prompt (for example
) instead of reusing an old success payloadScript timed out after 120s: ...
This avoids incorrectly replaying a previous run's
last_message.txt when the current run produced no trustworthy recoverable stdout.
Useful forensic locations
When investigating cron behavior after the fact, check:
for archived prompt/response markdown from prior runs~/.hermes/cron/output/<job_id>/
for the full cron conversation history~/.hermes/sessions/session_cron_<job_id>_*.json
and~/.hermes/logs/agent.log
for nearby timestamps and scheduler activity~/.hermes/logs/gateway.log
These are especially helpful for correlating whether a recovered temp artifact is from the same run.
Pitfalls
- Avoid immediate manual
if the user wants a cleanrunnext_run_at
is important when the point is minimal overheadskills=[]- A cron prompt still exists; keep it tiny and deterministic
- Test scripts directly before wiring them into cron
- Hermes cron will not fire unless the gateway scheduler is actually alive
- On macOS, Focus mode / notification permissions can hide successful notifications even when the command returns success
- If
is used withoutterminal-notifier
/-sender
, the icon will be terminal-notifier's own app icon, not the conceptual app being automated; use-appIcon
if the user wants a Terminal-looking notification-sender com.apple.Terminal - For cron-only local use with no messaging platforms enabled, idle gateway overhead is low (roughly tens of MB RSS and near-zero CPU), but it is still a user-level background process and should be explained clearly to the user
- Do not set
unless the user explicitly wants open access; for cron-only local use it is unnecessaryGATEWAY_ALLOW_ALL_USERS=true - On script timeout, Hermes reports only the timeout error string by default; do not assume partial stdout was preserved unless you verify the scheduler implementation or recover it from files/logs