Hermes resume reports clear while a directory sentinel remains

John Burns

hermes resume exited 0. The sentence was Hermes is not paused, and it named the sentinel path. The next hermes status still printed the pause banner, with no reason on it. The path in the resume sentence was a directory. The directory was still there after the command returned.

That is the line that will mislead a wrapper. On this install, a directory named ESTOP holds the stop. Resume reports that the stop is gone because it cannot unlink a directory. Status keeps reading the directory as engaged. A script that treats the resume sentence as the state of the pause will start work that the scheduler is still skipping.

A regular file does not have that split. The same binary held a due job while a file sentinel existed, and it ran the job after that file was removed. Both ticks printed nothing. The difference was a marker file the job itself wrote. If you only watch the tick transcript, the held run and the completed run look the same.

The directory the resume line misses

The home under test was a new directory, selected with HERMES_HOME. Nothing was written into the operator home. hermes status exited 0 and printed:

PAUSED (global emergency stop; `hermes resume` to lift)

The command’s own output puts a pause mark in front of that line. There was no reason clause. The only object at the sentinel path was an empty directory created before the status call. No JSON file was present.

hermes resume then exited 0 and printed:

Hermes is not paused (no sentinel at <profile-home>/ESTOP).

The directory was still a directory. A second hermes status exited 0 and printed the same banner, again with no reason. The resume sentence and the status banner disagreed, and the banner matched the filesystem.

The public command table, read the same evening, calls hermes pause / hermes resume a global emergency stop. It says new cron fires, kanban dispatch, and new gateway turns do not start until resumed, and that work already running finishes. It does not mention a sentinel path, and it does not say what resume prints when the path exists but cannot be removed. The checkout copy of that table, at the commit this binary was built from, is the same sentence. The module that implements the stop says an empty or corrupt file still counts as engaged, and that resume removes every visible sentinel. A directory satisfies the existence check and fails the removal. The CLI then reports the removal failure as “not paused.”

An empty file and a file containing the text not json took the other branch. Each one made status print the same banner with no reason. Resume printed the resumed line, and the path was gone afterward. A later status was not run against those two homes. The file listing is the evidence that resume could remove them. Corrupt content is not the defect. The type of the path is.

A symlink at the sentinel path, pointing at a regular file in the same home, also produced the banner with no reason. The target text was keep-me, which is not the JSON object the pause command writes, so there was no reason to print. Resume printed the resumed line, removed the symlink, and left the target file in place with that same text. Resume unlinks the path it was given. It does not follow the link and delete the file the link named.

A due job that tick does not mention

The file case is the one the resume line handles. It is also the one a quiet tick will hide.

The binary was Hermes Agent v0.22.5+471.g5894e75, dated 2026.9.24, local commit 5894e750c3db, on Linux x86_64. hermes --version reported Python 3.14.9. The pause and resume lines above, and the tick below, were the stock launcher with HERMES_HOME pointed at a disposable directory under the account tree. The operator home’s sentinel was absent before the run and absent after it. No model turn was started.

A no-agent job was created in that disposable home with hermes cron create every 1m --name marker --script mark.py --no-agent --deliver local. The script lived in that home’s scripts/ directory, which is the only place this command accepts a script path. It wrote one line, ran, to a marker file and printed marked. Create exited 0 and assigned a job id. It also printed:

Scheduler is not ready for profile 'custom': no gateway serves it and no fresh ticker heartbeat.

That warning is about the unattended ticker. It is not a refusal of hermes cron tick. The profile name custom is what this launcher prints when HERMES_HOME is not a named profile directory under the account root.

Create scheduled the next run a minute ahead. So that the tick had something due, the lab copy of jobs.json had that job’s next_run_at set to 2020-01-01T00:00:00+00:00. The job stayed enabled. No other field was changed for the pause check. The marker file was absent.

hermes pause --reason hold-tick exited 0. It printed Hermes paused, the reason, and the sentinel path, which was a regular file in that home. The file was JSON:

{
  "engaged_at": "2026-11-10T04:06:47.322088+00:00",
  "reason": "lab-alpha"
}

