diff --git a/docs/HERMES-HOOKS.md b/docs/HERMES-HOOKS.md index 0c7570650..6d9926b19 100644 --- a/docs/HERMES-HOOKS.md +++ b/docs/HERMES-HOOKS.md @@ -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 < { 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);