Skip to content

Commit f5f9cee

Browse files
fix(hooks): register hooks under correct Claude Code events (#524)
Story MIS-3.1: Fix Session-Digest Hook Registration - Introduced HOOK_EVENT_MAP for per-hook event routing in installer - Rewrote precompact-session-digest.cjs to stdin protocol - Added code-intel-pretool.cjs to hook copy allowlist - Updated .claude/settings.json with correct 3-event registration - 16/16 tests pass, QA approved, CodeRabbit approved Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 63db79b commit f5f9cee

File tree

9 files changed

+777
-78
lines changed

9 files changed

+777
-78
lines changed

.aios-core/hooks/unified/README.md

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -188,14 +188,44 @@ module.exports = { onMyEvent, getHookConfig };
188188

189189
### Step 2: Register Hook
190190

191+
**IMPORTANT:** Claude Code hooks must be registered as `type: "command"` in `settings.json`. They run as separate processes reading JSON from stdin — NOT as module exports.
192+
191193
```javascript
192-
// .claude/hooks/my-hook.js
194+
// .claude/hooks/my-hook.cjs — Process-based hook (reads stdin)
195+
#!/usr/bin/env node
196+
'use strict';
197+
198+
function readStdin() {
199+
return new Promise((resolve, reject) => {
200+
let data = '';
201+
process.stdin.setEncoding('utf8');
202+
process.stdin.on('data', (chunk) => { data += chunk; });
203+
process.stdin.on('end', () => {
204+
try { resolve(JSON.parse(data)); }
205+
catch (e) { reject(e); }
206+
});
207+
});
208+
}
209+
210+
async function main() {
211+
const input = await readStdin();
212+
const { onMyEvent } = require('../../.aios-core/hooks/unified/runners/my-runner');
213+
await onMyEvent(input);
214+
}
193215

194-
const { onMyEvent } = require('../../.aios-core/hooks/unified/runners/my-runner');
216+
if (require.main === module) {
217+
main().then(() => process.exit(0)).catch(() => process.exit(0));
218+
}
219+
```
195220

196-
module.exports = async (context) => {
197-
return await onMyEvent(context);
198-
};
221+
Then add the mapping to `HOOK_EVENT_MAP` in `packages/installer/src/wizard/ide-config-generator.js`:
222+
223+
```javascript
224+
'my-hook.cjs': {
225+
event: 'PreToolUse', // or UserPromptSubmit, PreCompact, etc.
226+
matcher: null, // or 'Write|Edit' for PreToolUse filtering
227+
timeout: 10,
228+
},
199229
```
200230

201231
### Step 3: Test Hook
@@ -298,10 +328,11 @@ const proModule = require('../../pro/...'); // Fails if pro absent
298328

299329
- **Story GEMINI-INT.8:** Unified Hook Interface (completed)
300330
- **Story MIS-2:** Dead Code Cleanup (restored hooks foundation)
301-
- **Story MIS-3:** Session Digest (PreCompact Hook) ← **CURRENT**
331+
- **Story MIS-3:** Session Digest (PreCompact Hook)
332+
- **Story MIS-3.1:** Fix Session-Digest Hook Registration ← **CURRENT**
302333
- **Story PRO-5:** aios-pro Repository Bootstrap (pro-detector pattern)
303334

304335
---
305336

306337
*Unified Hooks System - AIOS Core*
307-
*Updated: 2026-02-09 - Story MIS-3*
338+
*Updated: 2026-02-26 - Story MIS-3.1*

.aios-core/install-manifest.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
# - SHA256 hashes for change detection
88
# - File types for categorization
99
#
10-
version: 4.4.5
11-
generated_at: "2026-02-25T02:06:56.890Z"
10+
version: 4.4.6
11+
generated_at: "2026-02-26T15:33:49.548Z"
1212
generator: scripts/generate-install-manifest.js
1313
file_count: 1084
1414
files:

.claude/hooks/README.md

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,20 @@ Sistema de governança automática para regras do CLAUDE.md.
55
## Arquitetura
66

77
```
8+
UserPromptSubmit Hooks
9+
└── (all prompts) → synapse-engine.cjs
10+
811
PreToolUse Hooks
912
├── Read → read-protection.py
1013
├── Write|Edit → enforce-architecture-first.py
1114
│ → write-path-validation.py
1215
│ → mind-clone-governance.py
16+
│ → code-intel-pretool.cjs
1317
└── Bash → sql-governance.py
1418
→ slug-validation.py
19+
20+
PreCompact Hooks
21+
└── (manual+auto) → precompact-session-digest.cjs
1522
```
1623

1724
## Hooks Disponíveis
@@ -133,33 +140,51 @@ echo $? # Deve retornar 2 (bloqueado)
133140

134141
## Configuração
135142

136-
Hooks são configurados em `.claude/settings.local.json`:
143+
Hooks são registrados em `.claude/settings.json` (framework, commitado) ou `.claude/settings.local.json` (overrides locais).
144+
145+
**IMPORTANTE:** Claude Code NÃO usa filesystem discovery. Cada hook DEVE ser registrado explicitamente com o evento correto.
146+
147+
### Registro de Hooks JS (.cjs)
148+
149+
| Hook | Evento | Matcher | Descrição |
150+
|------|--------|---------|-----------|
151+
| `synapse-engine.cjs` | `UserPromptSubmit` || SYNAPSE context engine |
152+
| `code-intel-pretool.cjs` | `PreToolUse` | `Write\|Edit` | Code intelligence injection |
153+
| `precompact-session-digest.cjs` | `PreCompact` || Session digest capture |
154+
155+
### Exemplo de Configuração
137156

138157
```json
139158
{
140159
"hooks": {
160+
"UserPromptSubmit": [
161+
{
162+
"hooks": [{ "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/synapse-engine.cjs\"", "timeout": 10 }]
163+
}
164+
],
141165
"PreToolUse": [
142166
{
143-
"matcher": "Read",
144-
"hooks": [
145-
{
146-
"type": "command",
147-
"command": "python3 \"$CLAUDE_PROJECT_DIR/.claude/hooks/read-protection.py\"",
148-
"timeout": 5
149-
}
150-
]
167+
"matcher": "Write|Edit",
168+
"hooks": [{ "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/code-intel-pretool.cjs\"", "timeout": 10 }]
169+
}
170+
],
171+
"PreCompact": [
172+
{
173+
"hooks": [{ "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/precompact-session-digest.cjs\"", "timeout": 10 }]
151174
}
152175
]
153176
}
154177
}
155178
```
156179

180+
O installer (`ide-config-generator.js`) usa `HOOK_EVENT_MAP` para registrar automaticamente cada hook no evento correto durante `npx aios-core install`.
181+
157182
## Manutenção
158183

159184
Para adicionar novo hook:
160185

161-
1. Criar arquivo `.claude/hooks/novo-hook.py`
162-
2. Adicionar entrada em `.claude/settings.local.json`
186+
1. Criar arquivo `.claude/hooks/novo-hook.cjs` (deve ler stdin JSON, mesmo pattern do synapse-engine.cjs)
187+
2. Adicionar mapeamento em `HOOK_EVENT_MAP` no `ide-config-generator.js`
163188
3. Documentar neste README
164189
4. Testar com casos reais
165190

.claude/hooks/precompact-session-digest.cjs

Lines changed: 87 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,45 +2,105 @@
22
/**
33
* Claude Code Hook: PreCompact Session Digest
44
*
5-
* This hook is registered with Claude Code to trigger before context compact.
6-
* It delegates to the unified hook runner in aios-core.
5+
* Registered as PreCompact event — fires before context compaction.
6+
* Reads JSON from stdin (Claude Code hook protocol), delegates to
7+
* the unified hook runner in aios-core.
78
*
8-
* Installation:
9-
* - Claude Code automatically discovers hooks in .claude/hooks/
10-
* - Hook naming: {event}-{name}.js (e.g., precompact-session-digest.js)
9+
* Stdin format (PreCompact):
10+
* {
11+
* "session_id": "abc123",
12+
* "transcript_path": "/path/to/session.jsonl",
13+
* "cwd": "/path/to/project",
14+
* "hook_event_name": "PreCompact",
15+
* "trigger": "auto" | "manual"
16+
* }
1117
*
1218
* @see .aios-core/hooks/unified/runners/precompact-runner.js
1319
* @see Story MIS-3 - Session Digest (PreCompact Hook)
20+
* @see Story MIS-3.1 - Fix Session-Digest Hook Registration
1421
*/
1522

1623
'use strict';
1724

1825
const path = require('path');
1926

20-
// Resolve path to the unified hook runner
27+
// Resolve project root via __dirname (same pattern as synapse-engine.cjs)
28+
// More robust than input.cwd — doesn't depend on external input
2129
const PROJECT_ROOT = path.resolve(__dirname, '..', '..');
22-
const runnerPath = path.join(
23-
PROJECT_ROOT,
24-
'.aios-core',
25-
'hooks',
26-
'unified',
27-
'runners',
28-
'precompact-runner.js',
29-
);
30-
31-
// Load and execute the hook runner
32-
try {
33-
const { onPreCompact } = require(runnerPath);
3430

35-
// Export the hook handler for Claude Code
36-
module.exports = async (context) => {
37-
return await onPreCompact(context);
38-
};
39-
} catch (error) {
40-
console.error('[PreCompact Hook] Failed to load hook runner:', error.message);
31+
/** Safety timeout (ms) — defense-in-depth; Claude Code also manages hook timeout. */
32+
const HOOK_TIMEOUT_MS = 9000;
4133

42-
// Graceful degradation - export no-op function
43-
module.exports = async () => {
44-
console.log('[PreCompact Hook] Hook runner not available, skipping');
34+
/**
35+
* Read all data from stdin as a JSON object.
36+
* Same pattern as synapse-engine.cjs.
37+
* @returns {Promise<object>} Parsed JSON input
38+
*/
39+
function readStdin() {
40+
return new Promise((resolve, reject) => {
41+
let data = '';
42+
process.stdin.setEncoding('utf8');
43+
process.stdin.on('error', (e) => reject(e));
44+
process.stdin.on('data', (chunk) => { data += chunk; });
45+
process.stdin.on('end', () => {
46+
try { resolve(JSON.parse(data)); }
47+
catch (e) { reject(e); }
48+
});
49+
});
50+
}
51+
52+
/** Main hook execution pipeline. */
53+
async function main() {
54+
const input = await readStdin();
55+
56+
// Resolve path to the unified hook runner via __dirname (not input.cwd)
57+
// Same pattern as synapse-engine.cjs — robust against incorrect cwd
58+
const runnerPath = path.join(
59+
PROJECT_ROOT,
60+
'.aios-core',
61+
'hooks',
62+
'unified',
63+
'runners',
64+
'precompact-runner.js',
65+
);
66+
67+
// Build context object expected by onPreCompact
68+
const context = {
69+
sessionId: input.session_id,
70+
projectDir: input.cwd || PROJECT_ROOT,
71+
transcriptPath: input.transcript_path,
72+
trigger: input.trigger || 'auto',
73+
hookEventName: input.hook_event_name || 'PreCompact',
74+
permissionMode: input.permission_mode,
75+
conversation: input,
76+
provider: 'claude',
4577
};
78+
79+
const { onPreCompact } = require(runnerPath);
80+
await onPreCompact(context);
81+
}
82+
83+
/**
84+
* Safely exit the process — no-op inside Jest workers to prevent worker crashes.
85+
* @param {number} code - Exit code
86+
*/
87+
function safeExit(code) {
88+
if (process.env.JEST_WORKER_ID) return;
89+
process.exit(code);
4690
}
91+
92+
/** Entry point runner — sets safety timeout and executes main(). */
93+
function run() {
94+
const timer = setTimeout(() => safeExit(0), HOOK_TIMEOUT_MS);
95+
timer.unref();
96+
main()
97+
.then(() => safeExit(0))
98+
.catch((err) => {
99+
console.error(`[precompact-hook] ${err.message}`);
100+
safeExit(0); // Never block the compact operation
101+
});
102+
}
103+
104+
if (require.main === module) run();
105+
106+
module.exports = { readStdin, main, run, HOOK_TIMEOUT_MS };

.claude/settings.json

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,38 @@
1-
{}
1+
{
2+
"hooks": {
3+
"UserPromptSubmit": [
4+
{
5+
"hooks": [
6+
{
7+
"type": "command",
8+
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/synapse-engine.cjs\"",
9+
"timeout": 10
10+
}
11+
]
12+
}
13+
],
14+
"PreToolUse": [
15+
{
16+
"matcher": "Write|Edit",
17+
"hooks": [
18+
{
19+
"type": "command",
20+
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/code-intel-pretool.cjs\"",
21+
"timeout": 10
22+
}
23+
]
24+
}
25+
],
26+
"PreCompact": [
27+
{
28+
"hooks": [
29+
{
30+
"type": "command",
31+
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/hooks/precompact-session-digest.cjs\"",
32+
"timeout": 10
33+
}
34+
]
35+
}
36+
]
37+
}
38+
}

.claude/settings.local.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,5 @@
1010
"Bash(git push:*)",
1111
"Bash(node -e:*)"
1212
]
13-
},
14-
"hooks": {}
13+
}
1514
}

0 commit comments

Comments
 (0)