The timestamp and reason above are from an earlier pause in the same session shape. The hold-tick pause wrote the same two keys, with its own timestamp and the reason hold-tick. hermes cron tick then exited 0 and printed nothing. The marker file was still absent. The due job did not run.

hermes resume printed the resumed line and removed the file. hermes cron tick again exited 0 and again printed nothing. The marker file now contained ran. The script’s own marked line did not appear on the tick terminal. Local delivery did not put that line on stdout either. The side effect is the evidence. The tick transcript is not.

If you are writing a check that pause held a job, do not grep the tick output for a skip message. This binary does not print one. Write a marker from a no-agent script, make the job due, tick, and read the marker. Then resume, tick, and read it again. Empty stdout is what you get in both states.

The file pause writes, and the profile that does not see it

Pause writes one file: $HERMES_HOME/ESTOP. It does not write a second copy in the parent directory. A second home, created as a sibling and selected with its own HERMES_HOME, had no sentinel file. Its hermes status exited 0 and did not print the pause banner. Its hermes cron list exited 0 and said there were no scheduled jobs in profile custom. The first home’s status, run while its own file existed, printed the banner with the reason. The stop followed the process home, not the directory that happened to contain both homes.

That matters if you pause one profile and assume the fleet stopped. On this launcher, with both homes under the account tree, the sibling did not stop. The public table’s word “global” is not a promise that every HERMES_HOME on the machine shares one file.

A sentinel placed in the parent of those two homes was also invisible to both of them. The parent was itself under the account hermes directory, so it was not the account root. Status in each child home had no banner. Resume from the child printed the resumed line and removed the child’s own file, when one existed, and left the parent file where it was. Resume with HERMES_HOME set to the parent removed the parent file and printed the resumed line. A second resume against that same parent printed the not-paused sentence and named the parent path. The file you intend to be the stop has to be the file the process will stat. An intermediate directory under the account root is not that file.

While the child’s file was present, these commands still exited 0: hermes cron list, hermes cron tick, hermes bundles list, hermes skills list, and hermes project list. None of them printed the pause banner. Status is the management command that does. A wrapper that treats a zero exit from cron list, or from project list, as “the agent is accepting work” will not see the stop. Those commands are not gateway turns. The pause help text says new gateway turns are held, and that in-flight work keeps running. It does not say that list and tick refuse to start.

Re-pausing a home that already had a sentinel printed Still paused and the new reason, and rewrote the file. The first pause of an absent file printed Hermes paused. Both exited 0. The verb is the only difference in the first line. Do not key a retry on the exit code.

A profile path that was a symlink to a real directory behaved like the real directory for the file, and like the link for the message. Pause printed the link path as the sentinel. The file was written through the link into the real directory, with the reason that was passed. Status with HERMES_HOME set to the real directory printed the banner and that reason. The message and the directory listing are not the same string. Read the file, or follow the link, before you decide which home is paused.

A home outside the account tree

The two-path check described in the module is not what the stock launcher would run for a data root outside the account hermes directory. hermes pause with HERMES_HOME set to such a directory exited 1 before any pause line. Stderr began hermes: preparing dependencies for this data root and ended in a failed build of python-olm==3.2.16. No sentinel file was written. On this install, the shell command is not a way to pause a foreign data root. It tries to prepare a runtime for that root first, and that preparation failed.

The command functions the launcher calls after that preparation were run directly, under the checkout’s Python 3.14.9, with HERMES_HOME set and the account HOME left alone. That is not the shell command. It is the same pause and resume functions, which is the part the launcher never reached.

In that run the home layout was a fleet directory whose profiles/ child held two profile directories. Neither profile was under the account hermes directory. Pause with HERMES_HOME set to the fleet directory wrote ESTOP there and printed the paused line with the reason fleet. A state read from each profile reported engaged, with that reason, and named two candidate paths: the profile’s own ESTOP, which did not exist, and the fleet directory’s ESTOP, which did. Resume with HERMES_HOME set to the first profile printed the resumed line. The fleet file was gone afterward. A state read from the second profile then reported not engaged. One profile’s resume removed the file the other profile was honoring.

