claudekit-cli 4.3.1-dev.16 → 4.3.1-dev.17

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/cli-manifest.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
- "version": "4.3.1-dev.16",
3
- "generatedAt": "2026-05-26T17:24:46.493Z",
2
+ "version": "4.3.1-dev.17",
3
+ "generatedAt": "2026-05-27T03:32:56.469Z",
4
4
  "commands": {
5
5
  "agents": {
6
6
  "name": "agents",
@@ -1188,6 +1188,10 @@
1188
1188
  {
1189
1189
  "flags": "--force-overwrite-settings",
1190
1190
  "description": "Fully replace settings.json instead of selective merge"
1191
+ },
1192
+ {
1193
+ "flags": "--restore-ck-hooks",
1194
+ "description": "Restore CK-managed hook registrations during update self-heal"
1191
1195
  }
1192
1196
  ]
1193
1197
  },
package/dist/index.js CHANGED
@@ -15349,6 +15349,7 @@ var init_commands = __esm(() => {
15349
15349
  dryRun: exports_external.boolean().default(false),
15350
15350
  forceOverwrite: exports_external.boolean().default(false),
15351
15351
  forceOverwriteSettings: exports_external.boolean().default(false),
15352
+ restoreCkHooks: exports_external.boolean().default(false),
15352
15353
  skipSetup: exports_external.boolean().default(false),
15353
15354
  refresh: exports_external.boolean().default(false),
15354
15355
  docsDir: exports_external.string().optional(),
@@ -15891,8 +15892,12 @@ var init_ck_config = __esm(() => {
15891
15892
  CkHooksConfigSchema = exports_external.object({
15892
15893
  "session-init": exports_external.boolean().optional(),
15893
15894
  "subagent-init": exports_external.boolean().optional(),
15895
+ "session-state": exports_external.boolean().optional(),
15896
+ "cook-after-plan-reminder": exports_external.boolean().optional(),
15894
15897
  "descriptive-name": exports_external.boolean().optional(),
15895
15898
  "dev-rules-reminder": exports_external.boolean().optional(),
15899
+ "plan-format-kanban": exports_external.boolean().optional(),
15900
+ "usage-quota-cache-refresh": exports_external.boolean().optional(),
15896
15901
  "usage-context-awareness": exports_external.boolean().optional(),
15897
15902
  "context-tracking": exports_external.boolean().optional(),
15898
15903
  "scout-block": exports_external.boolean().optional(),
@@ -15989,8 +15994,12 @@ var init_ck_config = __esm(() => {
15989
15994
  hooks: {
15990
15995
  "session-init": true,
15991
15996
  "subagent-init": true,
15997
+ "session-state": true,
15998
+ "cook-after-plan-reminder": true,
15992
15999
  "descriptive-name": true,
15993
16000
  "dev-rules-reminder": true,
16001
+ "plan-format-kanban": true,
16002
+ "usage-quota-cache-refresh": true,
15994
16003
  "usage-context-awareness": true,
15995
16004
  "context-tracking": true,
15996
16005
  "scout-block": true,
@@ -16014,8 +16023,12 @@ var init_ck_config = __esm(() => {
16014
16023
  CK_HOOK_NAMES = [
16015
16024
  "session-init",
16016
16025
  "subagent-init",
16026
+ "session-state",
16027
+ "cook-after-plan-reminder",
16017
16028
  "descriptive-name",
16018
16029
  "dev-rules-reminder",
16030
+ "plan-format-kanban",
16031
+ "usage-quota-cache-refresh",
16019
16032
  "usage-context-awareness",
16020
16033
  "context-tracking",
16021
16034
  "scout-block",
@@ -45197,6 +45210,14 @@ var init_open = __esm(() => {
45197
45210
  open_default = open;
45198
45211
  });
45199
45212
 
45213
+ // src/shared/json-content.ts
45214
+ function stripJsonBom(content) {
45215
+ return content.charCodeAt(0) === 65279 ? content.slice(1) : content;
45216
+ }
45217
+ function parseJsonContent(content) {
45218
+ return JSON.parse(stripJsonBom(content));
45219
+ }
45220
+
45200
45221
  // src/domains/config/config-manager.ts
45201
45222
  import { existsSync as existsSync10 } from "node:fs";
45202
45223
  import { mkdir as mkdir6, readFile as readFile7, rename as rename3, rm as rm3, writeFile as writeFile5 } from "node:fs/promises";
@@ -45225,7 +45246,7 @@ class ConfigManager {
45225
45246
  try {
45226
45247
  if (existsSync10(configFile)) {
45227
45248
  const content = await readFile7(configFile, "utf-8");
45228
- const data = JSON.parse(content);
45249
+ const data = parseJsonContent(content);
45229
45250
  ConfigManager.config = ConfigSchema.parse(data);
45230
45251
  logger.debug(`Config loaded from ${configFile}`);
45231
45252
  return ConfigManager.config;
@@ -45279,7 +45300,7 @@ class ConfigManager {
45279
45300
  try {
45280
45301
  if (existsSync10(configPath)) {
45281
45302
  const content = await readFile7(configPath, "utf-8");
45282
- const data = JSON.parse(content);
45303
+ const data = parseJsonContent(content);
45283
45304
  const folders = FoldersConfigSchema.parse(data.paths || data);
45284
45305
  logger.debug(`Project config loaded from ${configPath}`);
45285
45306
  return folders;
@@ -45300,7 +45321,7 @@ class ConfigManager {
45300
45321
  if (existsSync10(configPath)) {
45301
45322
  try {
45302
45323
  const content = await readFile7(configPath, "utf-8");
45303
- existingConfig = JSON.parse(content);
45324
+ existingConfig = parseJsonContent(content);
45304
45325
  } catch (error) {
45305
45326
  logger.debug(`Could not parse existing config, starting fresh: ${error instanceof Error ? error.message : "Unknown error"}`);
45306
45327
  }
@@ -45475,7 +45496,7 @@ class CkConfigManager {
45475
45496
  if (!existsSync11(configPath))
45476
45497
  return null;
45477
45498
  const content = await readFile8(configPath, "utf-8");
45478
- const data = normalizeCkConfigInput(JSON.parse(content));
45499
+ const data = normalizeCkConfigInput(parseJsonContent(content));
45479
45500
  return CkConfigSchema.parse(data);
45480
45501
  } catch (error) {
45481
45502
  logger.warning(`Failed to load config from ${configPath}: ${error instanceof Error ? error.message : "Unknown"}`);
@@ -45542,7 +45563,7 @@ class CkConfigManager {
45542
45563
  if (!existingConfig && existsSync11(configPath)) {
45543
45564
  try {
45544
45565
  const content = await readFile8(configPath, "utf-8");
45545
- const parsed = JSON.parse(content);
45566
+ const parsed = parseJsonContent(content);
45546
45567
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
45547
45568
  existingConfig = normalizeCkConfigInput(parsed);
45548
45569
  }
@@ -45794,6 +45815,12 @@ function repairClaudeNodeCommandPath(cmd, root) {
45794
45815
  const command = formatCanonicalClaudeCommand(nodePrefix, root, relativePath, suffix);
45795
45816
  return { command, changed: command !== cmd, issue: "raw-relative" };
45796
45817
  }
45818
+ const quotedRelativeMatch = cmd.match(/^(node\s+)["'](?:\.\/)?(\.claude[/\\][^"']+)["'](.*)$/);
45819
+ if (quotedRelativeMatch) {
45820
+ const [, nodePrefix, relativePath, suffix] = quotedRelativeMatch;
45821
+ const command = formatCanonicalClaudeCommand(nodePrefix, root, relativePath, suffix);
45822
+ return { command, changed: command !== cmd, issue: "raw-relative" };
45823
+ }
45797
45824
  const embeddedQuotedMatch = cmd.match(/^(node\s+)"(?:\$HOME|\$CLAUDE_PROJECT_DIR|%USERPROFILE%|%CLAUDE_PROJECT_DIR%)[/\\](\.claude[/\\][^"]+)"(.*)$/);
45798
45825
  if (embeddedQuotedMatch) {
45799
45826
  const [, nodePrefix, relativePath, suffix] = embeddedQuotedMatch;
@@ -50122,6 +50149,16 @@ var init_ck_config_schema = __esm(() => {
50122
50149
  default: true,
50123
50150
  description: "SubagentStart hook - injects context to subagents"
50124
50151
  },
50152
+ "session-state": {
50153
+ type: "boolean",
50154
+ default: true,
50155
+ description: "Stop/SubagentStop/PostToolUse hook - persists session state for handoff and statusline context"
50156
+ },
50157
+ "cook-after-plan-reminder": {
50158
+ type: "boolean",
50159
+ default: true,
50160
+ description: "SubagentStop hook - reminds agents to continue implementation after planning"
50161
+ },
50125
50162
  "descriptive-name": {
50126
50163
  type: "boolean",
50127
50164
  default: true,
@@ -50132,6 +50169,16 @@ var init_ck_config_schema = __esm(() => {
50132
50169
  default: true,
50133
50170
  description: "UserPromptSubmit hook - injects dev rules context"
50134
50171
  },
50172
+ "plan-format-kanban": {
50173
+ type: "boolean",
50174
+ default: true,
50175
+ description: "PostToolUse hook - keeps plan kanban metadata synchronized after edits"
50176
+ },
50177
+ "usage-quota-cache-refresh": {
50178
+ type: "boolean",
50179
+ default: true,
50180
+ description: "Lifecycle hook - refreshes cached usage quota information"
50181
+ },
50135
50182
  "usage-context-awareness": {
50136
50183
  type: "boolean",
50137
50184
  default: true,
@@ -63806,7 +63853,7 @@ var package_default;
63806
63853
  var init_package = __esm(() => {
63807
63854
  package_default = {
63808
63855
  name: "claudekit-cli",
63809
- version: "4.3.1-dev.16",
63856
+ version: "4.3.1-dev.17",
63810
63857
  description: "CLI tool for bootstrapping and updating ClaudeKit projects",
63811
63858
  type: "module",
63812
63859
  repository: {
@@ -64290,8 +64337,9 @@ var init_package_manager_runner = __esm(() => {
64290
64337
  import { existsSync as existsSync44, readdirSync as readdirSync7 } from "node:fs";
64291
64338
  import { homedir as homedir40 } from "node:os";
64292
64339
  import { basename as basename23, dirname as dirname28, isAbsolute as isAbsolute10, resolve as resolve32, sep as sep11 } from "node:path";
64293
- function pruneZombieEngineerWirings(settings, hookDir) {
64340
+ function pruneZombieEngineerWirings(settings, hookDir, preserveCommands = new Set) {
64294
64341
  const pruned = [];
64342
+ const normalizedPreserveCommands = new Set(Array.from(preserveCommands).map((command) => normalizeCommand(command)));
64295
64343
  if (!existsSync44(hookDir)) {
64296
64344
  return { settings, pruned };
64297
64345
  }
@@ -64311,14 +64359,14 @@ function pruneZombieEngineerWirings(settings, hookDir) {
64311
64359
  for (const group of groups) {
64312
64360
  if (!("hooks" in group) || !Array.isArray(group.hooks)) {
64313
64361
  const entry = group;
64314
- if (shouldPruneEntry(entry, hookDir, pruned)) {
64362
+ if (shouldPruneEntry(entry, hookDir, pruned, normalizedPreserveCommands)) {
64315
64363
  continue;
64316
64364
  }
64317
64365
  keptGroups.push(group);
64318
64366
  continue;
64319
64367
  }
64320
64368
  const keptHooks = group.hooks.filter((h2) => {
64321
- return !shouldPruneEntry(h2, hookDir, pruned);
64369
+ return !shouldPruneEntry(h2, hookDir, pruned, normalizedPreserveCommands);
64322
64370
  });
64323
64371
  if (keptHooks.length > 0) {
64324
64372
  keptGroups.push({ ...group, hooks: keptHooks });
@@ -64335,13 +64383,16 @@ function pruneZombieEngineerWirings(settings, hookDir) {
64335
64383
  }
64336
64384
  return { settings, pruned };
64337
64385
  }
64338
- function shouldPruneEntry(entry, hookDir, pruned) {
64386
+ function shouldPruneEntry(entry, hookDir, pruned, preserveCommands) {
64339
64387
  if (isLegacyDescriptiveNamePrompt(entry)) {
64340
64388
  pruned.push("legacy-descriptive-name-prompt");
64341
64389
  return true;
64342
64390
  }
64343
64391
  if (entry._origin !== "engineer")
64344
64392
  return false;
64393
+ if (entry.command && preserveCommands.has(normalizeCommand(entry.command))) {
64394
+ return false;
64395
+ }
64345
64396
  const filePath = extractHookFilePath(entry.command, hookDir);
64346
64397
  if (!filePath)
64347
64398
  return false;
@@ -64426,7 +64477,9 @@ function extractHookFilePath(command, hookDir) {
64426
64477
  }
64427
64478
  return null;
64428
64479
  }
64429
- var init_zombie_wirings_pruner = () => {};
64480
+ var init_zombie_wirings_pruner = __esm(() => {
64481
+ init_command_normalizer();
64482
+ });
64430
64483
 
64431
64484
  // src/domains/health-checks/checkers/shared.ts
64432
64485
  function shouldSkipExpensiveOperations2() {
@@ -67391,7 +67444,7 @@ async function readMetadataFile(claudeDir3) {
67391
67444
  return null;
67392
67445
  }
67393
67446
  }
67394
- function buildInitCommand(isGlobal, kit, beta, yes) {
67447
+ function buildInitCommand(isGlobal, kit, beta, yes, restoreCkHooks) {
67395
67448
  const parts = ["ck init"];
67396
67449
  if (isGlobal)
67397
67450
  parts.push("-g");
@@ -67399,6 +67452,8 @@ function buildInitCommand(isGlobal, kit, beta, yes) {
67399
67452
  parts.push(`--kit ${kit}`);
67400
67453
  if (yes)
67401
67454
  parts.push("--yes");
67455
+ if (restoreCkHooks)
67456
+ parts.push("--restore-ck-hooks");
67402
67457
  parts.push("--install-skills");
67403
67458
  if (beta)
67404
67459
  parts.push("--beta");
@@ -67480,6 +67535,36 @@ async function promptKitUpdate(beta, yes, deps) {
67480
67535
  logger.verbose("No ClaudeKit installations detected, skipping kit update prompt");
67481
67536
  return;
67482
67537
  }
67538
+ let forceKitReinstall = false;
67539
+ if (hasLocal && setup.project.path) {
67540
+ try {
67541
+ const missingHookDeps = await findMissingHookDepsFn(setup.project.path);
67542
+ if (missingHookDeps.length > 0) {
67543
+ logger.warning(`Detected ${missingHookDeps.length} local missing hook dependency(ies); reinstalling local kit content`);
67544
+ forceKitReinstall = true;
67545
+ }
67546
+ } catch (error) {
67547
+ logger.verbose(`Local hook dependency self-heal check skipped: ${error instanceof Error ? error.message : "unknown"}`);
67548
+ }
67549
+ try {
67550
+ const countMissingHookRefsFn = deps?.countMissingHookFileReferencesFn ?? countMissingHookFileReferences;
67551
+ const missingHookRefs = await countMissingHookRefsFn(dirname29(setup.project.path));
67552
+ if (missingHookRefs > 0) {
67553
+ logger.warning(`Detected ${missingHookRefs} local broken hook registration(s); reinstalling local kit content`);
67554
+ forceKitReinstall = true;
67555
+ }
67556
+ } catch (error) {
67557
+ logger.verbose(`Local hook registration self-heal check skipped: ${error instanceof Error ? error.message : "unknown"}`);
67558
+ }
67559
+ if (forceKitReinstall && selection.isGlobal) {
67560
+ const kit = localKits[0] || selection.kit;
67561
+ selection = {
67562
+ isGlobal: false,
67563
+ kit,
67564
+ promptMessage: `Update local project ClaudeKit content${kit ? ` (${kit})` : ""}?`
67565
+ };
67566
+ }
67567
+ }
67483
67568
  let kitVersion = selection.kit ? selection.isGlobal ? globalMetadata?.kits?.[selection.kit]?.version : localMetadata?.kits?.[selection.kit]?.version : undefined;
67484
67569
  const isBetaInstalled = isBetaVersion(kitVersion);
67485
67570
  const promptMessage = selection.promptMessage;
@@ -67487,7 +67572,7 @@ async function promptKitUpdate(beta, yes, deps) {
67487
67572
  logger.info(`Current kit version: ${selection.kit}@${kitVersion}`);
67488
67573
  }
67489
67574
  let alreadyAtLatest = false;
67490
- if (yes && selection.kit && kitVersion) {
67575
+ if (yes && selection.kit && kitVersion && !forceKitReinstall) {
67491
67576
  const getTagFn = deps?.getLatestReleaseTagFn ?? fetchLatestReleaseTag;
67492
67577
  const latestTag = await getTagFn(selection.kit, beta || isBetaInstalled);
67493
67578
  if (latestTag && versionsMatch(kitVersion, latestTag)) {
@@ -67503,6 +67588,7 @@ async function promptKitUpdate(beta, yes, deps) {
67503
67588
  if (missingHookDeps.length > 0) {
67504
67589
  logger.warning(`Detected ${missingHookDeps.length} missing hook dependency(ies); reinstalling kit content`);
67505
67590
  alreadyAtLatest = false;
67591
+ forceKitReinstall = true;
67506
67592
  }
67507
67593
  } catch (error) {
67508
67594
  logger.verbose(`Hook dependency self-heal check skipped: ${error instanceof Error ? error.message : "unknown"}`);
@@ -67516,6 +67602,7 @@ async function promptKitUpdate(beta, yes, deps) {
67516
67602
  if (missingHookRefs > 0) {
67517
67603
  logger.warning(`Detected ${missingHookRefs} broken hook registration(s); reinstalling kit content`);
67518
67604
  alreadyAtLatest = false;
67605
+ forceKitReinstall = true;
67519
67606
  if (setup.project.path && selection.isGlobal) {
67520
67607
  selection = {
67521
67608
  isGlobal: false,
@@ -67552,7 +67639,7 @@ async function promptKitUpdate(beta, yes, deps) {
67552
67639
  }
67553
67640
  const useBeta = beta || isBetaInstalled;
67554
67641
  if (yes) {
67555
- const initCmd = buildInitCommand(selection.isGlobal, selection.kit, useBeta, true);
67642
+ const initCmd = buildInitCommand(selection.isGlobal, selection.kit, useBeta, true, forceKitReinstall);
67556
67643
  logger.info(`Running: ${initCmd}`);
67557
67644
  const s = (deps?.spinnerFn ?? de)();
67558
67645
  s.start("Updating ClaudeKit content...");
@@ -67585,6 +67672,8 @@ async function promptKitUpdate(beta, yes, deps) {
67585
67672
  const args = ["init"];
67586
67673
  if (selection.isGlobal)
67587
67674
  args.push("-g");
67675
+ if (forceKitReinstall)
67676
+ args.push("--restore-ck-hooks");
67588
67677
  args.push("--install-skills");
67589
67678
  if (useBeta)
67590
67679
  args.push("--beta");
@@ -67654,17 +67743,19 @@ async function promptMigrateUpdate(deps) {
67654
67743
  } catch {}
67655
67744
  try {
67656
67745
  const cleanupFn = deps?.cleanupMigratedHooksFn ?? (await Promise.resolve().then(() => (init_migrated_hooks_cleanup(), exports_migrated_hooks_cleanup))).cleanupMigratedHooksForProviders;
67657
- const cleanupProviders = allProviders.filter((p) => SAFE_PROVIDER_NAME.test(p));
67658
- const cleanupResults = await cleanupFn(cleanupProviders, { global: isGlobal });
67659
- const cleanupCount = cleanupResults.reduce((total, result) => total + result.hooksPruned + result.filesRemoved + result.registryEntriesRemoved, 0);
67660
- if (cleanupCount > 0) {
67661
- logger.info(`Cleaned up ${cleanupCount} generated-context hook artifact(s)`);
67746
+ const cleanupProviders = allProviders.filter((p) => p !== "claude-code" && SAFE_PROVIDER_NAME.test(p));
67747
+ if (cleanupProviders.length > 0) {
67748
+ const cleanupResults = await cleanupFn(cleanupProviders, { global: isGlobal });
67749
+ const cleanupCount = cleanupResults.reduce((total, result) => total + result.hooksPruned + result.filesRemoved + result.registryEntriesRemoved, 0);
67750
+ if (cleanupCount > 0) {
67751
+ logger.info(`Cleaned up ${cleanupCount} generated-context hook artifact(s)`);
67752
+ }
67753
+ await repairLegacyHookPromptsSafely();
67754
+ await repairHookFileReferencesSafely();
67662
67755
  }
67663
67756
  } catch (error) {
67664
67757
  logger.verbose(`Migrated hook cleanup skipped: ${error instanceof Error ? error.message : "unknown"}`);
67665
67758
  }
67666
- await repairLegacyHookPromptsSafely();
67667
- await repairHookFileReferencesSafely();
67668
67759
  const targets = allProviders.filter((p) => p !== "claude-code");
67669
67760
  if (targets.length === 0) {
67670
67761
  logger.verbose("No migration targets detected, skipping migrate step");
@@ -80723,6 +80814,10 @@ var init_init_command_help = __esm(() => {
80723
80814
  {
80724
80815
  flags: "--force-overwrite-settings",
80725
80816
  description: "Fully replace settings.json instead of selective merge"
80817
+ },
80818
+ {
80819
+ flags: "--restore-ck-hooks",
80820
+ description: "Restore CK-managed hook registrations during update self-heal"
80726
80821
  }
80727
80822
  ]
80728
80823
  },
@@ -103373,7 +103468,7 @@ class InstalledSettingsTracker {
103373
103468
  }
103374
103469
  try {
103375
103470
  const content = await readFile50(ckJsonPath, "utf-8");
103376
- const data = JSON.parse(content);
103471
+ const data = parseJsonContent(content);
103377
103472
  const installed = data.kits?.[this.kitName]?.installedSettings;
103378
103473
  if (installed) {
103379
103474
  return installed;
@@ -103390,7 +103485,7 @@ class InstalledSettingsTracker {
103390
103485
  let data = {};
103391
103486
  if (existsSync66(ckJsonPath)) {
103392
103487
  const content = await readFile50(ckJsonPath, "utf-8");
103393
- data = JSON.parse(content);
103488
+ data = parseJsonContent(content);
103394
103489
  }
103395
103490
  if (!data.kits) {
103396
103491
  data.kits = {};
@@ -103456,6 +103551,7 @@ class SettingsProcessor {
103456
103551
  installingKit;
103457
103552
  cachedVersion = undefined;
103458
103553
  deletionPatterns = [];
103554
+ restoreCkHooks = false;
103459
103555
  zombiePrunerHookDir = null;
103460
103556
  setGlobalFlag(isGlobal) {
103461
103557
  this.isGlobal = isGlobal;
@@ -103463,6 +103559,9 @@ class SettingsProcessor {
103463
103559
  setForceOverwriteSettings(force) {
103464
103560
  this.forceOverwriteSettings = force;
103465
103561
  }
103562
+ setRestoreCkHooks(restore) {
103563
+ this.restoreCkHooks = restore;
103564
+ }
103466
103565
  setProjectDir(dir) {
103467
103566
  this.projectDir = dir;
103468
103567
  this.initTracker();
@@ -103507,6 +103606,7 @@ class SettingsProcessor {
103507
103606
  } else {
103508
103607
  try {
103509
103608
  const parsedSettings = JSON.parse(transformedSource);
103609
+ await this.applyDisabledHookConfig(parsedSettings);
103510
103610
  this.logHookCommandRepair(this.fixHookCommandPaths(parsedSettings), "fresh install");
103511
103611
  await SettingsMerger.writeSettingsFile(destFile, parsedSettings);
103512
103612
  try {
@@ -103536,6 +103636,7 @@ class SettingsProcessor {
103536
103636
  let sourceSettings;
103537
103637
  try {
103538
103638
  sourceSettings = JSON.parse(transformedSourceContent);
103639
+ await this.applyDisabledHookConfig(sourceSettings);
103539
103640
  } catch {
103540
103641
  logger.warning("Failed to parse source settings.json, falling back to overwrite");
103541
103642
  const formattedContent = this.formatJsonContent(transformedSourceContent);
@@ -103558,10 +103659,15 @@ class SettingsProcessor {
103558
103659
  if (this.tracker) {
103559
103660
  installedSettings = await this.tracker.loadInstalledSettings();
103560
103661
  }
103662
+ const mergeInstalledSettings = this.restoreCkHooks ? { ...installedSettings, hooks: [] } : installedSettings;
103561
103663
  const mergeResult = SettingsMerger.merge(sourceSettings, destSettings, {
103562
- installedSettings,
103664
+ installedSettings: mergeInstalledSettings,
103563
103665
  sourceKit: this.installingKit
103564
103666
  });
103667
+ await this.applyDisabledHookConfig(mergeResult.merged);
103668
+ if (this.restoreCkHooks) {
103669
+ logger.info("Restored CK hook registrations while respecting .ck.json hook disables");
103670
+ }
103565
103671
  logger.verbose("Settings merge details", {
103566
103672
  hooksAdded: mergeResult.hooksAdded,
103567
103673
  hooksPreserved: mergeResult.hooksPreserved,
@@ -103591,7 +103697,8 @@ class SettingsProcessor {
103591
103697
  logger.info(`Pruned ${hooksPruned} stale hook(s) referencing deleted files`);
103592
103698
  }
103593
103699
  if (this.zombiePrunerHookDir) {
103594
- const { pruned: zombiePruned } = pruneZombieEngineerWirings(mergeResult.merged, this.zombiePrunerHookDir);
103700
+ const sourceHookCommands = this.collectHookCommands(sourceSettings);
103701
+ const { pruned: zombiePruned } = pruneZombieEngineerWirings(mergeResult.merged, this.zombiePrunerHookDir, sourceHookCommands);
103595
103702
  if (zombiePruned.length > 0) {
103596
103703
  logger.info(`Pruned ${zombiePruned.length} zombie hook entries: ${zombiePruned.join(", ")}`);
103597
103704
  }
@@ -103600,6 +103707,111 @@ class SettingsProcessor {
103600
103707
  logger.success("Merged settings.json (user customizations preserved)");
103601
103708
  await this.injectTeamHooksIfSupported(destFile, mergeResult.merged);
103602
103709
  }
103710
+ async getDisabledHookNames() {
103711
+ if (!this.projectDir) {
103712
+ return new Set;
103713
+ }
103714
+ const disabled = new Set;
103715
+ const addFromConfig = async (configPath) => {
103716
+ const names = await this.readDisabledHookNamesFromConfig(configPath);
103717
+ for (const name2 of names)
103718
+ disabled.add(name2);
103719
+ };
103720
+ try {
103721
+ if (this.isGlobal) {
103722
+ await addFromConfig(join109(this.projectDir, ".ck.json"));
103723
+ } else {
103724
+ await addFromConfig(join109(PathResolver.getGlobalKitDir(), ".ck.json"));
103725
+ await addFromConfig(join109(this.projectDir, ".claude", ".ck.json"));
103726
+ }
103727
+ } catch (error) {
103728
+ logger.debug(`Failed to load .ck.json hook preferences: ${error instanceof Error ? error.message : "unknown"}`);
103729
+ }
103730
+ return disabled;
103731
+ }
103732
+ async readDisabledHookNamesFromConfig(configPath) {
103733
+ if (!await import_fs_extra14.pathExists(configPath))
103734
+ return new Set;
103735
+ const raw2 = parseJsonContent(await import_fs_extra14.readFile(configPath, "utf-8"));
103736
+ const hooks = raw2.hooks;
103737
+ if (!hooks || typeof hooks !== "object")
103738
+ return new Set;
103739
+ return new Set(Object.entries(hooks).filter(([, enabled]) => enabled === false).map(([name2]) => name2));
103740
+ }
103741
+ async applyDisabledHookConfig(settings) {
103742
+ const disabledHooks = await this.getDisabledHookNames();
103743
+ if (disabledHooks.size === 0 || !settings.hooks)
103744
+ return 0;
103745
+ let removed = 0;
103746
+ const hooksRecord = settings.hooks;
103747
+ for (const [eventName, entries] of Object.entries(hooksRecord)) {
103748
+ const filteredEntries = [];
103749
+ for (const entry of entries) {
103750
+ if (Array.isArray(entry.hooks)) {
103751
+ const keptHooks = entry.hooks.filter((hook) => {
103752
+ const command = typeof hook.command === "string" ? hook.command : "";
103753
+ const hookName = this.extractCkHookName(command);
103754
+ if (hookName && disabledHooks.has(hookName)) {
103755
+ removed++;
103756
+ return false;
103757
+ }
103758
+ return true;
103759
+ });
103760
+ if (keptHooks.length > 0) {
103761
+ filteredEntries.push({ ...entry, hooks: keptHooks });
103762
+ }
103763
+ } else {
103764
+ const command = typeof entry.command === "string" ? entry.command : "";
103765
+ const hookName = this.extractCkHookName(command);
103766
+ if (hookName && disabledHooks.has(hookName)) {
103767
+ removed++;
103768
+ continue;
103769
+ }
103770
+ filteredEntries.push(entry);
103771
+ }
103772
+ }
103773
+ if (filteredEntries.length > 0) {
103774
+ hooksRecord[eventName] = filteredEntries;
103775
+ } else {
103776
+ delete hooksRecord[eventName];
103777
+ }
103778
+ }
103779
+ if (Object.keys(hooksRecord).length === 0) {
103780
+ settings.hooks = undefined;
103781
+ }
103782
+ if (removed > 0) {
103783
+ logger.info(`Skipped ${removed} hook registration(s) disabled in .ck.json`);
103784
+ }
103785
+ return removed;
103786
+ }
103787
+ collectHookCommands(settings) {
103788
+ const commands = new Set;
103789
+ if (!settings.hooks)
103790
+ return commands;
103791
+ for (const entries of Object.values(settings.hooks)) {
103792
+ for (const entry of entries) {
103793
+ if ("hooks" in entry && Array.isArray(entry.hooks)) {
103794
+ for (const hook of entry.hooks) {
103795
+ if (typeof hook.command === "string") {
103796
+ commands.add(hook.command);
103797
+ }
103798
+ }
103799
+ continue;
103800
+ }
103801
+ if ("command" in entry && typeof entry.command === "string") {
103802
+ commands.add(entry.command);
103803
+ }
103804
+ }
103805
+ }
103806
+ return commands;
103807
+ }
103808
+ extractCkHookName(command) {
103809
+ if (!command.trim().startsWith("node "))
103810
+ return null;
103811
+ const normalized = command.replace(/\\/g, "/");
103812
+ const match2 = normalized.match(/\/hooks\/([^/"'\s]+)\.(?:cjs|mjs|js)(?:["'\s]|$)/);
103813
+ return match2?.[1] ?? null;
103814
+ }
103603
103815
  migrateDeprecatedMatchers(destSettings, sourceSettings) {
103604
103816
  if (!destSettings.hooks || !sourceSettings.hooks)
103605
103817
  return;
@@ -103992,6 +104204,9 @@ class CopyExecutor {
103992
104204
  setForceOverwriteSettings(force) {
103993
104205
  this.settingsProcessor.setForceOverwriteSettings(force);
103994
104206
  }
104207
+ setRestoreCkHooks(restore) {
104208
+ this.settingsProcessor.setRestoreCkHooks(restore);
104209
+ }
103995
104210
  setDeletions(deletions) {
103996
104211
  this.settingsProcessor.setDeletions(deletions);
103997
104212
  }
@@ -104164,6 +104379,9 @@ class FileMerger {
104164
104379
  setForceOverwriteSettings(force) {
104165
104380
  this.copyExecutor.setForceOverwriteSettings(force);
104166
104381
  }
104382
+ setRestoreCkHooks(restore) {
104383
+ this.copyExecutor.setRestoreCkHooks(restore);
104384
+ }
104167
104385
  setProjectDir(dir) {
104168
104386
  this.copyExecutor.setProjectDir(dir);
104169
104387
  }
@@ -105487,6 +105705,7 @@ async function handleMerge(ctx) {
105487
105705
  }
105488
105706
  merger.setGlobalFlag(ctx.options.global);
105489
105707
  merger.setForceOverwriteSettings(ctx.options.forceOverwriteSettings);
105708
+ merger.setRestoreCkHooks(ctx.options.restoreCkHooks);
105490
105709
  merger.setProjectDir(ctx.resolvedDir);
105491
105710
  merger.setKitName(ctx.kit.name);
105492
105711
  merger.setZombiePrunerHookDir(join120(ctx.claudeDir, "hooks"));
@@ -106974,6 +107193,7 @@ async function resolveOptions(ctx) {
106974
107193
  skipSetup: parsed.skipSetup ?? false,
106975
107194
  forceOverwrite: parsed.forceOverwrite ?? false,
106976
107195
  forceOverwriteSettings: parsed.forceOverwriteSettings ?? false,
107196
+ restoreCkHooks: parsed.restoreCkHooks ?? false,
106977
107197
  dryRun: parsed.dryRun ?? false,
106978
107198
  prefix: parsed.prefix ?? false,
106979
107199
  sync: parsed.sync ?? false,
@@ -108016,7 +108236,7 @@ async function handleSelection(ctx) {
108016
108236
  logger.info("--force has no effect without --yes (the version-match skip only applies in non-interactive mode)");
108017
108237
  }
108018
108238
  const releaseTag = release?.tag_name;
108019
- if (ctx.options.yes && !ctx.options.fresh && !ctx.options.force && releaseTag && !isOfflineMode && !pendingKits?.length) {
108239
+ if (ctx.options.yes && !ctx.options.fresh && !ctx.options.force && !ctx.options.restoreCkHooks && releaseTag && !isOfflineMode && !pendingKits?.length) {
108020
108240
  try {
108021
108241
  const prefix = PathResolver.getPathPrefix(ctx.options.global);
108022
108242
  const claudeDir3 = prefix ? join134(resolvedDir, prefix) : resolvedDir;
@@ -108990,6 +109210,7 @@ function createInitContext(rawOptions, prompts) {
108990
109210
  skipSetup: false,
108991
109211
  forceOverwrite: false,
108992
109212
  forceOverwriteSettings: false,
109213
+ restoreCkHooks: false,
108993
109214
  dryRun: false,
108994
109215
  prefix: false,
108995
109216
  sync: false,
@@ -113083,6 +113304,7 @@ async function handleKitSelection(ctx) {
113083
113304
  dryRun: false,
113084
113305
  forceOverwrite: false,
113085
113306
  forceOverwriteSettings: false,
113307
+ restoreCkHooks: false,
113086
113308
  skipSetup: true,
113087
113309
  refresh: false,
113088
113310
  sync: false,
@@ -116437,7 +116659,7 @@ function registerCommands(cli) {
116437
116659
  }
116438
116660
  await newCommand(options2);
116439
116661
  });
116440
- cli.command("init", "Initialize or update ClaudeKit project (with interactive version selection)").option("--dir <dir>", "Target directory (default: .)").option("--kit <kit>", "Kit to use: engineer, marketing, all, or comma-separated").option("-r, --release <version>", "Skip version selection, use specific version (e.g., latest, v1.0.0)").option("--exclude <pattern>", "Exclude files matching glob pattern (can be used multiple times)").option("--only <pattern>", "Include only files matching glob pattern (can be used multiple times)").option("-g, --global", "Use platform-specific user configuration directory").option("--fresh", "Full reset: remove CK files, replace settings.json and CLAUDE.md, reinstall from scratch").option("--force", "Force reinstall even if already at latest version (use with --yes; re-onboards missing files without full reset)").option("--install-skills", "Install skills dependencies (non-interactive mode)").option("--with-sudo", "Include system packages requiring sudo (Linux: ffmpeg, imagemagick)").option("--prefix", "Add /ck: prefix to all slash commands by moving them to commands/ck/ subdirectory").option("--beta", "Show beta versions in selection prompt").option("--refresh", "Bypass release cache to fetch latest versions from GitHub").option("--dry-run", "Preview changes without applying them (requires --prefix)").option("--force-overwrite", "Override ownership protections and delete user-modified files (requires --prefix)").option("--force-overwrite-settings", "Fully replace settings.json instead of selective merge (destroys user customizations)").option("--skip-setup", "Skip interactive configuration wizard").option("--docs-dir <name>", "Custom docs folder name (default: docs)").option("--plans-dir <name>", "Custom plans folder name (default: plans)").option("-y, --yes", "Non-interactive mode with sensible defaults (skip all prompts)").option("--sync", "Sync config files from upstream with interactive hunk-by-hunk merge").option("--use-git", "Use git clone instead of GitHub API (uses SSH/HTTPS credentials)").option("--archive <path>", "Use local archive file instead of downloading (zip/tar.gz)").option("--kit-path <path>", "Use local kit directory instead of downloading").action(async (options2) => {
116662
+ cli.command("init", "Initialize or update ClaudeKit project (with interactive version selection)").option("--dir <dir>", "Target directory (default: .)").option("--kit <kit>", "Kit to use: engineer, marketing, all, or comma-separated").option("-r, --release <version>", "Skip version selection, use specific version (e.g., latest, v1.0.0)").option("--exclude <pattern>", "Exclude files matching glob pattern (can be used multiple times)").option("--only <pattern>", "Include only files matching glob pattern (can be used multiple times)").option("-g, --global", "Use platform-specific user configuration directory").option("--fresh", "Full reset: remove CK files, replace settings.json and CLAUDE.md, reinstall from scratch").option("--force", "Force reinstall even if already at latest version (use with --yes; re-onboards missing files without full reset)").option("--install-skills", "Install skills dependencies (non-interactive mode)").option("--with-sudo", "Include system packages requiring sudo (Linux: ffmpeg, imagemagick)").option("--prefix", "Add /ck: prefix to all slash commands by moving them to commands/ck/ subdirectory").option("--beta", "Show beta versions in selection prompt").option("--refresh", "Bypass release cache to fetch latest versions from GitHub").option("--dry-run", "Preview changes without applying them (requires --prefix)").option("--force-overwrite", "Override ownership protections and delete user-modified files (requires --prefix)").option("--force-overwrite-settings", "Fully replace settings.json instead of selective merge (destroys user customizations)").option("--restore-ck-hooks", "Restore CK-managed hook registrations during update self-heal").option("--skip-setup", "Skip interactive configuration wizard").option("--docs-dir <name>", "Custom docs folder name (default: docs)").option("--plans-dir <name>", "Custom plans folder name (default: plans)").option("-y, --yes", "Non-interactive mode with sensible defaults (skip all prompts)").option("--sync", "Sync config files from upstream with interactive hunk-by-hunk merge").option("--use-git", "Use git clone instead of GitHub API (uses SSH/HTTPS credentials)").option("--archive <path>", "Use local archive file instead of downloading (zip/tar.gz)").option("--kit-path <path>", "Use local kit directory instead of downloading").action(async (options2) => {
116441
116663
  if (options2.exclude && !Array.isArray(options2.exclude)) {
116442
116664
  options2.exclude = [options2.exclude];
116443
116665
  }