Hermes curator pin records a missing name, and lock is not a command

John Burns

hermes curator status counted one hand-written skill as curator-managed and agent-created. The pin command that had just written that skill’s record said the skill was unmanaged. hermes curator list-unmanaged still listed it. Those three prints came from one sidecar, one directory, and one build. They do not describe the same fact.

The curator page, read the same day, tells you to run hermes curator lock when a hand-authored skill must stay readable and must not be changed by the agent. On the installed launcher that word is not a curator subcommand. The process exits 2 before it looks for a skill, a directory, or a sidecar. Pin is the command this build actually has. Pin does not freeze the file. It also writes a record for a name that has no directory, and status does not show that record.

The command the page names

The binary was Hermes Agent v0.22.5+471.g5894e75, dated 2026.9.24, commit 5894e750c3db, on Linux x86_64. hermes --version also said an update was available. That update was not run. No model turn was started. No skill_manage call was made. Nothing below says an agent write was refused, because no agent write was attempted.

The lock check used the installed hermes launcher against the existing home. Stdout was empty. Stderr was two lines, and the exit code was 2:

hermes curator: 'lock' is not a `hermes curator` command.
Run `hermes curator --help` to see all commands.

hermes curator unlock hand-runbook exited 2 with the same shape, naming unlock. hermes curator --help exited 0. Its subcommand list was status, usage, run, pause, resume, pin, unpin, list-unmanaged, adopt, restore, list-archived, archive, prune, backup, rollback, ledger, and purge. lock and unlock were absent. pin was present, and its help line was “Pin a skill so the curator never auto-transitions it.” That line does not mention archive.

The live skills sidecar was compared before and after those three launcher commands. Its modification time, inode, and size did not change. The refusal is the parser. It does not write a lock flag, and it does not tell you that the flag is missing from the file format.

The pin, archive, prune, and status cases below did not go through that launcher. A new data root handed to hermes printed no dependency environment is committed for this install; run hermes pm repair from hermes --version, and hermes curator lock on that same root did not return within 90 seconds. The new root then held an update-in-progress marker. That process was not left running, and hermes pm repair was not run. The numbers below are from hermes_cli.curator.cli_main, the handler the subcommand calls once a dependency environment is already available, with HERMES_HOME pointed at a new directory. The install’s dependency interpreter was Python 3.14.9. The live home was not the directory under test.

Calling that handler with lock also exited 2, but the text was argparse’s invalid-choice error, not the launcher’s shorter sentence. A script that matches one of those strings will miss the other. Both exits mean the same thing on this build: there is no lock subcommand to run.

The v0.23.0 tag, released 5 November 2026, is a different tree. Its curator source defines lock and unlock, and the current curator page documents those names. That tag was read. It was not executed. This note does not say v0.23.0’s lock stops a write, and it does not say hermes update would make the command appear. It says the build that is installed, and that still reports an update available, rejects the command the page tells you to run.

A pin that status calls managed

The page’s pinning section says only agent-created skills can be pinned, and that pin refuses bundled and hub-installed skills. It says a pin is "pinned": true in skills/.usage.json, and that pin protects a skill from the curator’s automated archive passes and from skill_manage delete. It says edits still go through. The locking section says lock is the control that stops the agent from changing the skill at all, including a hand-authored one, and that status lists locked skills.

On this build a hand-written skill is not refused by pin. A directory skills/hand-runbook/ with a SKILL.md whose frontmatter name was hand-runbook, and with no created_by marker, was pinned. The command exited 0 and printed:

curator: pinned 'hand-runbook' (recorded; this skill is unmanaged — auto-transitions never consider it. Run `hermes curator adopt hand-runbook` to put it under curator management)

The sidecar record had "pinned": true and "created_by": null. The success line is doing two jobs. It records the flag, and it tells you the flag does not put the skill under the automatic lifecycle until you adopt it. Adopt is the command that writes created_by to agent. That write is what makes automatic archival possible. The page’s sentence that pin refuses anything other than an agent-created skill does not describe this exit.

Status, run immediately after that pin, did not repeat the unmanaged warning as the only classification. It printed:

curator-managed skills: 1 total  (agent-created=1  bundled=0)
  active     1
pinned (1): hand-runbook
unmanaged (no provenance marker): 1 total
  foreground-created  1

