# Hermes Safe Auto Upgrade

> Set up Hermes to safely self-upgrade on a schedule with pre-upgrade backup commits, push/tag safeguards, and user notifications before and after any real upgrade.

- Skill: `ichichuang/hermes-safe-auto-upgrade` (Agent Skill)
- Install (CLI): `npx skillmds@latest add ichichuang/hermes-safe-auto-upgrade`
- Raw SKILL.md: https://api.skillmd.com/api/skills/ichichuang/hermes-safe-auto-upgrade/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: ichichuang (https://skillmd.com/u/ichichuang)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/ichichuang/hermes-safe-auto-upgrade

---


# Hermes Safe Auto Upgrade

Use this skill when a user wants Hermes to periodically check for official updates and automatically upgrade itself **safely**, while preserving local work and notifying the user before and after a real upgrade.

## When to use

- Hermes is installed from git under `~/.hermes` and `~/.hermes/hermes-agent`
- The user wants scheduled update checks (for example every Friday at 18:00 in `America/Los_Angeles`)
- The user wants automatic backup of local changes before upgrade
- The user wants a **pre-upgrade notification** when a new version is detected and a **final result notification** after completion

## Required safety model

Treat the workflow as a two-repo system, but validate the actual remotes on disk before assuming the ideal layout:

1. `~/.hermes`
   - preferred official update source: `upstream`
   - common backup remote: `origin`
   - if `upstream` is missing, do **not** guess a new official remote URL; first verify whether the repo only has a known-safe `origin` and whether that is the only confirmable source
2. `~/.hermes/hermes-agent`
   - common official update source: `origin`
   - common backup remote: `fork`

Only allow automatic upgrade on branch `main`.
If a repo is not on `main`, a required remote is missing, git has an in-progress merge/rebase/cherry-pick state, push/tag fails, or verification fails, **stop** and report the blocking reason.

## Proven workflow

### 1. Inspect prerequisites first

Check these live rather than assuming them:

- `gh auth status`
- git remotes for both repos
- current branch for both repos
- `git status -sb`
- whether `~/.hermes` has `upstream`
- whether any candidate official repo can be safely confirmed from local config, `gh repo view`, or other grounded evidence

Real findings from validation:

- `~/.hermes` may have no `upstream` at all
- the existing `origin` for `~/.hermes` may be a private fork with `parent = null`
- if an official upstream cannot be safely confirmed, prefer reconfiguring the automation to use the known-good existing remote rather than inventing a speculative `upstream`
- `~/.hermes/hermes-agent` may be left on a feature branch; automatic upgrade must remain blocked until it is back on `main`

### 2. Implement a dedicated wrapper script

Create a script such as:

- `~/.hermes/scripts/auto_safe_upgrade.py`

The wrapper should add the missing safety layer around the built-in Hermes upgrade path:

- acquire a lock file to prevent re-entry
- verify `gh` auth
- verify both repos and required remotes
- fetch upstream refs/tags
- detect whether updates are pending
- if no update is pending, exit quietly
- if update is pending:
  - in older designs, commit/push/tag may be part of backup strategy
  - in the current Hermes-home script, the operator-safe path is typically:
    - stash local changes with `git stash --include-untracked`
    - switch/ensure `main` branch
    - fetch from `upstream` and rebase onto `upstream/main`
    - verify, restore branch/stash, and fail-closed on any verification failure
  - if verification fails, rollback and report that rollback happened
- write JSON/JSONL history under `~/.hermes/state/`

Recommended files:

- history log: `~/.hermes/state/auto_safe_upgrade_history.jsonl`
- summary snapshot: `~/.hermes/state/auto_safe_upgrade_summary.json` (current dashboard/cron truth)
- last result: `~/.hermes/state/auto_safe_upgrade_last.json` (last command detail; may be stale)
- lock file: `~/.hermes/state/auto_safe_upgrade.lock`
- current state: `~/.hermes/state/auto_safe_upgrade_state.json`
- state history: `~/.hermes/state/auto_safe_upgrade_state_history.jsonl`
- repo failure backoff: `~/.hermes/state/auto_safe_upgrade_failures.json`

### 3. Split the script into two modes

Use:

- `probe` — check prerequisites and whether updates are pending
- `upgrade` — run the allowed precondition repairs, perform upgrade/verification, and rollback on failure

This enables a cron task to notify only when a **real** upgrade is about to happen.

### 4. Notification pattern

Do **not** notify on every successful no-op check.

Instead:

1. Run `probe`
2. If no update is pending, finish quietly unless there is an error worth reporting
3. If any repo reports `update_pending`, immediately send the user a message like:
   - detected a new Hermes version
   - auto-upgrade is about to start
   - local changes will be backed up, pushed, and tagged first
   - a final result message will follow
4. Run `upgrade`
5. Send a second message with:
   - overall success/failure
   - per-repo status
   - whether backup commit/push/tag happened
   - whether verification passed
   - whether rollback happened or what blocked the run

