@cleocode/adapters 2026.10.21 → 2026.10.22
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.
- package/dist/heavy-command-hook.js +119 -5
- package/dist/heavy-command-hook.js.map +1 -1
- package/dist/index.js +177 -14
- package/dist/index.js.map +2 -2
- package/dist/project-hook-delivery.js.map +1 -1
- package/dist/providers/claude-code/hooks.d.ts +8 -6
- package/dist/providers/claude-code/hooks.d.ts.map +1 -1
- package/dist/providers/shared/heavy-command-hook-delivery.d.ts.map +1 -1
- package/dist/providers/shared/heavy-command-hook-install.d.ts +58 -2
- package/dist/providers/shared/heavy-command-hook-install.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/providers/claude-code/__tests__/stop-dispatch-hook.test.ts +22 -1
- package/src/providers/claude-code/hooks.ts +14 -7
- package/src/providers/shared/__tests__/heavy-command-hook-delivery.test.ts +59 -1
- package/src/providers/shared/__tests__/heavy-command-hook-install.test.ts +17 -0
- package/src/providers/shared/heavy-command-hook-delivery.ts +130 -12
- package/src/providers/shared/heavy-command-hook-install.ts +148 -2
|
@@ -52,14 +52,22 @@ import {
|
|
|
52
52
|
heavyCommandHookObject,
|
|
53
53
|
hookFileVisibleToGit,
|
|
54
54
|
isHeavyHookObject,
|
|
55
|
+
isSessionEndHookObject,
|
|
56
|
+
isStopDispatchHookObject,
|
|
57
|
+
isSubagentStartHookObject,
|
|
55
58
|
isUserHomeDir,
|
|
56
59
|
nonDirectoryAncestor,
|
|
57
60
|
OLDER_CLEO_MARKER_PREFIX,
|
|
58
61
|
OPENCODE_PLUGIN_FILE,
|
|
59
62
|
opencodeHeavyCommandPluginSource,
|
|
63
|
+
sessionEndHookObject,
|
|
64
|
+
stopDispatchHookObject,
|
|
65
|
+
subagentStartHookObject,
|
|
60
66
|
syncClaudeCodeHeavyCommandHook,
|
|
61
67
|
syncCodexHeavyCommandHook,
|
|
68
|
+
syncJsonSessionEndHook,
|
|
62
69
|
syncJsonStopDispatchHook,
|
|
70
|
+
syncJsonSubagentStartHook,
|
|
63
71
|
syncOpencodeHeavyCommandPlugin,
|
|
64
72
|
trackedFileModified,
|
|
65
73
|
} from './heavy-command-hook-install.js';
|
|
@@ -196,12 +204,15 @@ function errorText(err: unknown): string {
|
|
|
196
204
|
|
|
197
205
|
/** Sync one provider; never throws. */
|
|
198
206
|
/**
|
|
199
|
-
* Install CLEO's one Stop hook (T13796): goal tick → ask-enforce (T13420)
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
* the
|
|
203
|
-
*
|
|
204
|
-
*
|
|
207
|
+
* Install CLEO's one Stop hook (T13796): goal tick → ask-enforce (T13420),
|
|
208
|
+
* folding the separate ask-enforce and old session-end Stop hooks in place.
|
|
209
|
+
* Claude Code always keeps it, with the SessionEnd hook that ends the CLEO
|
|
210
|
+
* session when the harness session really ends (T13913; a turn's Stop never
|
|
211
|
+
* does); Codex's rides the heavy-command delivery and is removed when that
|
|
212
|
+
* mode is `off`. Ask-enforce inside it honours `CLEO_ASK_ENFORCE` at run time.
|
|
213
|
+
* For Claude Code it also installs the SubagentStart lean hook (T13667),
|
|
214
|
+
* removed when the heavy-command mode is `off`. Never fails the heavy-command
|
|
215
|
+
* outcome.
|
|
205
216
|
*/
|
|
206
217
|
async function syncStopDispatch(
|
|
207
218
|
provider: 'claude-code' | 'codex',
|
|
@@ -213,6 +224,20 @@ async function syncStopDispatch(
|
|
|
213
224
|
} catch {
|
|
214
225
|
// Fail open: a config the Stop hook cannot be written to keeps the heavy-command result.
|
|
215
226
|
}
|
|
227
|
+
if (provider !== 'claude-code') return;
|
|
228
|
+
try {
|
|
229
|
+
await syncJsonSessionEndHook(target, true);
|
|
230
|
+
} catch {
|
|
231
|
+
// Fail open, as above.
|
|
232
|
+
}
|
|
233
|
+
// T13667: the lean rules for harness-spawned subagents. The level (`off`
|
|
234
|
+
// included) is read at run time, so the hook is installed whenever the
|
|
235
|
+
// heavy-command hook is.
|
|
236
|
+
try {
|
|
237
|
+
await syncJsonSubagentStartHook(target, mode !== 'off');
|
|
238
|
+
} catch {
|
|
239
|
+
// Fail open, as above.
|
|
240
|
+
}
|
|
216
241
|
}
|
|
217
242
|
|
|
218
243
|
/**
|
|
@@ -249,8 +274,11 @@ export async function syncProjectStopDispatch(
|
|
|
249
274
|
if (shared !== null) throw new HeavyHookSharedConfigError(target, shared);
|
|
250
275
|
}
|
|
251
276
|
const status = await syncJsonStopDispatchHook(target, provider, true);
|
|
252
|
-
if (provider === 'claude-code')
|
|
253
|
-
|
|
277
|
+
if (provider === 'claude-code') {
|
|
278
|
+
// T13913: the Stop no longer ends the session; SessionEnd does.
|
|
279
|
+
await syncJsonSessionEndHook(target, true);
|
|
280
|
+
excludeLocalSettingsFromGit(projectDir);
|
|
281
|
+
} else excludeHookFileFromGit(projectDir, CODEX_HOOKS_FILE);
|
|
254
282
|
return { provider, status, target };
|
|
255
283
|
} catch (err) {
|
|
256
284
|
return {
|
|
@@ -364,9 +392,72 @@ export async function syncProjectHeavyCommandHooks(
|
|
|
364
392
|
/** What a JSON hook config holds: CLEO's hook (current or not), or why it cannot say. */
|
|
365
393
|
type JsonHookPresence =
|
|
366
394
|
| { readonly kind: 'absent' }
|
|
367
|
-
| {
|
|
395
|
+
| {
|
|
396
|
+
readonly kind: 'present';
|
|
397
|
+
readonly current: boolean;
|
|
398
|
+
/** Why it is not current, when a companion hook is missing or stale (T13960). */
|
|
399
|
+
readonly stale?: string;
|
|
400
|
+
}
|
|
368
401
|
| { readonly kind: 'unreadable'; readonly error: string };
|
|
369
402
|
|
|
403
|
+
/**
|
|
404
|
+
* The hooks CLEO installs beside the heavy-command hook (T13960): the Stop
|
|
405
|
+
* dispatcher, and for Claude Code the SessionEnd and SubagentStart hooks. A
|
|
406
|
+
* binary updated without a hook re-sync keeps the old set, e.g. a Stop that
|
|
407
|
+
* still ends the session and no SessionEnd, so each is audited too. With the
|
|
408
|
+
* heavy-command mode `off`, Claude Code keeps only its Stop and SessionEnd
|
|
409
|
+
* hooks and Codex keeps none (what sync installs).
|
|
410
|
+
*/
|
|
411
|
+
function companionHooks(
|
|
412
|
+
provider: 'claude-code' | 'codex',
|
|
413
|
+
modeOff = false,
|
|
414
|
+
): ReadonlyArray<readonly [string, Record<string, unknown>, (hook: unknown) => boolean]> {
|
|
415
|
+
const stop = ['Stop', stopDispatchHookObject(provider), isStopDispatchHookObject] as const;
|
|
416
|
+
if (provider === 'codex') return modeOff ? [] : [stop];
|
|
417
|
+
const end = ['SessionEnd', sessionEndHookObject(), isSessionEndHookObject] as const;
|
|
418
|
+
return modeOff
|
|
419
|
+
? [stop, end]
|
|
420
|
+
: [stop, end, ['SubagentStart', subagentStartHookObject(), isSubagentStartHookObject]];
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/** The first companion hook that is missing or differs from this build's, named, or null. */
|
|
424
|
+
function staleCompanion(
|
|
425
|
+
hooks: Record<string, unknown>,
|
|
426
|
+
provider: 'claude-code' | 'codex',
|
|
427
|
+
modeOff = false,
|
|
428
|
+
): string | null {
|
|
429
|
+
for (const [event, desired, isMine] of companionHooks(provider, modeOff)) {
|
|
430
|
+
const groups = hooks[event];
|
|
431
|
+
const mine = (Array.isArray(groups) ? groups : [])
|
|
432
|
+
.flatMap((g) => (isPlainObject(g) && Array.isArray(g.hooks) ? g.hooks : []))
|
|
433
|
+
.filter(isMine);
|
|
434
|
+
if (mine.length === 0) return `its ${event} hook is missing`;
|
|
435
|
+
if (!mine.some((h) => JSON.stringify(h) === JSON.stringify(desired))) {
|
|
436
|
+
return `its ${event} hook was written by another CLEO build`;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* With the heavy-command mode `off`: the first of Claude Code's own Stop and
|
|
444
|
+
* SessionEnd hooks that is missing or stale in `file`, or null (also when the
|
|
445
|
+
* file cannot be read: that is reported elsewhere).
|
|
446
|
+
*/
|
|
447
|
+
function offCompanionStale(file: string): string | null {
|
|
448
|
+
let config: unknown = {};
|
|
449
|
+
try {
|
|
450
|
+
const raw = readFileSync(file, 'utf-8');
|
|
451
|
+
if (raw.trim() !== '') config = JSON.parse(raw);
|
|
452
|
+
} catch (err) {
|
|
453
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
454
|
+
if (code !== 'ENOENT' && code !== 'ENOTDIR') return null;
|
|
455
|
+
}
|
|
456
|
+
if (!isPlainObject(config)) return null;
|
|
457
|
+
const hooks = isPlainObject(config.hooks) ? config.hooks : {};
|
|
458
|
+
return staleCompanion(hooks, 'claude-code', true);
|
|
459
|
+
}
|
|
460
|
+
|
|
370
461
|
/** Find CLEO's heavy-command hook in a JSON hook config (`hooks.PreToolUse[].hooks[]`). */
|
|
371
462
|
function jsonHookPresence(file: string, provider: 'claude-code' | 'codex'): JsonHookPresence {
|
|
372
463
|
let raw: string;
|
|
@@ -396,7 +487,12 @@ function jsonHookPresence(file: string, provider: 'claude-code' | 'codex'): Json
|
|
|
396
487
|
if (!isPlainObject(group) || !Array.isArray(group.hooks)) continue;
|
|
397
488
|
for (const hook of group.hooks) {
|
|
398
489
|
if (!isHeavyHookObject(hook)) continue;
|
|
399
|
-
if (JSON.stringify(hook) === expected)
|
|
490
|
+
if (JSON.stringify(hook) === expected) {
|
|
491
|
+
const stale = isPlainObject(hooks) ? staleCompanion(hooks, provider) : null;
|
|
492
|
+
return stale === null
|
|
493
|
+
? { kind: 'present', current: true }
|
|
494
|
+
: { kind: 'present', current: false, stale };
|
|
495
|
+
}
|
|
400
496
|
found = { kind: 'present', current: false };
|
|
401
497
|
}
|
|
402
498
|
}
|
|
@@ -469,6 +565,17 @@ function inspectProvider(
|
|
|
469
565
|
};
|
|
470
566
|
}
|
|
471
567
|
if (mode === 'off') {
|
|
568
|
+
// T13960: Claude Code keeps its Stop and SessionEnd hooks whatever the mode,
|
|
569
|
+
// so an update without a re-sync must not read as `disabled`.
|
|
570
|
+
const stale = provider === 'claude-code' && detected ? offCompanionStale(target) : null;
|
|
571
|
+
if (stale !== null) {
|
|
572
|
+
return {
|
|
573
|
+
...base,
|
|
574
|
+
state: 'outdated',
|
|
575
|
+
detail: `resources.heavyCommandHook is off, but ${stale} in ${target}`,
|
|
576
|
+
remedy: `run: ${HEAVY_HOOK_FIX_COMMAND}`,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
472
579
|
return presence.kind === 'present'
|
|
473
580
|
? {
|
|
474
581
|
...base,
|
|
@@ -479,11 +586,22 @@ function inspectProvider(
|
|
|
479
586
|
: { ...base, state: 'disabled', detail: 'resources.heavyCommandHook is off' };
|
|
480
587
|
}
|
|
481
588
|
if (presence.kind === 'present') {
|
|
482
|
-
|
|
589
|
+
// A project's own (committed) .codex/hooks.json is the team's: CLEO never
|
|
590
|
+
// writes its Stop entry there either, so a missing or stale companion is
|
|
591
|
+
// not CLEO's to flag (--fix would refuse it); the shared-config checks
|
|
592
|
+
// below still apply.
|
|
593
|
+
const companionOnly =
|
|
594
|
+
presence.stale !== undefined &&
|
|
595
|
+
provider === 'codex' &&
|
|
596
|
+
codexHooksSharedReason(projectDir, gitChecks) !== null;
|
|
597
|
+
if (!presence.current && !companionOnly) {
|
|
483
598
|
return {
|
|
484
599
|
...base,
|
|
485
600
|
state: 'outdated',
|
|
486
|
-
detail:
|
|
601
|
+
detail:
|
|
602
|
+
presence.stale !== undefined
|
|
603
|
+
? `the hook in ${target} is current, but ${presence.stale}`
|
|
604
|
+
: `the hook in ${target} (or a legacy location) was written by another CLEO build`,
|
|
487
605
|
remedy: `run: ${HEAVY_HOOK_FIX_COMMAND}`,
|
|
488
606
|
};
|
|
489
607
|
}
|
|
@@ -463,14 +463,15 @@ export function isAskEnforceHookObject(hook: unknown): boolean {
|
|
|
463
463
|
/** The command CLEO's Stop dispatcher runs (T13796); also its marker. */
|
|
464
464
|
export const STOP_DISPATCH_HOOK_ID = 'cleo hook stop';
|
|
465
465
|
|
|
466
|
-
/** Seconds a Stop dispatch may take: the goal tick (
|
|
466
|
+
/** Seconds a Stop dispatch may take: the goal tick (60 s at most) and ask-enforce. */
|
|
467
467
|
export const STOP_DISPATCH_HOOK_TIMEOUT_SEC = 120;
|
|
468
468
|
|
|
469
469
|
/** Claude Code's Stop command before T13796, which ended the session on its own. */
|
|
470
470
|
export const LEGACY_SESSION_END_STOP_COMMAND = 'cleo session end --quiet # cleo-hook';
|
|
471
471
|
|
|
472
472
|
/**
|
|
473
|
-
* CLEO's one Stop hook object (T13796): goal tick → ask-enforce
|
|
473
|
+
* CLEO's one Stop hook object (T13796): goal tick → ask-enforce. It never ends
|
|
474
|
+
* the session (T13913); {@link sessionEndHookObject} does, on `SessionEnd`.
|
|
474
475
|
* Like the ask-enforce script, it forwards only a JSON answer and always exits
|
|
475
476
|
* 0, so a machine without `cleo` (or with an older one) is never blocked.
|
|
476
477
|
*
|
|
@@ -561,6 +562,151 @@ export async function syncJsonStopDispatchHook(
|
|
|
561
562
|
return result;
|
|
562
563
|
}
|
|
563
564
|
|
|
565
|
+
/** The command CLEO's SubagentStart lean hook runs (T13667); also its marker. */
|
|
566
|
+
export const SUBAGENT_START_HOOK_ID = 'cleo hook subagent-start';
|
|
567
|
+
|
|
568
|
+
/**
|
|
569
|
+
* CLEO's SubagentStart lean hook object (T13667), Claude Code only. Like the
|
|
570
|
+
* Stop dispatcher, it forwards only a JSON answer and always exits 0, so a
|
|
571
|
+
* machine without `cleo` (or with an older one) never breaks a subagent.
|
|
572
|
+
*/
|
|
573
|
+
export function subagentStartHookObject(): Record<string, unknown> {
|
|
574
|
+
const script = `command -v cleo >/dev/null 2>&1 || exit 0; out="$(${SUBAGENT_START_HOOK_ID} --provider claude-code 2>/dev/null)"; case "$out" in "{"*) printf '%s\\n' "$out";; esac; exit 0`;
|
|
575
|
+
return {
|
|
576
|
+
type: 'command',
|
|
577
|
+
command: `${script} ${CLEO_HOOK_MARKER}`,
|
|
578
|
+
timeout: HEAVY_COMMAND_HOOK_TIMEOUT_SEC,
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/** Whether one hook object is CLEO's SubagentStart lean hook. */
|
|
583
|
+
export function isSubagentStartHookObject(hook: unknown): boolean {
|
|
584
|
+
return (
|
|
585
|
+
isPlainObject(hook) &&
|
|
586
|
+
typeof hook.command === 'string' &&
|
|
587
|
+
hook.command.includes(CLEO_HOOK_MARKER) &&
|
|
588
|
+
hook.command.includes(`${SUBAGENT_START_HOOK_ID} `)
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* Add, refresh or (`install: false`) remove CLEO's SubagentStart lean hook
|
|
594
|
+
* (`hooks.SubagentStart`, T13667) through CAAMP's locked atomic writer. Claude
|
|
595
|
+
* Code only: Codex has no SubagentStart event, and CLEO-spawned agents get the
|
|
596
|
+
* rules from the spawn prompt. Only CLEO's own object is touched; a malformed
|
|
597
|
+
* file throws untouched.
|
|
598
|
+
*
|
|
599
|
+
* @param configPath - Claude Code's `.claude/settings.local.json`.
|
|
600
|
+
* @param install - `false` removes the hook.
|
|
601
|
+
* @returns what changed.
|
|
602
|
+
*/
|
|
603
|
+
export function syncJsonSubagentStartHook(
|
|
604
|
+
configPath: string,
|
|
605
|
+
install: boolean,
|
|
606
|
+
): Promise<HeavyHookSyncResult> {
|
|
607
|
+
return syncJsonEventHook(
|
|
608
|
+
configPath,
|
|
609
|
+
'SubagentStart',
|
|
610
|
+
install ? subagentStartHookObject() : null,
|
|
611
|
+
isSubagentStartHookObject,
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/** The command CLEO's SessionEnd hook runs (T13913); also its marker. */
|
|
616
|
+
export const SESSION_END_HOOK_ID = 'cleo hook session-end';
|
|
617
|
+
|
|
618
|
+
/** Seconds the SessionEnd hook may take: `cleo session end`, waited for at most 20 s, then detached. */
|
|
619
|
+
export const SESSION_END_HOOK_TIMEOUT_SEC = 30;
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* CLEO's SessionEnd hook object (T13913), Claude Code only: the harness
|
|
623
|
+
* session really ended, so the CLEO session ends. A turn's Stop no longer
|
|
624
|
+
* does. Prints nothing and always exits 0, so a machine without `cleo` (or
|
|
625
|
+
* with an older one) never breaks the exit.
|
|
626
|
+
*/
|
|
627
|
+
export function sessionEndHookObject(): Record<string, unknown> {
|
|
628
|
+
const script = `command -v cleo >/dev/null 2>&1 || exit 0; ${SESSION_END_HOOK_ID} --provider claude-code >/dev/null 2>&1; exit 0`;
|
|
629
|
+
return {
|
|
630
|
+
type: 'command',
|
|
631
|
+
command: `${script} ${CLEO_HOOK_MARKER}`,
|
|
632
|
+
timeout: SESSION_END_HOOK_TIMEOUT_SEC,
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/** Whether one hook object is CLEO's SessionEnd hook. */
|
|
637
|
+
export function isSessionEndHookObject(hook: unknown): boolean {
|
|
638
|
+
return (
|
|
639
|
+
isPlainObject(hook) &&
|
|
640
|
+
typeof hook.command === 'string' &&
|
|
641
|
+
hook.command.includes(CLEO_HOOK_MARKER) &&
|
|
642
|
+
hook.command.includes(`${SESSION_END_HOOK_ID} `)
|
|
643
|
+
);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* Leave exactly one CLEO SessionEnd hook (T13913) in a `hooks` map, or
|
|
648
|
+
* (`install: false`) none; user hooks stay.
|
|
649
|
+
*
|
|
650
|
+
* @param hooks - The config's `hooks` map (edited in place).
|
|
651
|
+
* @param install - `false` removes it.
|
|
652
|
+
* @returns whether the map changed.
|
|
653
|
+
*/
|
|
654
|
+
export function applySessionEndHook(hooks: Record<string, unknown>, install: boolean): boolean {
|
|
655
|
+
const desired = install ? sessionEndHookObject() : null;
|
|
656
|
+
const placed = placeHeavyHook(hooks, desired, 'SessionEnd', isSessionEndHookObject);
|
|
657
|
+
if (desired === null || placed.found) return placed.changed;
|
|
658
|
+
appendHookEntry(hooks, 'SessionEnd', { hooks: [desired] });
|
|
659
|
+
return true;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* Add, refresh or (`install: false`) remove CLEO's SessionEnd hook
|
|
664
|
+
* (`hooks.SessionEnd`, T13913) through CAAMP's locked atomic writer. Claude
|
|
665
|
+
* Code only: Codex has no session-end event. A malformed file throws
|
|
666
|
+
* untouched.
|
|
667
|
+
*
|
|
668
|
+
* @param configPath - Claude Code's `.claude/settings.local.json`.
|
|
669
|
+
* @param install - `false` removes the hook.
|
|
670
|
+
* @returns what changed.
|
|
671
|
+
*/
|
|
672
|
+
export function syncJsonSessionEndHook(
|
|
673
|
+
configPath: string,
|
|
674
|
+
install: boolean,
|
|
675
|
+
): Promise<HeavyHookSyncResult> {
|
|
676
|
+
return syncJsonEventHook(
|
|
677
|
+
configPath,
|
|
678
|
+
'SessionEnd',
|
|
679
|
+
install ? sessionEndHookObject() : null,
|
|
680
|
+
isSessionEndHookObject,
|
|
681
|
+
);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/** Place one CLEO hook object on one event (null removes it), through CAAMP's locked writer. */
|
|
685
|
+
async function syncJsonEventHook(
|
|
686
|
+
configPath: string,
|
|
687
|
+
event: string,
|
|
688
|
+
desired: Record<string, unknown> | null,
|
|
689
|
+
match: (hook: unknown) => boolean,
|
|
690
|
+
): Promise<HeavyHookSyncResult> {
|
|
691
|
+
const install = desired !== null;
|
|
692
|
+
if (!install && !existsSync(configPath)) return 'unchanged';
|
|
693
|
+
if (install) ensureParentDir(configPath);
|
|
694
|
+
let result: HeavyHookSyncResult = 'unchanged';
|
|
695
|
+
await updateJsonConfigFile(configPath, (config) => {
|
|
696
|
+
if (!install && config.hooks === undefined) return false;
|
|
697
|
+
const hooks = hookMap(config);
|
|
698
|
+
const placed = placeHeavyHook(hooks, desired, event, match);
|
|
699
|
+
if (desired === null || placed.found) {
|
|
700
|
+
if (placed.changed) result = install ? 'updated' : 'removed';
|
|
701
|
+
return placed.changed;
|
|
702
|
+
}
|
|
703
|
+
appendHookEntry(hooks, event, { hooks: [desired] });
|
|
704
|
+
result = 'installed';
|
|
705
|
+
return true;
|
|
706
|
+
});
|
|
707
|
+
return result;
|
|
708
|
+
}
|
|
709
|
+
|
|
564
710
|
/** Claude Code's per-machine settings file, relative to the project. */
|
|
565
711
|
export const CLAUDE_LOCAL_SETTINGS = '.claude/settings.local.json';
|
|
566
712
|
|