chati-dev 4.2.0 → 4.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -83,13 +83,20 @@ Parse the JSON output. The `action` field tells you what to do:
83
83
  | `complete` | Action: Complete |
84
84
  | `error` | Display error, suggest `/chati status` |
85
85
 
86
- **After every action**, display context bracket from JSON `context_bracket`:
87
- ```
88
- FRESH → "Context: FRESH ({remaining}%)Proceeding to {agent}"
89
- MODERATE → "Context: MODERATE ({remaining}%) — Proceeding (context layers reduced)"
90
- DEPLETED "Context: DEPLETED ({remaining}%) Warning: context running low"
91
- CRITICAL "Context: CRITICAL ({remaining}%) Initiating handoff protocol"
92
- ```
86
+ **Internal: track context bracket from JSON `context_bracket` but DO NOT display it to the user.** The bracket is internal telemetry — users should never see "Context: FRESH (90%)" or framework jargon like "Initiating handoff protocol". Speak in natural language about what you are doing, not the internal state.
87
+
88
+ Only mention the bracket when it reaches CRITICAL, and only in plain language. Example: "I'm running low on memory let me wrap up the current step before continuing." Never say "CRITICAL bracket", "PRISM", "L0-L5", "deviation protocol", or other internal framework terms.
89
+
90
+ **Never reveal agent technical names** to the user. Refer to phases naturally:
91
+ - `greenfield-wu` / `brownfield-wu` "let's understand your goals"
92
+ - `brief` → "let's confirm what we're building"
93
+ - `detail` → "let's expand the requirements"
94
+ - `architect` → "let's design the architecture"
95
+ - `ux` → "let's design the user experience"
96
+ - `phases` / `tasks` → "let's break this down into phases"
97
+ - `qa-planning` / `qa-implementation` → "let's review what we have"
98
+ - `dev` → "let's build it"
99
+ - `devops` → "let's deploy it"
93
100
 
94
101
  ---
95
102
 
@@ -135,11 +142,7 @@ The user should experience a smooth start: they describe their project, the orch
135
142
 
136
143
  These agents (greenfield-wu, brownfield-wu, brief) run in the same conversation.
137
144
 
138
- 1. Display model recommendation from JSON `model_info`:
139
- ```
140
- Model recommendation for {agent}: {model} ({upgrade condition})
141
- To switch: /model {model}
142
- ```
145
+ 1. Track model recommendation from JSON `model_info` internally. Do NOT display "Model recommendation for {agent}" or expose internal agent names. If the recommended model is materially different from the current one and the difference will affect quality, mention it briefly in plain language: e.g., "This step works best on Opus — switch with /model opus if you want stronger reasoning." Otherwise stay silent.
143
146
  2. Read the agent file from `agent_file` in the JSON response
144
147
  3. Load its full content and **become** that agent
145
148
  4. Follow the agent's instructions — the user interacts with you directly
@@ -206,17 +209,17 @@ Quality gate passed: {score}%.
206
209
 
207
210
  These agents run in separate Claude Code processes.
208
211
 
209
- 1. Display context bracket status from JSON
212
+ 1. Track context bracket internally do NOT display it.
210
213
  2. Check `handoff_status.valid` from JSON:
211
- - If `false` with missing fields:
214
+ - If `false` with missing fields, present user-facing options in plain language:
212
215
  ```
213
- Context check: FAILED missing: {missing_fields}
214
- 1. Re-run previous agent to regenerate handoff (Recommended)
215
- 2. Continue anyway (risk: missing context)
216
- 3. Manual context injection (provide missing info)
216
+ I noticed some context from the previous step is missing. How should I proceed?
217
+ 1. Redo the previous step to regenerate the missing information (Recommended)
218
+ 2. Continue anyway, with the missing context
219
+ 3. Provide the missing details manually
217
220
  ```
218
- - If warnings only: display warnings, proceed
219
- - If `true`: display "Context check: OK — handoff verified"
221
+ - If warnings only: proceed silently. Only mention the warning if it directly affects what the user just asked for.
222
+ - If `true`: proceed silently. Do NOT announce "Context check: OK".
220
223
  3. Execute `spawn_command` via Bash tool (the full command is in the JSON)
221
224
  4. Wait for JSON output from the spawned process. Handle `status`:
222
225
  - `"complete"` → Present **Completion Options** (see above) → on approval, run `orchestrate advance --agent {name} --score {score}`
@@ -240,7 +243,7 @@ Planning phase agents (detail, architect, ux) run simultaneously.
240
243
 
241
244
  **NOTE (Article XXI):** When `features.agent_teams: true` in config.yaml, the orchestrator MUST attempt `spawn_team` FIRST for GROUP 1 agents (detail, architect, ux) and BUILD pair (dev, qa-implementation). Only fall back to `spawn_parallel` if team spawning is unavailable or fails. The CLI `orchestrate next` command handles this automatically — when teams are enabled, it returns `spawn_team` instead of `spawn_parallel`.
242
245
 
243
- 1. Display: "Spawning parallel group: {agents}"
246
+ 1. Display naturally what is happening using user-facing language. Examples (pick one that fits the phase): "Working on the planning phase now." / "Reviewing the architecture and UX in parallel." NEVER say "spawning parallel group", "spawn_parallel", or list internal agent names like "detail, architect, ux".
244
247
  2. Execute `parallel_spawn_command` via Bash tool
245
248
  3. Parse consolidated JSON output
246
249
  4. Present **Completion Options** (Parallel Group variant) — wait for user choice
@@ -271,7 +274,7 @@ node packages/chati-dev/bin/chati.js orchestrate spawn-team --team-type {plannin
271
274
  ```
272
275
  Parse JSON. If `fallback_required: true` → fall back silently to `spawn_parallel`. Do NOT display error messages.
273
276
 
274
- Display: "Forming {team_type} team: {members}"
277
+ Display naturally what the team is doing using user-facing language. For Planning Team: "Working on the planning — designing the requirements, architecture, and user experience together." For Build Team: "Building this with continuous quality review." NEVER say "team", "spawn_team", "Article XXI", "mailbox", "Planning Team", or list internal agent names.
275
278
 
276
279
  ### Step 2: Spawn Teammates via Agent Tool
277
280
 
@@ -413,13 +416,13 @@ If the Agent tool is unavailable or any sub-agent spawn fails:
413
416
 
414
417
  The user is returning to an active session.
415
418
 
416
- 1. Present `status_summary` from JSON in the user's language
417
- 2. Display context bracket
418
- 3. Offer options:
419
+ 1. Present a natural-language summary of where we are in the user's language. Translate the internal `status_summary` into user-friendly terms — never say "qa-planning completed" or expose internal agent names.
420
+ 2. Track context bracket internally — do NOT display.
421
+ 3. Offer options in plain language (translate internal agent names to user-facing phase descriptions):
419
422
  ```
420
- 1. Continue with {next_agent} (Recommended)
421
- 2. Review last output
422
- 3. View full status (/chati status)
423
+ 1. Continue (Recommended)
424
+ 2. Review what we just produced
425
+ 3. Show me the full project status
423
426
  ```
424
427
 
425
428
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chati-dev",
3
- "version": "4.2.0",
3
+ "version": "4.2.2",
4
4
  "description": "AI-Powered Multi-Agent Orchestration System — Structured vibe coding for Full Stack Development",
5
5
  "type": "module",
6
6
  "bin": {
@@ -166,6 +166,114 @@ function checkBinExists(packageRoot, results) {
166
166
  }
167
167
  }
168
168
 
169
+ /**
170
+ * Validate that every hook referenced in hooks/settings.json exists on disk.
171
+ * Prevents shipping a bundle where settings.json references a hook file that
172
+ * was never copied.
173
+ */
174
+ function checkHooksParity(packageRoot, results) {
175
+ results.checks++;
176
+ const settingsPath = join(packageRoot, 'framework', 'hooks', 'settings.json');
177
+ if (!existsSync(settingsPath)) {
178
+ results.warnings.push('framework/hooks/settings.json not found — skipping hooks parity check.');
179
+ results.passed++;
180
+ return;
181
+ }
182
+
183
+ try {
184
+ const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
185
+ const missing = new Set();
186
+ const hooksDir = join(packageRoot, 'framework', 'hooks');
187
+
188
+ // Walk all hook events and collect referenced .js files
189
+ for (const event of Object.keys(settings.hooks || {})) {
190
+ for (const matcher of settings.hooks[event] || []) {
191
+ for (const hook of matcher.hooks || []) {
192
+ if (hook.type === 'command' && typeof hook.command === 'string') {
193
+ // command format: "node chati.dev/hooks/foo.js"
194
+ const match = hook.command.match(/hooks\/([^\s]+\.js)/);
195
+ if (match) {
196
+ const hookFile = match[1];
197
+ if (!existsSync(join(hooksDir, hookFile))) {
198
+ missing.add(hookFile);
199
+ }
200
+ }
201
+ }
202
+ }
203
+ }
204
+ }
205
+
206
+ if (missing.size === 0) {
207
+ results.passed++;
208
+ } else {
209
+ results.errors.push(`Hooks referenced in settings.json but missing from framework/hooks/: ${[...missing].join(', ')}`);
210
+ }
211
+ } catch (err) {
212
+ results.errors.push(`Failed to parse hooks/settings.json: ${err.message}`);
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Parity check: every file in source chati.dev/ (that belongs to a copied
218
+ * directory) must exist in framework/. Catches bundling bugs where new files
219
+ * are added to the source but not included in the bundle.
220
+ *
221
+ * Only runs when the monorepo source is available (skipped inside the
222
+ * published package since the source is not shipped).
223
+ */
224
+ function checkSourceBundleParity(packageRoot, results) {
225
+ results.checks++;
226
+ const monorepoSource = join(packageRoot, '..', '..', 'chati.dev');
227
+ const bundleDir = join(packageRoot, 'framework');
228
+
229
+ if (!existsSync(monorepoSource)) {
230
+ // Running inside the published package — skip (nothing to compare against)
231
+ results.passed++;
232
+ return;
233
+ }
234
+
235
+ // Collect all files in source, grouped by the top-level directory
236
+ const missing = [];
237
+ const COPIED_DIRS = [
238
+ 'orchestrator', 'agents', 'templates', 'workflows', 'quality-gates',
239
+ 'schemas', 'frameworks', 'intelligence', 'patterns', 'hooks',
240
+ 'domains', 'i18n', 'migrations', 'data', 'context', 'tasks', 'presets',
241
+ ];
242
+ const COPIED_ROOT_FILES = ['constitution.md'];
243
+
244
+ function walk(srcDir, relBase) {
245
+ if (!existsSync(srcDir)) return;
246
+ for (const entry of readdirSync(srcDir, { withFileTypes: true })) {
247
+ const srcPath = join(srcDir, entry.name);
248
+ const relPath = relBase ? `${relBase}/${entry.name}` : entry.name;
249
+ if (entry.isDirectory()) {
250
+ walk(srcPath, relPath);
251
+ } else if (entry.isFile()) {
252
+ const bundlePath = join(bundleDir, relPath);
253
+ if (!existsSync(bundlePath)) missing.push(relPath);
254
+ }
255
+ }
256
+ }
257
+
258
+ for (const file of COPIED_ROOT_FILES) {
259
+ const src = join(monorepoSource, file);
260
+ const bundled = join(bundleDir, file);
261
+ if (existsSync(src) && !existsSync(bundled)) missing.push(file);
262
+ }
263
+
264
+ for (const dir of COPIED_DIRS) {
265
+ walk(join(monorepoSource, dir), dir);
266
+ }
267
+
268
+ if (missing.length === 0) {
269
+ results.passed++;
270
+ } else {
271
+ const sample = missing.slice(0, 10).join(', ');
272
+ const more = missing.length > 10 ? ` (+ ${missing.length - 10} more)` : '';
273
+ results.errors.push(`Files in source chati.dev/ missing from framework/ bundle: ${sample}${more}`);
274
+ }
275
+ }
276
+
169
277
  /**
170
278
  * Compare entity count in registry vs actual files in framework/.
171
279
  */
@@ -201,12 +309,146 @@ function checkEntityCount(packageRoot, results) {
201
309
  }
202
310
  }
203
311
 
312
+ /**
313
+ * v4.2.2: Validate that generateClaudeSettings() produces a settings.json
314
+ * that wires all 10 chati hooks via the canonical hookSpecificOutput schema
315
+ * — at the right events and with paths that resolve to existing files.
316
+ *
317
+ * This catches the v4.2.0/v4.2.1 catastrophe (hooks dormant because the
318
+ * installer never wrote .claude/settings.json) at validation time.
319
+ */
320
+ async function checkClaudeHooksWired(packageRoot, results) {
321
+ results.checks++;
322
+ try {
323
+ const genPath = join(packageRoot, 'src', 'config', 'claude-settings-generator.js');
324
+ if (!existsSync(genPath)) {
325
+ results.errors.push('claude-settings-generator.js missing — Claude hooks would not be wired in installs.');
326
+ return;
327
+ }
328
+
329
+ const mod = await import(genPath);
330
+ if (typeof mod.generateClaudeSettings !== 'function') {
331
+ results.errors.push('claude-settings-generator.js does not export generateClaudeSettings.');
332
+ return;
333
+ }
334
+
335
+ const settings = JSON.parse(mod.generateClaudeSettings());
336
+
337
+ // Verify required events
338
+ const requiredEvents = ['UserPromptSubmit', 'PreToolUse', 'PreCompact'];
339
+ const missingEvents = requiredEvents.filter(e => !settings.hooks?.[e]);
340
+ if (missingEvents.length > 0) {
341
+ results.errors.push(`generateClaudeSettings missing events: ${missingEvents.join(', ')}`);
342
+ return;
343
+ }
344
+
345
+ // Collect all referenced hook commands
346
+ const allCommands = [];
347
+ for (const event of Object.values(settings.hooks)) {
348
+ for (const group of event) {
349
+ for (const hook of group.hooks || []) {
350
+ allCommands.push(hook.command || '');
351
+ }
352
+ }
353
+ }
354
+
355
+ // Verify all 10 chati hooks are referenced
356
+ const requiredHooks = [
357
+ 'license-guard', 'prism-engine', 'model-governance',
358
+ 'read-protection', 'constitution-guard', 'mode-governance',
359
+ 'style-guard', 'undercover-guard', 'team-quality-gate', 'session-digest',
360
+ ];
361
+ const missingHooks = requiredHooks.filter(h => !allCommands.some(c => c.includes(h)));
362
+ if (missingHooks.length > 0) {
363
+ results.errors.push(`generateClaudeSettings missing hook references: ${missingHooks.join(', ')}`);
364
+ return;
365
+ }
366
+
367
+ // Verify each referenced hook file exists in framework/hooks/
368
+ const hooksDir = join(packageRoot, 'framework', 'hooks');
369
+ if (existsSync(hooksDir)) {
370
+ const missingFiles = [];
371
+ for (const cmd of allCommands) {
372
+ const match = cmd.match(/hooks\/([^\s]+\.js)/);
373
+ if (match && !existsSync(join(hooksDir, match[1]))) {
374
+ missingFiles.push(match[1]);
375
+ }
376
+ }
377
+ if (missingFiles.length > 0) {
378
+ results.errors.push(`Claude settings reference hook files missing from bundle: ${missingFiles.join(', ')}`);
379
+ return;
380
+ }
381
+ }
382
+
383
+ results.passed++;
384
+ } catch (err) {
385
+ results.errors.push(`checkClaudeHooksWired failed: ${err.message}`);
386
+ }
387
+ }
388
+
389
+ /**
390
+ * v4.2.2: Verify license-guard is wired for ALL three providers.
391
+ * Without this, an expired trial keeps working forever — exactly the bug
392
+ * the user flagged in v4.2.1 (they could see chati.dev with no license check).
393
+ */
394
+ async function checkLicenseEntryPointAcrossProviders(packageRoot, results) {
395
+ results.checks++;
396
+ try {
397
+ const missing = [];
398
+
399
+ // Claude — settings.json must include license-guard in UserPromptSubmit
400
+ const genPath = join(packageRoot, 'src', 'config', 'claude-settings-generator.js');
401
+ if (existsSync(genPath)) {
402
+ const mod = await import(genPath);
403
+ const settings = JSON.parse(mod.generateClaudeSettings());
404
+ const ups = settings.hooks?.UserPromptSubmit || [];
405
+ const claudeCmds = ups.flatMap(g => (g.hooks || []).map(h => h.command || ''));
406
+ if (!claudeCmds.some(c => c.includes('license-guard'))) {
407
+ missing.push('Claude (UserPromptSubmit)');
408
+ }
409
+ } else {
410
+ missing.push('Claude (generator file missing)');
411
+ }
412
+
413
+ // Gemini — HOOK_MAP must include license-guard at BeforeModel
414
+ const geminiPath = join(packageRoot, 'src', 'config', 'gemini-hooks-generator.js');
415
+ if (existsSync(geminiPath)) {
416
+ const mod = await import(geminiPath);
417
+ if (!mod.HOOK_MAP?.['license-guard'] || mod.HOOK_MAP['license-guard'].event !== 'BeforeModel') {
418
+ missing.push('Gemini (HOOK_MAP missing license-guard at BeforeModel)');
419
+ }
420
+ } else {
421
+ missing.push('Gemini (generator file missing)');
422
+ }
423
+
424
+ // Codex — installer/core.js must write .codex/hooks.json with license-guard
425
+ const corePath = join(packageRoot, 'src', 'installer', 'core.js');
426
+ if (existsSync(corePath)) {
427
+ const coreContent = readFileSync(corePath, 'utf-8');
428
+ if (!coreContent.includes('.codex/hooks.json') || !coreContent.includes("license-guard")) {
429
+ missing.push('Codex (.codex/hooks.json not wired with license-guard in core.js)');
430
+ }
431
+ } else {
432
+ missing.push('Codex (core.js missing)');
433
+ }
434
+
435
+ if (missing.length > 0) {
436
+ results.errors.push(`License enforcement gaps: ${missing.join('; ')}`);
437
+ return;
438
+ }
439
+
440
+ results.passed++;
441
+ } catch (err) {
442
+ results.errors.push(`checkLicenseEntryPointAcrossProviders failed: ${err.message}`);
443
+ }
444
+ }
445
+
204
446
  /**
205
447
  * Validate the npm package completeness.
206
448
  * @param {string} packageRoot - Root of the package (packages/chati-dev/)
207
- * @returns {{ errors: string[], warnings: string[], checks: number, passed: number }}
449
+ * @returns {Promise<{ errors: string[], warnings: string[], checks: number, passed: number }>}
208
450
  */
209
- export function validatePackage(packageRoot) {
451
+ export async function validatePackage(packageRoot) {
210
452
  const results = { errors: [], warnings: [], checks: 0, passed: 0 };
211
453
 
212
454
  checkFrameworkExists(packageRoot, results);
@@ -216,6 +458,10 @@ export function validatePackage(packageRoot) {
216
458
  checkExportsResolve(packageRoot, results);
217
459
  checkBinExists(packageRoot, results);
218
460
  checkEntityCount(packageRoot, results);
461
+ checkHooksParity(packageRoot, results);
462
+ checkSourceBundleParity(packageRoot, results);
463
+ await checkClaudeHooksWired(packageRoot, results);
464
+ await checkLicenseEntryPointAcrossProviders(packageRoot, results);
219
465
 
220
466
  return results;
221
467
  }
@@ -247,11 +493,13 @@ function formatResults(results) {
247
493
  // CLI entry point
248
494
  if (process.argv[1] === fileURLToPath(import.meta.url)) {
249
495
  const packageRoot = join(__dirname, '..');
250
- const results = validatePackage(packageRoot);
251
-
252
- console.log(formatResults(results));
253
-
254
- if (results.errors.length > 0) {
496
+ validatePackage(packageRoot).then(results => {
497
+ console.log(formatResults(results));
498
+ if (results.errors.length > 0) {
499
+ process.exit(1);
500
+ }
501
+ }).catch(err => {
502
+ console.error('validate-package failed:', err);
255
503
  process.exit(1);
256
- }
504
+ });
257
505
  }
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Claude Code settings.json generator.
3
+ *
4
+ * Produces .claude/settings.json content that wires the 10 chati.dev hooks
5
+ * into Claude Code's hook system and applies a baseline permission policy.
6
+ *
7
+ * Background: prior to v4.2.2, the installer wrote hooks to
8
+ * chati.dev/hooks/settings.json — but Claude Code reads .claude/settings.json,
9
+ * not the framework's bundled file. As a result, every hook was dormant for
10
+ * every Claude Code user since the project started. This generator is the fix:
11
+ * it produces the file Claude Code actually loads, registers all 10 hooks at
12
+ * the canonical events, and applies a generic permission policy modeled on
13
+ * production Claude Code setups.
14
+ *
15
+ * Hook output schema (v4.2.2): all PreToolUse hooks now use the canonical
16
+ * `hookSpecificOutput.permissionDecision` field per Anthropic docs at
17
+ * https://code.claude.com/docs/en/hooks.md. The previous `decision` field
18
+ * was silently no-op'd by Claude Code.
19
+ */
20
+
21
+ /**
22
+ * Hook registration map. Each entry is one hook script and the events it
23
+ * should fire on. Source of truth lives in chati.dev/hooks/.
24
+ */
25
+ const HOOK_REGISTRY = [
26
+ // UserPromptSubmit — fires on every user message turn.
27
+ // license-guard MUST be first so license check runs before any other work.
28
+ { name: 'license-guard', event: 'UserPromptSubmit', matcher: '.*' },
29
+ { name: 'prism-engine', event: 'UserPromptSubmit', matcher: '.*' },
30
+ { name: 'model-governance', event: 'UserPromptSubmit', matcher: '.*' },
31
+
32
+ // PreToolUse — fires before each tool call. Matcher targets specific tools.
33
+ { name: 'read-protection', event: 'PreToolUse', matcher: 'Read' },
34
+ { name: 'constitution-guard',event: 'PreToolUse', matcher: 'Bash|Write|Edit' },
35
+ { name: 'mode-governance', event: 'PreToolUse', matcher: 'Write|Edit' },
36
+ { name: 'style-guard', event: 'PreToolUse', matcher: 'Write|Edit|Bash' },
37
+ { name: 'undercover-guard', event: 'PreToolUse', matcher: 'Write|Edit|Bash' },
38
+ { name: 'team-quality-gate', event: 'PreToolUse', matcher: 'Write|Edit' },
39
+
40
+ // PreCompact — fires before context compaction (observational).
41
+ { name: 'session-digest', event: 'PreCompact', matcher: '' },
42
+ ];
43
+
44
+ /**
45
+ * Baseline permission allow list.
46
+ * Modeled on production Claude Code setups: lets the assistant work freely
47
+ * inside the project without prompting for every tool call. Personal MCP
48
+ * permissions are intentionally NOT included — those are user-specific.
49
+ */
50
+ const PERMISSIONS_ALLOW = [
51
+ 'Read(**/*)',
52
+ 'Write(**/*)',
53
+ 'Edit(**/*)',
54
+ 'Bash',
55
+ 'WebFetch',
56
+ 'WebSearch',
57
+ 'Task',
58
+ 'Glob',
59
+ 'Grep',
60
+ 'NotebookEdit',
61
+ 'Skill(*)',
62
+ 'TodoWrite',
63
+ 'AskUserQuestion',
64
+ 'EnterPlanMode',
65
+ 'ExitPlanMode',
66
+ 'KillShell',
67
+ ];
68
+
69
+ /**
70
+ * Baseline permission deny list.
71
+ *
72
+ * Categories:
73
+ * - Catastrophic filesystem: deletes that hit root, mass-format, recursive chmod
74
+ * - Git destructive: force push, hard reset, .git removal, hook bypass
75
+ * - System path writes: /etc, /usr, /System, /bin, /sbin (note `//` prefix
76
+ * means absolute filesystem path, not project-relative)
77
+ * - Secret reads: SSH keys, AWS/GCP credentials, npm tokens, GitHub tokens
78
+ * - Accidental publish: npm/yarn/pnpm/pip/cargo publish commands
79
+ *
80
+ * Path syntax (from Claude Code docs):
81
+ * - `Bash(cmd:*)` matches "cmd" with any args
82
+ * - `~/path/**` expands ~ to home dir, ** matches recursive
83
+ * - `//etc/**` double-slash means absolute filesystem path
84
+ * - `/path/**` single-slash is project-root-relative (NOT absolute)
85
+ */
86
+ const PERMISSIONS_DENY = [
87
+ // Catastrophic filesystem
88
+ 'Bash(rm -rf /)',
89
+ 'Bash(rm -rf /*)',
90
+ 'Bash(rm -rf ~)',
91
+ 'Bash(rm -rf ~/*)',
92
+ 'Bash(rm -rf $HOME)',
93
+ 'Bash(rm -rf $HOME/*)',
94
+ 'Bash(sudo rm -rf:*)',
95
+ 'Bash(mkfs:*)',
96
+ 'Bash(dd if=/dev/zero:*)',
97
+ 'Bash(dd if=/dev/random:*)',
98
+ 'Bash(chmod -R 777 /)',
99
+ 'Bash(chown -R:* /)',
100
+
101
+ // Git destructive
102
+ 'Bash(git push --force:*)',
103
+ 'Bash(git push -f:*)',
104
+ 'Bash(git push --force-with-lease:*)',
105
+ 'Bash(git reset --hard:*)',
106
+ 'Bash(git clean -fd:*)',
107
+ 'Bash(git clean -fdx:*)',
108
+ 'Bash(git clean -fx:*)',
109
+ 'Bash(rm -rf .git)',
110
+ 'Bash(rm -rf .git/*)',
111
+
112
+ // Bypass hooks (always suspect)
113
+ 'Bash(git commit --no-verify:*)',
114
+ 'Bash(git commit -n:*)',
115
+ 'Bash(git commit --no-gpg-sign:*)',
116
+ 'Bash(git rebase --no-verify:*)',
117
+
118
+ // System path writes — `//` prefix = absolute filesystem
119
+ 'Write(//etc/**)',
120
+ 'Write(//usr/**)',
121
+ 'Write(//System/**)',
122
+ 'Write(//bin/**)',
123
+ 'Write(//sbin/**)',
124
+ 'Write(//Library/**)',
125
+ 'Edit(//etc/**)',
126
+ 'Edit(//usr/**)',
127
+ 'Edit(//System/**)',
128
+ 'Edit(//bin/**)',
129
+ 'Edit(//sbin/**)',
130
+ 'Edit(//Library/**)',
131
+
132
+ // Credentials and secrets
133
+ 'Read(~/.ssh/**)',
134
+ 'Read(~/.aws/credentials)',
135
+ 'Read(~/.aws/config)',
136
+ 'Read(~/.gnupg/**)',
137
+ 'Read(~/.config/gh/hosts.yml)',
138
+ 'Read(~/.docker/config.json)',
139
+ 'Read(~/.npmrc)',
140
+ 'Read(~/.pypirc)',
141
+ 'Read(~/.netrc)',
142
+
143
+ // Accidental publish
144
+ 'Bash(npm publish:*)',
145
+ 'Bash(yarn publish:*)',
146
+ 'Bash(pnpm publish:*)',
147
+ 'Bash(pip upload:*)',
148
+ 'Bash(twine upload:*)',
149
+ 'Bash(cargo publish:*)',
150
+ ];
151
+
152
+ /**
153
+ * Group HOOK_REGISTRY entries by event, then by matcher within each event.
154
+ * Returns the shape Claude Code expects under the top-level "hooks" key.
155
+ */
156
+ function buildHooksObject() {
157
+ const events = {};
158
+
159
+ for (const entry of HOOK_REGISTRY) {
160
+ if (!events[entry.event]) events[entry.event] = new Map();
161
+ const matcherGroups = events[entry.event];
162
+ if (!matcherGroups.has(entry.matcher)) {
163
+ matcherGroups.set(entry.matcher, []);
164
+ }
165
+ matcherGroups.get(entry.matcher).push({
166
+ type: 'command',
167
+ command: `node chati.dev/hooks/${entry.name}.js`,
168
+ });
169
+ }
170
+
171
+ const result = {};
172
+ for (const [event, matcherGroups] of Object.entries(events)) {
173
+ result[event] = [];
174
+ for (const [matcher, hooks] of matcherGroups.entries()) {
175
+ const group = { hooks };
176
+ if (matcher) group.matcher = matcher;
177
+ result[event].push(group);
178
+ }
179
+ }
180
+ return result;
181
+ }
182
+
183
+ /**
184
+ * Generate the full settings.json content.
185
+ *
186
+ * @returns {string} JSON string with trailing newline, ready to write to disk.
187
+ */
188
+ export function generateClaudeSettings() {
189
+ const settings = {
190
+ $schema: 'https://json.schemastore.org/claude-code-settings.json',
191
+ permissions: {
192
+ allow: PERMISSIONS_ALLOW,
193
+ deny: PERMISSIONS_DENY,
194
+ defaultMode: 'default',
195
+ },
196
+ hooks: buildHooksObject(),
197
+ };
198
+
199
+ return JSON.stringify(settings, null, 2) + '\n';
200
+ }
201
+
202
+ export {
203
+ HOOK_REGISTRY,
204
+ PERMISSIONS_ALLOW,
205
+ PERMISSIONS_DENY,
206
+ };