## Cron job pattern

Create a cron job with a self-contained script invocation that:

1. runs `python3 ~/.hermes/scripts/auto_safe_upgrade.py cycle --repo all --force-upgrade-with-stash`
2. lets the script run probe first and upgrade only when updates are pending
3. parses the JSON output injected by the cron script runner
4. sends a final message only when an upgrade succeeds or fails

For messaging targets, discover the available destination first with `send_message(action='list')`, then use the exact returned target (for example a Feishu DM target).

Recommended deterministic schedule:

- `0 18 * * 5` with `timezone: America/Los_Angeles`
- set `recover_missed: true` for this job so Hermes runs one recovery cycle after downtime instead of silently skipping a missed Friday window; recovery is bounded so very old missed windows are fast-forwarded

## Important implementation notes

- `~/.hermes/hermes-agent` being on a feature/chore branch is a valid block condition; do not auto-switch branches silently
- missing `upstream` on `~/.hermes` is a common blocker and should be reported explicitly
- after three consecutive failures for the same repo, the wrapper marks that repo `upgrade_blocked`, then allows one recheck after the configured re-evaluation window
- the wrapper uses a cycle-bound expiring lock to avoid overlapping manual and scheduled cycles and to recover stale `UPGRADE_RUNNING` state
- upstream divergence is classified as `UPSTREAM_DIVERGED` and fails closed for manual policy review
- keep user-facing notifications concise and actionable
- keep the cron prompt self-contained because cron runs have no access to current chat context
- prefer reporting exact block reasons over attempting risky repair during scheduled runs

## Repair guidance for prerequisite failures

When probe fails on prerequisites, prefer **grounded repair** over blind configuration:

1. Inspect `~/.hermes/.git/config` and live remotes/branches for both repos.
2. Use `gh repo view` on any known remote to determine whether it is a fork, whether it has a `parent`, and what its default branch is.
3. If `~/.hermes` has no safely confirmable official `upstream`, update the script's `RepoSpec` for `hermes_core` to use the known-good existing remote (for example `origin`) as `upstream_remote` rather than adding an unverified remote.
4. If `~/.hermes/hermes-agent` is not on `main`, fix the branch precondition before re-running probe.
5. Re-run `python3 ~/.hermes/scripts/auto_safe_upgrade.py probe` and treat its structured JSON plus exit code as the validation result.
   - A non-zero business-status exit (for example `10` meaning update pending) is not the same as a crash.

This approach was validated in practice: the blocker on `~/.hermes` was removed by repointing `hermes_core.upstream_remote` from `upstream` to the only confirmed remote, and the `hermes-agent` blocker was removed by returning it to `main` before re-running probe.


After setup, verify all of the following:

1. the script exists and is executable
2. `probe` runs and returns structured JSON
3. the cron job exists and has the expected Friday 18:00 `America/Los_Angeles` schedule
4. notification target is valid
5. any blocking prerequisite is explicitly surfaced (for example missing `upstream` or non-`main` branch)

## Runtime triage when user says "not working"

When this exact behavior is reported, run these checks in order before changing files:

1. `python3 ~/.hermes/scripts/auto_safe_upgrade.py probe`
2. Inspect exit code:
   - `0`: no blocker, no pending update
   - `10`: one or more repos are `update_pending` (non-fatal business status)
   - non-10/0 non-zero: execution failure; report error text directly
3. Read `~/.hermes/state/auto_safe_upgrade_summary.json` first for the current cycle report, then `~/.hermes/state/auto_safe_upgrade_last.json` and `~/.hermes/state/auto_safe_upgrade_history.jsonl` for command details/history
4. Verify schedule freshness in `~/.hermes/profiles/hermes-home/cron/jobs.json` and check `next_run_at`

Common blocking pattern to expect in real environments:

- If the run appears "not triggered," check whether `next_run_at` is still in the future.
- If `upgrade` fails immediately, check for dirty worktree blocks:
  - `working_tree_clean` must be true unless running with `--force-upgrade-with-stash`.
- If no work is possible on `hermes_agent` because drift is large, output may show `risk_level: high`; in this wrapper design the scheduled cycle uses `--force-upgrade-with-stash` and records any failure in the state machine.
- Confirm `~/.hermes/hermes-agent` is on `main` and has `upstream`; missing/invalid remotes should be surfaced as blockers.

In this codebase, a practical recovery sequence is often:

- `python3 ~/.hermes/scripts/auto_safe_upgrade.py cycle --repo all --dry-run --force-upgrade-with-stash`
- Review planned actions and blockers
- If accepted, run `python3 ~/.hermes/scripts/auto_safe_upgrade.py cycle --repo all --force-upgrade-with-stash`

## Good final report contents

When reporting setup status back to the user, include:

- script path
- cron job name and id
- schedule
- whether pre/post notifications are wired in
- the exact current blocker, if any
- the next concrete remediation step
- if relevant, `next_run_at` and why it may legitimately make it appear idle

