A Hermes hooks doctor says the hook will not fire when auto-accept wires it

John Burns

hermes hooks doctor printed hook will NOT fire at runtime. In that same process HERMES_ACCEPT_HOOKS was 1. The registration call then wired the hook and wrote the allowlist pair the doctor line had just called missing.

That sentence is the allowlist file. It is not the runtime decision. On Hermes 0.22.5 the runtime also accepts hooks_auto_accept: true and --accept-hooks. Doctor does not read any of the three. A second doctor run, after the write, said the same hook was healthy.

The executable line makes the same kind of mistake. Doctor said python3 -c 'print("{}")' was missing or not executable. hermes hooks test ran that command, exited 0, and printed {}.

What was measured

The binary was Hermes Agent v0.22.5+471.g5894e75, commit 5894e750c3, dated 2026.9.24 in its own version line, on Linux x86_64. The run was 4 November 2026. No gateway was started. No login was used. No prompt was sent. Each hook script printed {} and exited. The normal Hermes home was not the directory under test.

A new data root handed to the hermes launcher did not reach the subcommand. The launcher started a dependency install for that root and failed building python-olm because clang++ was not on the path. The numbers below are from hermes_cli.main.cmd_hooks, the wrapper hermes hooks forwards to, and from agent.shell_hooks.register_from_config, loaded by the interpreter already installed for that checkout. Each case set HERMES_HOME to a new directory. That is the same handler the subcommand uses. It is not a second implementation.

The checkout’s hooks page, read the same day, lists three ways past the interactive prompt: --accept-hooks, HERMES_ACCEPT_HOOKS=1, and hooks_auto_accept: true. It says the gateway, cron, and CI never prompt, so they need one of those three or an allowlist entry, or a newly added hook stays unregistered. It says doctor checks the exec bit, allowlist status, mtime drift, JSON output, and rough execution time. It says to re-run doctor after pulling a shared config, to spot newly added hooks before they register. It does not say that the will-not-fire line ignores the three opt-ins. It does not say that a command doctor calls missing can still run under hooks test.

The public hooks page, read the same day, names shell hooks as the hooks: block in the profile config, running in the CLI, the gateway, and desktop chat. The doctor sentences in this note are the handler’s sentences, checked against the checkout page that shipped with this binary.

The will-not-fire line is the file

Each opt-in case used a mode 755 script that printed {}. The configured command was that script’s absolute path. hooks_auto_accept was false unless the case says otherwise. No allowlist file was present at the start, except in the control that already had a matching approval.

With the environment variable unset and auto-accept false, doctor printed one issue:

✗ not allowlisted — hook will NOT fire at runtime (run with --accept-hooks once, or confirm at the TTY prompt)

register_from_config, called with accept_hooks false and interactive false, wired nothing. The registered set stayed empty. A log line named the missing allowlist file. That case matches the sentence. An empty allowlist and no opt-in really does leave the hook unwired.

The same script, with hooks_auto_accept: true and no allowlist file, produced the same doctor lines, including will-not-fire. The registered set was empty before the registration call, which is why the file check failed. The registration call then returned the hook as newly wired. The allowlist file appeared, with that event and that exact command. A later doctor run on the same home printed allowlisted, script unchanged since approval, produced valid JSON, and All shell hooks look healthy.

HERMES_ACCEPT_HOOKS=1 did not need the config flag. In one process the variable was set, hooks_auto_accept was false, and the allowlist was empty. Doctor still printed will-not-fire. cmd_hooks returned None. The registration call in that process wired the hook. The allowlist pair was present afterward.

Doctor’s own suggested remedy is --accept-hooks or a TTY prompt. The message does not name hooks_auto_accept or HERMES_ACCEPT_HOOKS, even though both are enough for the registration call, and even though the environment variable was already set when doctor printed the line. hermes hooks list on a home with no opt-in and no allowlist printed not allowlisted. It also does not print the opt-in. If you only read the list, a home that will accept every new hook on the next registration looks the same as a home that will skip it.

The wrapper return is not a gate. cmd_hooks returned None both when doctor printed 1 issue(s) found and when it printed All shell hooks look healthy. The CLI treats a non-integer return as success. A script that checks the exit status will not see the issue count. Read the last line of the text.

The checkout page is right that a gateway which never prompts needs an opt-in or an allowlist entry. Doctor compresses that into one file check, then states a runtime outcome the file check does not have. Pull a shared config that already sets hooks_auto_accept: true, run doctor, see will-not-fire, and you have not shown that the next registration will skip the hook. You have shown that the allowlist file does not contain the pair yet. The next registration writes the pair.

A missing script can be a PATH command

