feat(install): hermes-hooks install module — installer-native delivery

Greptile's review asked for committed coverage and installer parity; this
makes the hooks installable through ECC's own machinery instead of a
copy-paste doc:

- manifests/install-modules.json: new module hermes-hooks (kind hooks,
  targets hermes + openclaw, paths hooks/hermes + docs/HERMES-HOOKS.md,
  light/stable, defaultInstall). Disjoint from hooks-runtime, which stays
  Claude/Cursor/OpenCode/CodeBuddy — the Hermes target gets hooks support
  without inheriting the Claude-format hook graph.
- manifests/install-profiles.json: full profile gains hermes-hooks (validator
  requires full to include every module).
- docs/HERMES-HOOKS.md: install section now leads with
  ./install.sh --target hermes --modules hermes-hooks --enable-hooks; the
  manual block registers from the landed ~/.hermes/hooks/hermes/ paths. The
  installer never edits config.yaml — registration stays operator-owned.
- tests/hermes-hooks.test.js: installer-integration case (14 total) drives
  the real installer into a throwaway HOME and proves the LANDED guard
  blocks an eslint.config edit.

Validated: install-manifests (37 modules), no-personal-paths, markdownlint,
14/14 tests. E2E on a throwaway HOME: module lands both scripts + doc and
the landed guard emits a block decision.
This commit is contained in:
ravitejarenangi committed 2026-09-08 13:03:06 +05:30
1 parent b30a8c782a
commit df77893e0a
4 files changed
+56 -10

No files matched your search

+14 -9
View File
@@ -30,18 +30,23 @@ These two are harness-independent policy:
## Install
Preferred — through the ECC installer (module `hermes-hooks`):
```bash
# 1. Put the scripts anywhere stable (they are referenced by absolute path):
mkdir -p ~/.hermes/hooks
cp hooks/hermes/config-protection.py ~/.hermes/hooks/ecc-config-protection.py
cp hooks/hermes/check-console-log.py ~/.hermes/hooks/ecc-check-console-log.py
chmod +x ~/.hermes/hooks/ecc-*.py
./install.sh --target hermes --modules hermes-hooks --enable-hooks
# or: node scripts/install-apply.js --target hermes --modules hermes-hooks --enable-hooks
```
# 2. Point the commands at the copies you just made. Substitute YOUR absolute
# home path — Hermes resolves hook commands verbatim, without ~ expansion.
HOOK_DIR="$HOME/.hermes/hooks"
This lands `hooks/hermes/*.py` under `~/.hermes/hooks/hermes/` and this guide
under `~/.hermes/docs/`. Then register the hooks (the installer deliberately
never edits your `config.yaml`):
# 3. Append to ~/.hermes/config.yaml (Hermes wants the mapping keyed by event):
```bash
# Scripts are already at ~/.hermes/hooks/hermes/. Point the commands at them —
# Hermes resolves hook commands verbatim, without ~ expansion.
HOOK_DIR="$HOME/.hermes/hooks/hermes"
# Append to ~/.hermes/config.yaml (Hermes wants the mapping keyed by event):
cat >> ~/.hermes/config.yaml <<EOF
hooks:
pre_tool_call:
+17
View File
@@ -106,6 +106,23 @@
"cost": "medium",
"stability": "stable"
},
{
"id": "hermes-hooks",
"kind": "hooks",
"description": "Hermes-native guard hooks (config protection, console.log warning) plus install guide.",
"paths": [
"hooks/hermes",
"docs/HERMES-HOOKS.md"
],
"targets": [
"hermes",
"openclaw"
],
"dependencies": [],
"defaultInstall": true,
"cost": "light",
"stability": "stable"
},
{
"id": "platform-configs",
"kind": "platform",
+2 -1
View File
@@ -98,7 +98,8 @@
"devops-infra",
"machine-learning",
"supply-chain-domain",
"document-processing"
"document-processing",
"hermes-hooks"
]
}
}
+23
View File
@@ -237,5 +237,28 @@ run('unreadable file: skips AND reports on stderr', () => {
assert.ok(/not a regular file|skipping/.test(stderr), 'diagnostic required');
});
// --- installer integration: the hermes-hooks module installs and the landed copy runs ---
run('hermes-hooks module installs via the real installer and the landed guard blocks', () => {
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-mod-'));
const inst = spawnSync('node', ['scripts/install-apply.js', '--target', 'hermes', '--modules', 'hermes-hooks', '--enable-hooks'], {
cwd: repoRoot, encoding: 'utf8', env: { ...process.env, HOME: home }, timeout: 60000,
});
assert.strictEqual(inst.status, 0, 'installer must exit 0');
const landed = path.join(home, '.hermes', 'hooks', 'hermes', 'config-protection.py');
assert.ok(fs.existsSync(landed), 'guard must land under ~/.hermes/hooks/hermes/');
assert.ok(fs.existsSync(path.join(home, '.hermes', 'docs', 'HERMES-HOOKS.md')), 'doc must land');
const t = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-guard-'));
const target = path.join(t, 'eslint.config.mjs');
fs.writeFileSync(target, 'export default {}\n');
const g = spawnSync('python3', [landed], {
input: JSON.stringify({ hook_event_name: 'pre_tool_call', tool_name: 'patch', tool_input: { path: target, old_string: 'a', new_string: 'b' } }),
encoding: 'utf8', timeout: 15000,
});
assert.ok((g.stdout || '').includes('"decision": "block"'), 'landed guard must block');
fs.rmSync(home, { recursive: true, force: true });
fs.rmSync(t, { recursive: true, force: true });
});
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
if (failed > 0) process.exit(1);