list-unmanaged listed hand-runbook with created_by:null. So the same skill was, in one status print, both a curator-managed agent-created skill and an unmanaged foreground skill. The parenthetical is a provenance label. In this tree that label is agent for a local skill that is not bundled, not hub-installed, and not external-only. The comment on that function says the label covers agent-authored skills and local hand-made skills. The status heading still says agent-created. The pin line and list-unmanaged read the created_by marker, which was null. If you use the managed count to decide that the curator already owns the skill, you are reading the provenance label, not the marker adopt writes.

The bundled and hub refusals did match the page. A local skill named in .bundled_manifest, and a local skill named in .hub/lock.json, each made pin exit 1. The message called the skill bundled or hub-installed and said only agent-created skills participate in curation. Those two names were not added to the sidecar. The refusal is real for those two lists. It is not a general check that a skill was created by the agent, and it is not a check that a directory exists.

A name with no directory

hermes curator pin no-such-skill exited 0. There was no skills/no-such-skill/ directory and no SKILL.md. The success line was the same unmanaged sentence, with that name substituted. The sidecar gained a second record, created_by null, pinned true, state active, and a created_at timestamp from the pin itself.

Status after that pin still listed one pinned name, hand-runbook. It did not list no-such-skill. list-unmanaged also omitted it. The managed count stayed at 1. The pin was in the file and not in the report that is supposed to show pins.

Archive did not use that report. hermes curator archive no-such-skill exited 1 and printed:

curator: 'no-such-skill' is pinned — unpin first with `hermes curator unpin no-such-skill`

The refusal happened before any “not found” result. A typo, or a skill you have already deleted, can leave a pin that status will not display and that archive will still honor. The v0.23.0 lock handler, in the tag that was read and not run, refuses a name it cannot resolve and says nothing was locked, specifically so a typo does not forge a record. This build’s pin does the opposite. The record is the result. There is no lock command on this build to compare in-process.

That split is the check worth keeping. After any pin, read the sidecar, not only status. A pinned key whose name has no directory is a record status has already dropped. hermes curator unpin is what removes it. Waiting for status to look clean does not remove it.

Archive reads the pin flag

The pin help line talks about auto-transitions. The archive command reads the flag too. With hand-runbook still pinned, hermes curator archive hand-runbook exited 1 and named unpin. The directory stayed in place. That was before adopt.

hermes curator adopt hand-runbook exited 0 and printed that the skill was adopted into curator management. The sidecar created_by value became agent. The pin flag stayed true. Status dropped the unmanaged summary and still listed the skill under pinned. Archive still exited 1 with the same unpin sentence. Adopt did not weaken the manual archive refusal. It changed the marker the automatic pass consults. The pin is what both the manual command and, after adopt, the automatic pass are supposed to honor.

Prune makes that second half visible, because a brand-new record is not idle. hermes curator prune --days 1 --dry-run on the fresh pin printed that nothing was eligible. That print cannot separate “pinned” from “not old enough.” The record’s created_at was then set to 2020-01-01T00:00:00+00:00, with no use, view, or patch timestamp. Dry-run prune at one day still printed that no unpinned skill was idle that long. The same record with pinned set back to false printed one candidate, hand-runbook, idle 2502 days, and said the dry run had made no changes. The directory was still the original directory. Pin exempts a managed skill from that prune selection. The help line’s “auto-transitions” claim is narrower than the archive refusal, and the prune exemption is real once the skill is old enough to be selected. A fresh pin’s “nothing to prune” line is not that proof.

The page’s lock flag was then written by hand. pinned was false, locked was true, on the adopted hand-runbook record. Status did not print a locked list. It printed the managed skill and no pin line. hermes curator archive hand-runbook exited 0 and moved the directory to skills/.archive/hand-runbook. The SKILL.md was in the archive and was not left in the active tree. A locked key in the sidecar is not a control this build reads. The page says that key is what hermes curator lock stores, and that status lists it. This status had no such line, and archive treated the skill as ordinary.

That is the protection boundary on this install. Pin, once recorded, blocks hermes curator archive even when status does not list the name, and it blocks prune of a managed skill that is otherwise idle. It does not freeze SKILL.md. This run did not edit the file through an agent tool, so it does not add a result about skill_manage, write_file, or terminal. The page says those paths still allow edits on a pin, and says lock is what stops them. Lock is not a command here. A hand-written locked key did not become a substitute. If the requirement is that the agent cannot change the file, this build does not offer the command the page names, and pin is not that command.