That is the opposite of the nested-under-account result above, and it is why the layout has to be named. Under the account tree, the second candidate is the account hermes root, not the parent you happened to create. Outside that tree, when the process home’s parent directory is named profiles, the second candidate is the grandparent, and resume deletes it. The public table does not describe either rule. If you operate more than one home, find the file before you resume, or a resume aimed at one profile can lift a stop that was set on the parent.

This direct call did not start the launcher, and it did not build the isolated runtime the launcher demanded. It does not say that a future launcher, past the python-olm failure, will print the same lines. It does say what these command functions do with the two paths when they are invoked.

Verify the stop before you trust the line

Read the path before you trust either sentence.

After hermes pause, the first line should say Hermes paused on a new stop, or Still paused if a sentinel was already visible. The following line names the file the command wrote. That path should be a regular file. If it is a directory, status will keep printing the banner, and resume will say the pause is gone. If it is a symlink, resume will remove the link and leave the target. Replace the link with a regular file if you want the next resume to remove the stop itself.

Open the file. A pause command writes JSON with engaged_at and reason. An empty file, or a file that is not that JSON, still pauses, and the status banner omits the reason. A missing reason is not proof that the file is absent. It is proof that the body did not parse as an object with a reason.

If the process home is under the account hermes directory, also look for ESTOP in that account root. A file in an intermediate parent is not the second candidate. A sibling profile with its own HERMES_HOME will not see the file you just wrote. If the process home is outside the account tree and sits under a directory named profiles, the second candidate is the grandparent, and resume from the profile removes that file too. The stock launcher on this build did not reach that layout. The command functions did.

Do not use hermes cron list, hermes project list, hermes skills list, or hermes bundles list as the pause check. They exited 0 while a file sentinel was present, and they did not mention it. Use hermes status and read the banner, then confirm the path type on disk.

For the scheduler, do not read an empty hermes cron tick as either success or a skip. Put a no-agent script under the home’s scripts/ directory, pass the filename rather than an absolute path, and have the script write a marker outside the transcript. Make the job due. Tick once while the sentinel is a regular file, and require the marker to be absent. Resume, confirm the file is gone and that status has no banner, tick again, and require the marker. The create warning about a missing gateway ticker does not block that explicit tick. It does mean an unattended scheduler may not be running for that home at all. Pause cannot hold a ticker that is not there.

The operator home was not paused by any of these commands. A pause aimed at a disposable HERMES_HOME stayed in that home, except for the direct call outside the account tree, where a profile resume deleted the parent sentinel that the other profile was reading. Check both candidate paths before you treat one resume line as the state of every home on the machine.

References

  • Hermes Agent v0.22.5+471.g5894e75, local commit 5894e750c3db, Python 3.14.9, Linux x86_64, 9 November 2026. hermes --version and the stock launcher produced the status, pause, resume, cron, project, skills, and bundles lines. HERMES_HOME pointed at disposable directories. The operator home’s sentinel was absent before and after.
  • Checkout module agent/estop.py at that commit, SHA-256 d06a18fc5bd9a75abc1526a4918891fc688f8cc8f8328b9c989af6ea21d64be5. An empty or corrupt body still engages. Resume unlinks every candidate path it can.
  • Checkout command module hermes_cli/subcommands/pause.py at that commit, SHA-256 ba834c9c3b4e353519fbb826a9540676ffd18832c5107bf346d953cb834df65b. The resumed line is printed only when an unlink succeeds. Otherwise the command says it is not paused and names sentinel_path().
  • The fleet-root resume, where one profile’s resume removed the parent file the other profile was honoring, was those command functions under the checkout interpreter. The stock launcher exited 1 on that home during dependency preparation, on python-olm==3.2.16, and wrote no sentinel.
  • Hermes command reference, https://hermes-agent.nousresearch.com/docs/reference/cli-commands, read 9 November 2026. The pause row describes a global emergency stop for new cron, kanban, and gateway turns. It does not describe the sentinel path, a directory, or a silent tick. The checkout copy of that row at 5894e750c3db matches.