The plugin now registers a pre_tool_call hook, but plugin.yaml still listed
only post_tool_call/on_session_end, so `plugin doctor` warned "registration
adds hook 'pre_tool_call' not listed in provides_hooks" and catalog/capability
listings under-reported what the plugin hooks.
Also: README notes calls without a tool_call_id (execute_code RPC) are not
tracked; a single _owner() helper keys both _pre_call and
_recent_test_tracks so the two can't drift; drop the unused `result`
parameter from the post hook (still absorbed by **_); trim the _pre_call
comment.
The pre/post snapshot handshake had two ways to mark a user's existing
file as created, which queues it for deletion when the turn ends:
- Snapshots were keyed by tool_call_id alone. llama.cpp sends one
constant id for every call. When session B's pre hook ran between
session A's pre and post, A consumed B's snapshot. A's
already-existing test_user.py then looked "not in existing", and A
tracked it.
- Every pre_tool_call hook sees the original args. Another plugin's
`modify` directive is merged afterwards, and post_tool_call sees the
final args. A path swapped in by that hook was never snapshotted, so
"not in existing" held for it too. The file was tracked even when the
write was refused.
Snapshots are now keyed by (task_id or session_id, tool_call_id), the
identity both hooks receive from the dispatcher. They store the paths the
call named that did not exist yet, instead of the ones that did. The post
hook tracks a path only if its own snapshot saw it absent and it exists
now. Any path the snapshot did not cover fails closed.
I reproduced both end to end through model_tools.handle_function_call
with a second plugin's modify hook and two interleaved sessions. The
user's file was deleted before this fix and survives after it, and a
file the call really created is still cleaned up. The README hook
contract is updated to match.
(cherry picked from commit 8d0b853981f1ab0775549f4831a3d4f733a46106)
post_tool_call sent write_file/patch paths, every absolute path in a
terminal command AND every path in its output to dg.track(), gated only
on `p.exists()` plus the test_*/tmp_* name rule. Category 'test' is
deleted at age 0 by quick(), which on_session_end runs after every turn,
and directories are rmtree'd. So a user's cron script
$HERMES_HOME/scripts/test_uptime.py the agent merely patched, or a hook
dir $HERMES_HOME/hooks/test_notify it listed (`ls -la`, `find ... -name
'test_*'`), was irreversibly deleted when the turn ended — then the
emptied scripts/ and hooks/ dirs too. The plugin's contract is files
"created during Hermes sessions".
A pre_tool_call hook now snapshots which of the call's argument paths
already exist (keyed by tool_call_id; it never raises, since pre hooks
fail closed; entries expire after an hour if the post hook never
fires). post_tool_call tracks a path only when the call named it, it
did not exist before and exists now. Paths seen only in terminal output
carry no such proof and are no longer tracked; no snapshot means
nothing is tracked. Existing tests now drive the real hook order.
(cherry picked from commit e0e0e1f7450df0c75a8499fe57b63dc46c6d2af2)
The seventeen remaining literals are container-side paths, AF_UNIX socket-path-limit
candidates on darwin, detection needles, guard regexes and guidance text that tells the
model to avoid /tmp. Each carries an inline `no-tmp: ok — <why>` so the reason lives
next to the line; the baseline keeps only a fenced tree listing where a marker would render.
The tracked-item delete path in quick() called shutil.rmtree() on any tracked
directory without consulting the protection list, which only the empty-dir
sweep used. guess_category() files every path under cache/ as "temp", so a
terminal command that merely mentioned $HERMES_HOME/cache tracked the
directory itself, and 7 days later quick() removed cache/ wholesale — taking
cache/terminal (terminal snapshots) with it and breaking every later command
with a mktemp "No such file or directory".
Fix: _is_protected_dir() — a tracked DIRECTORY that is HERMES_HOME itself or
sits under an _EMPTY_DIR_PROTECTED_TOP_LEVEL tree is never tracked by
guess_category(), never listed by dry_run(), and skipped (logged SKIPPED,
entry dropped) by quick(). Files under cache/ still age out as before.
kanban/ (task attachments and workspaces have their own lifecycle) is added to
both _NEVER_TRACK_TOP_LEVEL and _EMPTY_DIR_PROTECTED_TOP_LEVEL; stale pre-fix
"test" entries under it are dropped by the existing re-validation instead of
deleted. The kanban row was first proposed in #80842 (@nicha16).
Fixes#114552
Extends the salvaged #112860 entry (`workspace`) to the whole class: `plans` and
`home` are bootstrapped alongside `workspace` by `hermes_cli/profiles.py::_PROFILE_DIRS`
and listed as user data in `profile_distribution.py::USER_OWNED_EXCLUDE`, so a
`test_*`/`tmp_*` file inside any of them is a user's file, never scratch.
Trims the contributor's five tests to two invariants: the end-to-end
post_tool_call -> on_session_end path (workspace file survives, a root-level
`tmp_scratch.py` control is still removed) and the empty-dir sweep leaving a
directory under `workspace/` alone. Documents the protected trees in the plugin
README and the built-in-plugins page.
Dropped: test_workspace_project_tree_never_tracked, test_quick_keeps_workspace_test_file,
test_root_level_test_file_still_auto_deleted (folded into the E2E test as its control).
Part of #112859.
The original name was cute but non-obvious; disk-cleanup says what it
does. Plugin directory, script, state path, log lines, slash command,
and test module all renamed. No user-visible state exists yet, so no
migration path is needed.
New website page "Built-in Plugins" documents the <repo>/plugins/<name>/
source, how discovery interacts with user/project plugins, the
HERMES_DISABLE_BUNDLED_PLUGINS escape hatch, disk-cleanup's hook
behaviour and deletion rules, and guidance on when a plugin belongs
bundled vs. user-installable. Added to the Features → Core sidebar next
to the main Plugins page, with a cross-reference from plugins.md.