Two day counts on one page

An empty probe home, before any skill existed, made status print stale after: 14d unused and archive after: 30d unused, with consolidate off. Those are the numbers in the page’s configuration block. The same page, in the section that describes the agent-created lifecycle, still says active, then 30 days unused to stale, then 90 days unused to archived. Those are not the numbers an empty home prints.

They are the numbers status prints if you put them in config.yaml. A curator block with stale_after_days: 30 and archive_after_days: 90 made the next status print 30 and 90. Status did not rewrite the file to 14 and 30. A block with stale_after_days: 0 and archive_after_days: -3 did not get used. Status printed 14 and 30, and stderr named both keys, said each must be at least 1, and said it was using the default. The fallback is per bad value. It is not a migration of the old 30 and 90 pair. A config that still has the lifecycle sentence’s numbers will be honored. An empty config will not invent them.

The update path in this tree contains a migration that rewrites a stored 30 to 14 and a stored 90 to 30. This run did not execute that migration and did not run hermes update. Do not treat the lifecycle sentence and the status line as one clock. Read the status line on the home you are about to leave running. If it says 14 and 30, the 30-then-90 sentence on the page is not the clock that home will use.

Verification

Confirm the version before comparing a refusal with this note. hermes --version on the tested launcher printed Hermes Agent v0.22.5+471.g5894e75 (2026.9.24) and named commit 5894e750c3db. A newer tree can add lock. This note is the tree that does not have it.

On the installed launcher, against the home that launcher already uses:

hermes curator lock hand-runbook

Expect exit 2, empty stdout, and the sentence that lock is not a curator command. hermes curator --help should list pin and should not list lock. If lock is accepted, you are not on the build measured here, and the missing-name result below is not yours to assume.

Do not point this launcher at an empty data root to make a scratch home. On this install that path printed the missing-dependency-environment line and did not return from hermes curator lock within 90 seconds. The pin results were taken from the curator handler with a home that was not handed to the launcher.

If you repeat the pin cases, use a name you can unpin, and read skills/.usage.json after each command. A hand-written skill with a frontmatter name should pin with exit 0, created_by null, and the unmanaged sentence. Status can still say agent-created=1 and list-unmanaged can still list that name. Those prints together are the result, not a contradiction you need to repair before you believe either one.

Then pin a name that has no directory. Expect exit 0 and a new sidecar record. Expect status to omit that name from the pinned list. Expect hermes curator archive of that name to exit 1 and tell you to unpin, even though the directory was never there. Unpin it and confirm the record is gone or no longer pinned. Leaving the typo in place is how a later archive fails for a skill you cannot see in status.

For the idle check, a dry-run prune of a minutes-old pin proves nothing about the exemption. Backdate created_at, keep pinned true, and expect the dry run to skip it. Clear the flag and expect the same name to be listed. Do not drop --dry-run on a home you care about. This run did not.

A locked key written into the sidecar is not a passing test of lock. On this build archive still moved the skill. If your status prints a locked list, the tree has the newer command, and this note’s archive result does not apply.

Stop there. The page’s lock command is the one this launcher rejects. The pin this build records is a sidecar flag that archive honors and that status can hide when the directory is gone. It is not a freeze of the skill file, and a hand-written locked value does not become one.

Sources

  • Hermes Agent v0.22.5+471.g5894e75, commit 5894e750c3db, Linux x86_64, 7 November 2026. hermes curator lock and hermes curator unlock were the installed launcher against the existing home. Pin, status, list-unmanaged, adopt, archive, and prune were hermes_cli.curator.cli_main under the install’s Python 3.14.9, with a disposable home. No model turn, no skill_manage, no hermes update.
  • Curator page, read 7 November 2026: Curator. It documents hermes curator lock, says status lists locked skills, says only agent-created skills can be pinned, and still describes a 30-day stale and 90-day archive lifecycle beside configuration defaults of 14 and 30.
  • Hermes Agent v0.23.0 release, 5 November 2026: v0.23.0. The release notes name hermes curator lock as the control that stops an agent from rewriting a hand-curated skill. That tag’s hermes_cli/curator.py defines lock and refuses an unresolved name. The tag was not executed here.