Shell-hook allowlist keys on the command string, so an approved hook script can be edited and stays trusted
Affected versions
Confirmed in 0.18.0 (sdist from PyPI). Same code path is present in 0.17.0.
What happens
The shell-hook allowlist grants trust to an (event, command) string pair. The
approval gate never checks the on-disk script's contents, so once a hook is
approved you can edit the script it points at and it keeps running without a
re-prompt. The approval record does store the script mtime at approval time, but
only hermes hooks list / hermes hooks doctor read it, and they only print an
advisory warning. They do not re-prompt or revoke.
Where
agent/shell_hooks.py:
-
_is_allowlisted(event, command) (lines 664-671) matches only on the stored
event and command strings:
def _is_allowlisted(event: str, command: str) -> bool:
data = load_allowlist()
return any(
isinstance(e, dict)
and e.get("event") == event
and e.get("command") == command
for e in data.get("approvals", [])
)
-
_record_approval (lines 746-761) captures script_mtime_at_approval, but
nothing in the gate consults it.
-
Registration path (lines 244-268): if _is_allowlisted(...) is true, the hook
is registered with no prompt.
-
_spawn (around lines 419-517) then runs
shlex.split(os.path.expanduser(spec.command)) with no content check.
hermes_cli/hooks.py:
hermes hooks list (lines 88-99) and hermes hooks doctor (lines 347-355)
compare script_mtime_iso(command) against script_mtime_at_approval and
print a "script modified since approval" line. This is advisory only; neither
re-prompts nor removes the allowlist entry.
There is no content hash of the script anywhere in agent/shell_hooks.py.
Minimal repro
- Configure a shell hook whose command runs a script, e.g.
pre_tool_call ->
/path/hook.sh.
- Approve it once (answer
y at the TTY prompt, or use --accept-hooks). An
entry {event, command, script_mtime_at_approval, ...} is written to
~/.hermes/shell-hooks-allowlist.json.
- Edit
/path/hook.sh so it does something different. The command string is
unchanged; only the file's contents and mtime change.
- Run again.
_is_allowlisted(event, command) returns true, the hook registers
with no prompt, and _spawn executes the edited script with full user
credentials.
hermes hooks doctor prints a mtime-drift warning but the hook keeps firing;
the allowlist entry is never revoked and the user is never re-prompted.
Expected vs actual
- Expected: an approved hook whose on-disk script has changed should require
re-approval before it runs again.
- Actual: trust is keyed on the command string only, so editing the script at
the same path retains approval; the mtime drift is surfaced only if the user
happens to run hooks doctor / hooks list.
Suggested direction
Capture a content hash (for example sha256) of the resolved script at approval
time and include it in the _is_allowlisted gate, so a changed script fails the
gate and triggers a re-prompt. The mtime can stay as the cheap advisory signal.
This report was prepared with AI assistance.
Shell-hook allowlist keys on the command string, so an approved hook script can be edited and stays trusted
Affected versions
Confirmed in 0.18.0 (sdist from PyPI). Same code path is present in 0.17.0.
What happens
The shell-hook allowlist grants trust to an
(event, command)string pair. Theapproval gate never checks the on-disk script's contents, so once a hook is
approved you can edit the script it points at and it keeps running without a
re-prompt. The approval record does store the script mtime at approval time, but
only
hermes hooks list/hermes hooks doctorread it, and they only print anadvisory warning. They do not re-prompt or revoke.
Where
agent/shell_hooks.py:_is_allowlisted(event, command)(lines 664-671) matches only on the storedeventandcommandstrings:_record_approval(lines 746-761) capturesscript_mtime_at_approval, butnothing in the gate consults it.
Registration path (lines 244-268): if
_is_allowlisted(...)is true, the hookis registered with no prompt.
_spawn(around lines 419-517) then runsshlex.split(os.path.expanduser(spec.command))with no content check.hermes_cli/hooks.py:hermes hooks list(lines 88-99) andhermes hooks doctor(lines 347-355)compare
script_mtime_iso(command)againstscript_mtime_at_approvalandprint a "script modified since approval" line. This is advisory only; neither
re-prompts nor removes the allowlist entry.
There is no content hash of the script anywhere in
agent/shell_hooks.py.Minimal repro
pre_tool_call->/path/hook.sh.yat the TTY prompt, or use--accept-hooks). Anentry
{event, command, script_mtime_at_approval, ...}is written to~/.hermes/shell-hooks-allowlist.json./path/hook.shso it does something different. The command string isunchanged; only the file's contents and mtime change.
_is_allowlisted(event, command)returns true, the hook registerswith no prompt, and
_spawnexecutes the edited script with full usercredentials.
hermes hooks doctorprints a mtime-drift warning but the hook keeps firing;the allowlist entry is never revoked and the user is never re-prompted.
Expected vs actual
re-approval before it runs again.
the same path retains approval; the mtime drift is surfaced only if the user
happens to run
hooks doctor/hooks list.Suggested direction
Capture a content hash (for example sha256) of the resolved script at approval
time and include it in the
_is_allowlistedgate, so a changed script fails thegate and triggers a re-prompt. The mtime can stay as the cheap advisory signal.
This report was prepared with AI assistance.