Doctor’s first check is not the exec bit of the command as a whole. It resolves one token: the first token with a script suffix, otherwise the first path-like token, otherwise the first token. If that token is not a file, the line is script missing or not executable. The runner does not use that test. It splits the command and passes the argument vector to a subprocess with the shell disabled, which searches PATH for the first token.

python3 -c 'print("{}")' failed the file test. Doctor also skipped the JSON smoke test, because the hook was not allowlisted, and said to approve the hook before relying on it. hooks test pre_tool_call fired the command anyway:

exit=0  elapsed=0.187s
stdout: {}

The command true failed the same file test. Test exited 0 in 0.037 seconds and produced no stdout. /usr/bin/true passed the file test, because that token is a file and the executable bit is set. Test exited 0. The bare name and the absolute path are not the same check. A doctor failure on true does not mean /usr/bin/true would fail, and a pass on the absolute path does not mean the bare name would pass.

A mode 644 script used as the whole command failed both sides. Doctor said missing or not executable. Test printed command not executable and did not run the body. The same file, invoked as bash plus the absolute path, passed doctor’s executable line. The file mode was still 644. Test exited 0 and printed {}. The interpreter form only needs the script to be readable. Doctor’s word “executable” is the spawn rule, not a chmod result. The checkout page’s phrase “exec bit” is the wrong phrase for that line when the command starts with an interpreter.

hooks test does not consult the allowlist. The checkout page says test fires every matching hook. It does not say that this fire is the smoke test doctor refuses to run on an unreviewed script. Doctor’s skip is the review boundary. Test is not. Do not point test at a command you have not read. On the python3 -c home, test ran the command doctor had declined to run.

An allowlisted control, with script_mtime_at_approval set to the mtime doctor itself prints, passed every line: executable, allowlisted, unchanged, valid JSON, and All shell hooks look healthy. A mismatched timestamp on an otherwise identical approval printed script modified since approval and still ran the smoke test. The drift line is a timestamp comparison. It is not a hash of the file. The checkout page already says script edits are trusted because the allowlist keys on the command string. This run did not edit the script after a matching approval, so it does not add a new fact about silent trust. It does show that a wrong timestamp is enough for the warning, and that the warning does not stop the smoke test or the registration. The hook still wired.

Verify the opt-in before the doctor sentence

Read hooks_auto_accept in the home you mean to test. Read HERMES_ACCEPT_HOOKS in the environment of the process that will register hooks. Doctor will not read either one. If either is set, will-not-fire means the allowlist file does not yet contain the pair. The next registration writes the pair and wires the hook. That includes a process started with the flag, and a config a teammate pulled in with the key already true.

The checkout page is still the right page for the three opt-ins and for the rule that gateway, cron, and CI never prompt. It is the wrong page if the question is whether doctor’s issue line is that rule. The line is the file. The opt-in is the config or the environment. Confirm the file after a registration, not before, if the opt-in is on. The healthy doctor run in this note is that later file. The will-not-fire run is the earlier file. They are not two opinions about one state.

Then compare the executable line with hooks test only after you have read the command. A PATH command doctor calls missing can still exit 0. A bare script without the executable bit should fail both, and did. A bash prefix can make a mode 644 file look executable to doctor, and the runner will honor that prefix. If the command is an absolute path to a mode 755 script, doctor’s executable line and the test agreed. That agreement is the case the page’s “exec bit” phrase actually describes. It is not the case for python3 -c or for true.

If the hook must stay off, the opt-in has to be off and the allowlist pair has to be absent. hermes hooks revoke removes the pair. It does not clear hooks_auto_accept. A later registration with the key still true writes the pair again. This run showed that write. It did not start a gateway, so it does not show the 30-second reload the page describes for a process that is already up. A running gateway with the opt-in already set is outside this measurement. What this measurement shows is that doctor, before that write, will still say the hook will not fire.

cmd_hooks returning None is not a clean bill. Read the issue count in the text. Zero issues, on this binary, was the sentence All shell hooks look healthy. That sentence appeared only after the allowlist write, and only for a command whose resolved token was a readable or executable file. It did not appear for the PATH command, and it did not appear while the opt-in was set and the file was still empty.

Sources

  • Hermes Agent v0.22.5+471.g5894e75, commit 5894e750c3, Linux x86_64, 4 November 2026. cmd_hooks and register_from_config on disposable homes. No gateway, no login, no model turn.
  • The same checkout’s hooks page, consent model and hermes hooks table, compared with the public hooks page read the same day: Event Hooks.
  • Handler text from hermes hooks doctor and hermes hooks test on that binary. The launcher did not reach the subcommand on a new data root.