oh-my-customcode 1.1.91 → 1.1.92

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/README.md CHANGED
@@ -249,7 +249,7 @@ oh-my-customcode ships 42 lifecycle hook scripts covering security, drift detect
249
249
 
250
250
  Most hooks are advisory (exit 0) — they warn but never block. A small number of hard-block hooks (e.g. `stage-blocker`, `rule-deletion-guard`) reject the tool call outright (exit 2). oh-my-customcode's own governance rules (`.claude/rules/`) follow an **advisory-first enforcement model**: prompt-based guidance is the default, and a rule is only promoted to a blocking hook after repeated, observed violations. Rules also retire — clauses tied to since-fixed platform bugs or that go unused for two minor releases are wrapped in HTML comments (still readable via the source file, invisible to the agent's context) rather than left to accumulate indefinitely.
251
251
 
252
- The hook source of truth is `.claude/hooks/hooks.json`; `omcustom init` compiles it into the `hooks` block of `.claude/settings.json` (via `src/core/hooks-settings.ts`), which is the file Claude Code actually loads.
252
+ The hook source of truth is `.claude/hooks/hooks.json`; `omcustom init` compiles it into the `hooks` block of `.claude/settings.json` (via `src/core/hooks-settings.ts`), which is the file Claude Code actually loads. Hook commands are anchored as `bash "${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/<path>"`, so hooks keep working when the session's working directory moves to a subdirectory. Contributors: do not hand-edit the `hooks` blocks in settings; edit `hooks.json` and run `bun run sync:hooks` (also run at the end of `bun run build`) to regenerate them. Existing installs need no manual step: `omcustom update` (full or hooks component) and `omcustom init --backup` automatically migrate old relative hook commands in `.claude/settings.local.json`, and custom hooks are left untouched.
253
253
 
254
254
  ---
255
255
 
package/dist/cli/index.js CHANGED
@@ -217,7 +217,7 @@ var init_package = __esm(() => {
217
217
  workspaces: [
218
218
  "packages/*"
219
219
  ],
220
- version: "1.1.91",
220
+ version: "1.1.92",
221
221
  description: "Batteries-included agent harness for Claude Code",
222
222
  type: "module",
223
223
  bin: {
@@ -241,7 +241,8 @@ var init_package = __esm(() => {
241
241
  },
242
242
  scripts: {
243
243
  dev: "bun run src/cli/index.ts",
244
- build: "bun build src/cli/index.ts --outdir dist/cli --target node && bun build src/index.ts --outdir dist --target node && bun run scripts/sync-source-lockfile.ts",
244
+ build: "bun build src/cli/index.ts --outdir dist/cli --target node && bun build src/index.ts --outdir dist --target node && bun run scripts/sync-source-lockfile.ts && bun run scripts/sync-hooks-settings.ts",
245
+ "sync:hooks": "bun run scripts/sync-hooks-settings.ts",
245
246
  test: "bun test tests/ packages/eval-core/",
246
247
  "test:unit": "bun test tests/unit",
247
248
  "test:integration": "bun test tests/integration",
@@ -15615,6 +15616,8 @@ var MESSAGES = {
15615
15616
  "install.entry_md_skipped": "{{entry}} skipped ({{reason}})",
15616
15617
  "install.lockfile_generated": "Lockfile generated ({{files}} files tracked)",
15617
15618
  "install.lockfile_failed": "Failed to generate lockfile: {{error}}",
15619
+ "install.hook_commands_migrated": "Migrated {{rewritten}} hook command(s) in settings.local.json",
15620
+ "install.hook_commands_migration_failed": "Failed to migrate hook commands in settings.local.json: {{error}}",
15618
15621
  "lockfile.not_found": "Lockfile not found: {{path}}",
15619
15622
  "lockfile.invalid_version": "Invalid lockfile version: {{path}}",
15620
15623
  "lockfile.invalid_structure": "Invalid lockfile structure: {{path}}",
@@ -15636,6 +15639,8 @@ var MESSAGES = {
15636
15639
  "update.lockfile_failed": "Failed to regenerate lockfile: {{error}}",
15637
15640
  "update.protected_file_updated": "⟳ Protected file {{file}} in {{component}} updated: {{hint}}",
15638
15641
  "update.namespace_synced": "Namespace synced: {{file}} ({{component}})",
15642
+ "update.hook_commands_migrated": "Migrated {{count}} hook command(s) to CLAUDE_PROJECT_DIR-anchored paths",
15643
+ "update.hook_commands_migration_failed": "Failed to migrate hook commands in {{path}}",
15639
15644
  "config.load_failed": "Failed to load config: {{error}}",
15640
15645
  "config.not_found": "Config not found at {{path}}, using defaults",
15641
15646
  "config.saved": "Config saved to {{path}}",
@@ -15661,6 +15666,8 @@ var MESSAGES = {
15661
15666
  "install.entry_md_skipped": "{{entry}} 건너뜀 ({{reason}})",
15662
15667
  "install.lockfile_generated": "잠금 파일 생성 완료 ({{files}}개 파일 추적)",
15663
15668
  "install.lockfile_failed": "잠금 파일 생성 실패: {{error}}",
15669
+ "install.hook_commands_migrated": "settings.local.json의 훅 명령 {{rewritten}}개를 마이그레이션했습니다",
15670
+ "install.hook_commands_migration_failed": "settings.local.json의 훅 명령 마이그레이션에 실패했습니다: {{error}}",
15664
15671
  "lockfile.not_found": "잠금 파일 없음: {{path}}",
15665
15672
  "lockfile.invalid_version": "잠금 파일 버전 유효하지 않음: {{path}}",
15666
15673
  "lockfile.invalid_structure": "잠금 파일 구조 유효하지 않음: {{path}}",
@@ -15682,6 +15689,8 @@ var MESSAGES = {
15682
15689
  "update.lockfile_failed": "잠금 파일 재생성 실패: {{error}}",
15683
15690
  "update.protected_file_updated": "⟳ 보호 파일 {{file}} ({{component}}) 업데이트됨: {{hint}}",
15684
15691
  "update.namespace_synced": "네임스페이스 동기화: {{file}} ({{component}})",
15692
+ "update.hook_commands_migrated": "CLAUDE_PROJECT_DIR 기준 경로로 훅 명령 {{count}}개를 마이그레이션했습니다",
15693
+ "update.hook_commands_migration_failed": "{{path}}의 훅 명령 마이그레이션에 실패했습니다",
15685
15694
  "config.load_failed": "설정 로드 실패: {{error}}",
15686
15695
  "config.not_found": "{{path}}에 설정 없음, 기본값 사용",
15687
15696
  "config.saved": "설정 저장: {{path}}",
@@ -17349,6 +17358,52 @@ function getDefaultWorkflow() {
17349
17358
  };
17350
17359
  }
17351
17360
 
17361
+ // src/core/hook-command-migration.ts
17362
+ var OLD_HOOK_COMMAND = /^(bash )?(?:\.\/)?\.claude\/hooks\/((?:scripts\/)?[A-Za-z0-9._-]+\.sh)$/;
17363
+ function rewriteRelativeHookCommand(command) {
17364
+ const match = OLD_HOOK_COMMAND.exec(command);
17365
+ if (match === null) {
17366
+ return null;
17367
+ }
17368
+ const interpreter = match[1] ?? "";
17369
+ return `${interpreter}"\${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/${match[2]}"`;
17370
+ }
17371
+ function isRecord(value) {
17372
+ return typeof value === "object" && value !== null && !Array.isArray(value);
17373
+ }
17374
+ function rewriteNode(node, counter) {
17375
+ if (Array.isArray(node)) {
17376
+ return node.map((item) => rewriteNode(item, counter));
17377
+ }
17378
+ if (!isRecord(node)) {
17379
+ return node;
17380
+ }
17381
+ const out = {};
17382
+ for (const [key, value] of Object.entries(node)) {
17383
+ if (key === "command" && typeof value === "string") {
17384
+ const rewritten = rewriteRelativeHookCommand(value);
17385
+ if (rewritten !== null) {
17386
+ counter.count++;
17387
+ out[key] = rewritten;
17388
+ continue;
17389
+ }
17390
+ }
17391
+ out[key] = rewriteNode(value, counter);
17392
+ }
17393
+ return out;
17394
+ }
17395
+ function migrateHookCommands(settings) {
17396
+ if (!isRecord(settings.hooks)) {
17397
+ return { settings, rewritten: 0 };
17398
+ }
17399
+ const counter = { count: 0 };
17400
+ const hooks = rewriteNode(settings.hooks, counter);
17401
+ if (counter.count === 0) {
17402
+ return { settings, rewritten: 0 };
17403
+ }
17404
+ return { settings: { ...settings, hooks }, rewritten: counter.count };
17405
+ }
17406
+
17352
17407
  // src/core/hooks-settings.ts
17353
17408
  init_fs();
17354
17409
  import { dirname as dirname3 } from "node:path";
@@ -18036,6 +18091,32 @@ async function installHooksSettings(targetDir, _options, result) {
18036
18091
  warn("install.hooks_settings_failed", { error: message });
18037
18092
  }
18038
18093
  }
18094
+ var BOM = "\uFEFF";
18095
+ async function migrateRestoredHookCommands(targetDir, result) {
18096
+ const layout = getProviderLayout();
18097
+ const settingsPath = join8(targetDir, layout.rootDir, "settings.local.json");
18098
+ if (!await fileExists(settingsPath)) {
18099
+ return;
18100
+ }
18101
+ try {
18102
+ const raw = await readTextFile(settingsPath);
18103
+ const hasBom = raw.startsWith(BOM);
18104
+ const trailingNewline = raw.endsWith(`
18105
+ `) ? `
18106
+ ` : "";
18107
+ const current = JSON.parse(hasBom ? raw.slice(BOM.length) : raw);
18108
+ const { settings, rewritten } = migrateHookCommands(current);
18109
+ if (rewritten > 0) {
18110
+ const body = `${JSON.stringify(settings, null, 2)}${trailingNewline}`;
18111
+ await writeTextFile(settingsPath, hasBom ? `${BOM}${body}` : body);
18112
+ debug("install.hook_commands_migrated", { rewritten: String(rewritten) });
18113
+ }
18114
+ } catch (err) {
18115
+ const message = err instanceof Error ? err.message : String(err);
18116
+ result.warnings.push(`Failed to migrate hook commands in settings.local.json: ${message}`);
18117
+ warn("install.hook_commands_migration_failed", { error: message });
18118
+ }
18119
+ }
18039
18120
  async function installEntryDocWithTracking(targetDir, options, result) {
18040
18121
  const language = options.language ?? DEFAULT_LANGUAGE2;
18041
18122
  const overwrite = !!(options.force || options.backup);
@@ -18111,6 +18192,7 @@ async function install(options) {
18111
18192
  result.warnings.push(`Failed to restore ${failure.path}: ${failure.reason}`);
18112
18193
  }
18113
18194
  }
18195
+ await migrateRestoredHookCommands(options.targetDir, result);
18114
18196
  await cleanupPreservation(preservation.tempDir);
18115
18197
  }
18116
18198
  await updateInstallConfig(options.targetDir, options, result.installedComponents);
@@ -20948,6 +21030,37 @@ async function backfillStatusLineRefreshInterval(targetDir, options) {
20948
21030
  });
20949
21031
  }
20950
21032
  }
21033
+ async function migrateHookCommandsInSettingsLocal(targetDir, options) {
21034
+ if (options.dryRun) {
21035
+ return;
21036
+ }
21037
+ const layout = getProviderLayout();
21038
+ const settingsPath = join18(targetDir, layout.rootDir, "settings.local.json");
21039
+ if (!await fileExists(settingsPath)) {
21040
+ return;
21041
+ }
21042
+ try {
21043
+ const raw = await readTextFile(settingsPath);
21044
+ const bom = raw.startsWith("\uFEFF") ? "\uFEFF" : "";
21045
+ const parsed = JSON.parse(bom ? raw.slice(bom.length) : raw);
21046
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
21047
+ return;
21048
+ }
21049
+ const { settings, rewritten } = migrateHookCommands(parsed);
21050
+ if (rewritten === 0) {
21051
+ return;
21052
+ }
21053
+ const trailingNewline = raw.endsWith(`
21054
+ `) ? `
21055
+ ` : "";
21056
+ await writeTextFile(settingsPath, `${bom}${JSON.stringify(settings, null, 2)}${trailingNewline}`);
21057
+ info("update.hook_commands_migrated", { count: String(rewritten) });
21058
+ } catch {
21059
+ warn("update.hook_commands_migration_failed", {
21060
+ path: settingsPath
21061
+ });
21062
+ }
21063
+ }
20951
21064
  async function runFullUpdatePostProcessing(options, result, config) {
20952
21065
  const isFullUpdate = !options.components || options.components.length === 0;
20953
21066
  if (isFullUpdate) {
@@ -20960,6 +21073,9 @@ async function runFullUpdatePostProcessing(options, result, config) {
20960
21073
  await backfillStatusLineRefreshInterval(options.targetDir, options);
20961
21074
  }
20962
21075
  }
21076
+ if (isFullUpdate || options.components?.includes("hooks")) {
21077
+ await migrateHookCommandsInSettingsLocal(options.targetDir, options);
21078
+ }
20963
21079
  if (!options.dryRun) {
20964
21080
  config.version = result.newVersion;
20965
21081
  config.lastUpdated = new Date().toISOString();
package/dist/index.js CHANGED
@@ -330,6 +330,8 @@ var MESSAGES = {
330
330
  "install.entry_md_skipped": "{{entry}} skipped ({{reason}})",
331
331
  "install.lockfile_generated": "Lockfile generated ({{files}} files tracked)",
332
332
  "install.lockfile_failed": "Failed to generate lockfile: {{error}}",
333
+ "install.hook_commands_migrated": "Migrated {{rewritten}} hook command(s) in settings.local.json",
334
+ "install.hook_commands_migration_failed": "Failed to migrate hook commands in settings.local.json: {{error}}",
333
335
  "lockfile.not_found": "Lockfile not found: {{path}}",
334
336
  "lockfile.invalid_version": "Invalid lockfile version: {{path}}",
335
337
  "lockfile.invalid_structure": "Invalid lockfile structure: {{path}}",
@@ -351,6 +353,8 @@ var MESSAGES = {
351
353
  "update.lockfile_failed": "Failed to regenerate lockfile: {{error}}",
352
354
  "update.protected_file_updated": "⟳ Protected file {{file}} in {{component}} updated: {{hint}}",
353
355
  "update.namespace_synced": "Namespace synced: {{file}} ({{component}})",
356
+ "update.hook_commands_migrated": "Migrated {{count}} hook command(s) to CLAUDE_PROJECT_DIR-anchored paths",
357
+ "update.hook_commands_migration_failed": "Failed to migrate hook commands in {{path}}",
354
358
  "config.load_failed": "Failed to load config: {{error}}",
355
359
  "config.not_found": "Config not found at {{path}}, using defaults",
356
360
  "config.saved": "Config saved to {{path}}",
@@ -376,6 +380,8 @@ var MESSAGES = {
376
380
  "install.entry_md_skipped": "{{entry}} 건너뜀 ({{reason}})",
377
381
  "install.lockfile_generated": "잠금 파일 생성 완료 ({{files}}개 파일 추적)",
378
382
  "install.lockfile_failed": "잠금 파일 생성 실패: {{error}}",
383
+ "install.hook_commands_migrated": "settings.local.json의 훅 명령 {{rewritten}}개를 마이그레이션했습니다",
384
+ "install.hook_commands_migration_failed": "settings.local.json의 훅 명령 마이그레이션에 실패했습니다: {{error}}",
379
385
  "lockfile.not_found": "잠금 파일 없음: {{path}}",
380
386
  "lockfile.invalid_version": "잠금 파일 버전 유효하지 않음: {{path}}",
381
387
  "lockfile.invalid_structure": "잠금 파일 구조 유효하지 않음: {{path}}",
@@ -397,6 +403,8 @@ var MESSAGES = {
397
403
  "update.lockfile_failed": "잠금 파일 재생성 실패: {{error}}",
398
404
  "update.protected_file_updated": "⟳ 보호 파일 {{file}} ({{component}}) 업데이트됨: {{hint}}",
399
405
  "update.namespace_synced": "네임스페이스 동기화: {{file}} ({{component}})",
406
+ "update.hook_commands_migrated": "CLAUDE_PROJECT_DIR 기준 경로로 훅 명령 {{count}}개를 마이그레이션했습니다",
407
+ "update.hook_commands_migration_failed": "{{path}}의 훅 명령 마이그레이션에 실패했습니다",
400
408
  "config.load_failed": "설정 로드 실패: {{error}}",
401
409
  "config.not_found": "{{path}}에 설정 없음, 기본값 사용",
402
410
  "config.saved": "설정 저장: {{path}}",
@@ -1094,6 +1102,52 @@ async function cleanupPreservation(tempDir) {
1094
1102
  }
1095
1103
  }
1096
1104
 
1105
+ // src/core/hook-command-migration.ts
1106
+ var OLD_HOOK_COMMAND = /^(bash )?(?:\.\/)?\.claude\/hooks\/((?:scripts\/)?[A-Za-z0-9._-]+\.sh)$/;
1107
+ function rewriteRelativeHookCommand(command) {
1108
+ const match = OLD_HOOK_COMMAND.exec(command);
1109
+ if (match === null) {
1110
+ return null;
1111
+ }
1112
+ const interpreter = match[1] ?? "";
1113
+ return `${interpreter}"\${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/${match[2]}"`;
1114
+ }
1115
+ function isRecord(value) {
1116
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1117
+ }
1118
+ function rewriteNode(node, counter) {
1119
+ if (Array.isArray(node)) {
1120
+ return node.map((item) => rewriteNode(item, counter));
1121
+ }
1122
+ if (!isRecord(node)) {
1123
+ return node;
1124
+ }
1125
+ const out = {};
1126
+ for (const [key, value] of Object.entries(node)) {
1127
+ if (key === "command" && typeof value === "string") {
1128
+ const rewritten = rewriteRelativeHookCommand(value);
1129
+ if (rewritten !== null) {
1130
+ counter.count++;
1131
+ out[key] = rewritten;
1132
+ continue;
1133
+ }
1134
+ }
1135
+ out[key] = rewriteNode(value, counter);
1136
+ }
1137
+ return out;
1138
+ }
1139
+ function migrateHookCommands(settings) {
1140
+ if (!isRecord(settings.hooks)) {
1141
+ return { settings, rewritten: 0 };
1142
+ }
1143
+ const counter = { count: 0 };
1144
+ const hooks = rewriteNode(settings.hooks, counter);
1145
+ if (counter.count === 0) {
1146
+ return { settings, rewritten: 0 };
1147
+ }
1148
+ return { settings: { ...settings, hooks }, rewritten: counter.count };
1149
+ }
1150
+
1097
1151
  // src/core/hooks-settings.ts
1098
1152
  init_fs();
1099
1153
  import { dirname as dirname2 } from "node:path";
@@ -2026,6 +2080,32 @@ async function installHooksSettings(targetDir, _options, result) {
2026
2080
  warn("install.hooks_settings_failed", { error: message });
2027
2081
  }
2028
2082
  }
2083
+ var BOM = "\uFEFF";
2084
+ async function migrateRestoredHookCommands(targetDir, result) {
2085
+ const layout = getProviderLayout();
2086
+ const settingsPath = join5(targetDir, layout.rootDir, "settings.local.json");
2087
+ if (!await fileExists(settingsPath)) {
2088
+ return;
2089
+ }
2090
+ try {
2091
+ const raw = await readTextFile(settingsPath);
2092
+ const hasBom = raw.startsWith(BOM);
2093
+ const trailingNewline = raw.endsWith(`
2094
+ `) ? `
2095
+ ` : "";
2096
+ const current = JSON.parse(hasBom ? raw.slice(BOM.length) : raw);
2097
+ const { settings, rewritten } = migrateHookCommands(current);
2098
+ if (rewritten > 0) {
2099
+ const body = `${JSON.stringify(settings, null, 2)}${trailingNewline}`;
2100
+ await writeTextFile(settingsPath, hasBom ? `${BOM}${body}` : body);
2101
+ debug("install.hook_commands_migrated", { rewritten: String(rewritten) });
2102
+ }
2103
+ } catch (err) {
2104
+ const message = err instanceof Error ? err.message : String(err);
2105
+ result.warnings.push(`Failed to migrate hook commands in settings.local.json: ${message}`);
2106
+ warn("install.hook_commands_migration_failed", { error: message });
2107
+ }
2108
+ }
2029
2109
  async function installEntryDocWithTracking(targetDir, options, result) {
2030
2110
  const language = options.language ?? DEFAULT_LANGUAGE;
2031
2111
  const overwrite = !!(options.force || options.backup);
@@ -2101,6 +2181,7 @@ async function install(options) {
2101
2181
  result.warnings.push(`Failed to restore ${failure.path}: ${failure.reason}`);
2102
2182
  }
2103
2183
  }
2184
+ await migrateRestoredHookCommands(options.targetDir, result);
2104
2185
  await cleanupPreservation(preservation.tempDir);
2105
2186
  }
2106
2187
  await updateInstallConfig(options.targetDir, options, result.installedComponents);
@@ -2326,7 +2407,7 @@ var package_default = {
2326
2407
  workspaces: [
2327
2408
  "packages/*"
2328
2409
  ],
2329
- version: "1.1.91",
2410
+ version: "1.1.92",
2330
2411
  description: "Batteries-included agent harness for Claude Code",
2331
2412
  type: "module",
2332
2413
  bin: {
@@ -2350,7 +2431,8 @@ var package_default = {
2350
2431
  },
2351
2432
  scripts: {
2352
2433
  dev: "bun run src/cli/index.ts",
2353
- build: "bun build src/cli/index.ts --outdir dist/cli --target node && bun build src/index.ts --outdir dist --target node && bun run scripts/sync-source-lockfile.ts",
2434
+ build: "bun build src/cli/index.ts --outdir dist/cli --target node && bun build src/index.ts --outdir dist --target node && bun run scripts/sync-source-lockfile.ts && bun run scripts/sync-hooks-settings.ts",
2435
+ "sync:hooks": "bun run scripts/sync-hooks-settings.ts",
2354
2436
  test: "bun test tests/ packages/eval-core/",
2355
2437
  "test:unit": "bun test tests/unit",
2356
2438
  "test:integration": "bun test tests/integration",
@@ -5157,6 +5239,37 @@ async function backfillStatusLineRefreshInterval(targetDir, options) {
5157
5239
  });
5158
5240
  }
5159
5241
  }
5242
+ async function migrateHookCommandsInSettingsLocal(targetDir, options) {
5243
+ if (options.dryRun) {
5244
+ return;
5245
+ }
5246
+ const layout = getProviderLayout();
5247
+ const settingsPath = join7(targetDir, layout.rootDir, "settings.local.json");
5248
+ if (!await fileExists(settingsPath)) {
5249
+ return;
5250
+ }
5251
+ try {
5252
+ const raw = await readTextFile(settingsPath);
5253
+ const bom = raw.startsWith("\uFEFF") ? "\uFEFF" : "";
5254
+ const parsed = JSON.parse(bom ? raw.slice(bom.length) : raw);
5255
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
5256
+ return;
5257
+ }
5258
+ const { settings, rewritten } = migrateHookCommands(parsed);
5259
+ if (rewritten === 0) {
5260
+ return;
5261
+ }
5262
+ const trailingNewline = raw.endsWith(`
5263
+ `) ? `
5264
+ ` : "";
5265
+ await writeTextFile(settingsPath, `${bom}${JSON.stringify(settings, null, 2)}${trailingNewline}`);
5266
+ info("update.hook_commands_migrated", { count: String(rewritten) });
5267
+ } catch {
5268
+ warn("update.hook_commands_migration_failed", {
5269
+ path: settingsPath
5270
+ });
5271
+ }
5272
+ }
5160
5273
  async function runFullUpdatePostProcessing(options, result, config) {
5161
5274
  const isFullUpdate = !options.components || options.components.length === 0;
5162
5275
  if (isFullUpdate) {
@@ -5169,6 +5282,9 @@ async function runFullUpdatePostProcessing(options, result, config) {
5169
5282
  await backfillStatusLineRefreshInterval(options.targetDir, options);
5170
5283
  }
5171
5284
  }
5285
+ if (isFullUpdate || options.components?.includes("hooks")) {
5286
+ await migrateHookCommandsInSettingsLocal(options.targetDir, options);
5287
+ }
5172
5288
  if (!options.dryRun) {
5173
5289
  config.version = result.newVersion;
5174
5290
  config.lastUpdated = new Date().toISOString();
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "workspaces": [
4
4
  "packages/*"
5
5
  ],
6
- "version": "1.1.91",
6
+ "version": "1.1.92",
7
7
  "description": "Batteries-included agent harness for Claude Code",
8
8
  "type": "module",
9
9
  "bin": {
@@ -27,7 +27,8 @@
27
27
  },
28
28
  "scripts": {
29
29
  "dev": "bun run src/cli/index.ts",
30
- "build": "bun build src/cli/index.ts --outdir dist/cli --target node && bun build src/index.ts --outdir dist --target node && bun run scripts/sync-source-lockfile.ts",
30
+ "build": "bun build src/cli/index.ts --outdir dist/cli --target node && bun build src/index.ts --outdir dist --target node && bun run scripts/sync-source-lockfile.ts && bun run scripts/sync-hooks-settings.ts",
31
+ "sync:hooks": "bun run scripts/sync-hooks-settings.ts",
31
32
  "test": "bun test tests/ packages/eval-core/",
32
33
  "test:unit": "bun test tests/unit",
33
34
  "test:integration": "bun test tests/integration",
@@ -7,7 +7,7 @@
7
7
  "hooks": [
8
8
  {
9
9
  "type": "command",
10
- "command": "bash .claude/hooks/scripts/stage-blocker.sh"
10
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/stage-blocker.sh\""
11
11
  }
12
12
  ],
13
13
  "description": "Block Write/Edit tools during plan/verify/compound stages — only allow in implement stage"
@@ -67,7 +67,7 @@
67
67
  "hooks": [
68
68
  {
69
69
  "type": "command",
70
- "command": "bash .claude/hooks/scripts/content-hash-validator.sh"
70
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/content-hash-validator.sh\""
71
71
  }
72
72
  ],
73
73
  "description": "Validate file content hash before Edit — advisory staleness warning"
@@ -77,7 +77,7 @@
77
77
  "hooks": [
78
78
  {
79
79
  "type": "command",
80
- "command": "bash .claude/hooks/scripts/schema-validator.sh"
80
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/schema-validator.sh\""
81
81
  }
82
82
  ],
83
83
  "description": "Schema-based tool input validation — Phase 1 advisory only"
@@ -87,7 +87,7 @@
87
87
  "hooks": [
88
88
  {
89
89
  "type": "command",
90
- "command": "bash .claude/hooks/scripts/rtk-intercept.sh"
90
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/rtk-intercept.sh\""
91
91
  }
92
92
  ],
93
93
  "description": "RTK auto-intercept — transparently rewrites CLI commands through RTK proxy when available (R013 advisory)"
@@ -97,7 +97,7 @@
97
97
  "hooks": [
98
98
  {
99
99
  "type": "command",
100
- "command": "bash .claude/hooks/scripts/destructive-git-guard.sh"
100
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/destructive-git-guard.sh\""
101
101
  }
102
102
  ],
103
103
  "description": "Warn before destructive git commands — advisory recovery guidance (R001/R021)"
@@ -107,7 +107,7 @@
107
107
  "hooks": [
108
108
  {
109
109
  "type": "command",
110
- "command": "bash .claude/hooks/scripts/rule-deletion-guard.sh"
110
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/rule-deletion-guard.sh\""
111
111
  }
112
112
  ],
113
113
  "description": "Block rule file deletion — requires individual user confirmation per rule (R001 safety)"
@@ -121,15 +121,15 @@
121
121
  },
122
122
  {
123
123
  "type": "command",
124
- "command": "bash .claude/hooks/scripts/git-delegation-guard.sh"
124
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/git-delegation-guard.sh\""
125
125
  },
126
126
  {
127
127
  "type": "command",
128
- "command": "bash .claude/hooks/scripts/agent-teams-advisor.sh"
128
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/agent-teams-advisor.sh\""
129
129
  },
130
130
  {
131
131
  "type": "command",
132
- "command": "bash .claude/hooks/scripts/model-escalation-advisor.sh"
132
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/model-escalation-advisor.sh\""
133
133
  }
134
134
  ],
135
135
  "description": "HUD statusline + R010 git delegation guard + R018 Agent Teams advisor on Agent/Task spawn"
@@ -141,7 +141,7 @@
141
141
  "hooks": [
142
142
  {
143
143
  "type": "command",
144
- "command": ".claude/hooks/scripts/omcustom-auto-update.sh"
144
+ "command": "\"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/omcustom-auto-update.sh\""
145
145
  }
146
146
  ],
147
147
  "description": "Interactive oh-my-customcode update check at session start (#752)"
@@ -151,7 +151,7 @@
151
151
  "hooks": [
152
152
  {
153
153
  "type": "command",
154
- "command": "bash .claude/hooks/scripts/session-env-check.sh"
154
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/session-env-check.sh\""
155
155
  }
156
156
  ],
157
157
  "description": "Check codex CLI and Agent Teams availability at session start"
@@ -161,7 +161,7 @@
161
161
  "hooks": [
162
162
  {
163
163
  "type": "command",
164
- "command": "bash .claude/hooks/scripts/stale-todo-scanner.sh"
164
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/stale-todo-scanner.sh\""
165
165
  }
166
166
  ],
167
167
  "description": "Scan TODO.md files for stale items at session start"
@@ -171,7 +171,7 @@
171
171
  "hooks": [
172
172
  {
173
173
  "type": "command",
174
- "command": "bash .claude/hooks/scripts/adaptive-harness-scan.sh"
174
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/adaptive-harness-scan.sh\""
175
175
  }
176
176
  ],
177
177
  "description": "Lightweight project profile staleness check for adaptive harness (#831)"
@@ -181,7 +181,7 @@
181
181
  "hooks": [
182
182
  {
183
183
  "type": "command",
184
- "command": "bash .claude/hooks/scripts/session-autofix.sh"
184
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/session-autofix.sh\""
185
185
  }
186
186
  ],
187
187
  "description": "Auto-detect and fix previous session issues at start (#838)"
@@ -191,7 +191,7 @@
191
191
  "hooks": [
192
192
  {
193
193
  "type": "command",
194
- "command": "bash .claude/hooks/scripts/plugin-cache-check.sh"
194
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/plugin-cache-check.sh\""
195
195
  }
196
196
  ],
197
197
  "description": "Advisory check for plugin cache directories missing node_modules (#1207)"
@@ -201,7 +201,7 @@
201
201
  "hooks": [
202
202
  {
203
203
  "type": "command",
204
- "command": "bash .claude/hooks/scripts/claude-md-reinject.sh"
204
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/claude-md-reinject.sh\""
205
205
  }
206
206
  ],
207
207
  "description": "Re-inject project CLAUDE.md into model context on compact re-entry only — CC already natively auto-loads CLAUDE.md on startup/resume/clear, so re-injecting there is pure duplication (~13.8KB/session); matcher narrowed to \"compact\" to keep the original #1617 purpose (post-compact amnesia defense) without the startup double-injection (#1626)"
@@ -213,7 +213,7 @@
213
213
  "hooks": [
214
214
  {
215
215
  "type": "command",
216
- "command": "bash .claude/hooks/scripts/user-prompt-preprocessor.sh"
216
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/user-prompt-preprocessor.sh\""
217
217
  }
218
218
  ],
219
219
  "description": "Advisory pre-processing of user input — skill matching hints and intent analysis"
@@ -223,7 +223,7 @@
223
223
  "hooks": [
224
224
  {
225
225
  "type": "command",
226
- "command": "bash .claude/hooks/scripts/r007-r008-drift-advisor.sh"
226
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/r007-r008-drift-advisor.sh\""
227
227
  }
228
228
  ],
229
229
  "description": "Proactive R007/R008 drift advisory — checks last assistant turn for identification omission (#1229)"
@@ -233,7 +233,7 @@
233
233
  "hooks": [
234
234
  {
235
235
  "type": "command",
236
- "command": "bash .claude/hooks/scripts/session-autofix-prompt.sh"
236
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/session-autofix-prompt.sh\""
237
237
  }
238
238
  ],
239
239
  "description": "Inject session auto-fix findings into first user prompt (#838)"
@@ -243,7 +243,7 @@
243
243
  "hooks": [
244
244
  {
245
245
  "type": "command",
246
- "command": "bash .claude/hooks/scripts/fail-axis-cause-advisor.sh"
246
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/fail-axis-cause-advisor.sh\""
247
247
  }
248
248
  ],
249
249
  "description": "Advisory on cause-free progress nudges when this session has logged tool failures — delivers via hookSpecificOutput.additionalContext (#1561)"
@@ -259,7 +259,7 @@
259
259
  },
260
260
  {
261
261
  "type": "command",
262
- "command": "bash .claude/hooks/scripts/agent-start-recorder.sh"
262
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/agent-start-recorder.sh\""
263
263
  }
264
264
  ],
265
265
  "description": "HUD display + agent start time recording for stall detection (R009)"
@@ -271,23 +271,23 @@
271
271
  "hooks": [
272
272
  {
273
273
  "type": "command",
274
- "command": "bash .claude/hooks/scripts/task-outcome-recorder.sh"
274
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/task-outcome-recorder.sh\""
275
275
  },
276
276
  {
277
277
  "type": "command",
278
- "command": "bash .claude/hooks/scripts/stall-detection-advisor.sh"
278
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/stall-detection-advisor.sh\""
279
279
  },
280
280
  {
281
281
  "type": "command",
282
- "command": "bash .claude/hooks/scripts/auto-continue-guard.sh"
282
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/auto-continue-guard.sh\""
283
283
  },
284
284
  {
285
285
  "type": "command",
286
- "command": "bash .claude/hooks/scripts/r007-r008-drift-advisor.sh"
286
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/r007-r008-drift-advisor.sh\""
287
287
  },
288
288
  {
289
289
  "type": "command",
290
- "command": "bash .claude/hooks/scripts/subagent-failure-advisor.sh"
290
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/subagent-failure-advisor.sh\""
291
291
  }
292
292
  ],
293
293
  "description": "Record agent outcomes + auto-continue workflow + R007/R008 drift advisory + failure advisory on subagent completion (autonomous-loop re-entry, #1545; #1631 replaced the self-referential prompt-type judge hook with an advisory-only command hook)"
@@ -299,7 +299,7 @@
299
299
  "hooks": [
300
300
  {
301
301
  "type": "command",
302
- "command": "bash .claude/hooks/scripts/cwd-change-detector.sh"
302
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/cwd-change-detector.sh\""
303
303
  }
304
304
  ],
305
305
  "description": "Auto-detect project type on working directory change (v2.1.83+)"
@@ -311,7 +311,7 @@
311
311
  "hooks": [
312
312
  {
313
313
  "type": "command",
314
- "command": "bash .claude/hooks/scripts/file-change-validator.sh"
314
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/file-change-validator.sh\""
315
315
  }
316
316
  ],
317
317
  "description": "Validate externally modified files — advisory staleness warning (v2.1.83+)"
@@ -371,7 +371,7 @@
371
371
  "hooks": [
372
372
  {
373
373
  "type": "command",
374
- "command": "bash .claude/hooks/scripts/task-state-precompact.sh"
374
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/task-state-precompact.sh\""
375
375
  }
376
376
  ],
377
377
  "description": "Serialize active task state before compaction — pairs with PostCompact restore (#922)"
@@ -469,7 +469,7 @@
469
469
  "hooks": [
470
470
  {
471
471
  "type": "command",
472
- "command": "bash .claude/hooks/scripts/context-budget-advisor.sh",
472
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/context-budget-advisor.sh\"",
473
473
  "continueOnBlock": true
474
474
  }
475
475
  ],
@@ -480,7 +480,7 @@
480
480
  "hooks": [
481
481
  {
482
482
  "type": "command",
483
- "command": "bash .claude/hooks/scripts/r007-r008-drift-advisor.sh",
483
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/r007-r008-drift-advisor.sh\"",
484
484
  "continueOnBlock": true
485
485
  }
486
486
  ],
@@ -491,7 +491,7 @@
491
491
  "hooks": [
492
492
  {
493
493
  "type": "command",
494
- "command": "bash .claude/hooks/scripts/stuck-detector.sh",
494
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/stuck-detector.sh\"",
495
495
  "continueOnBlock": true
496
496
  }
497
497
  ],
@@ -502,7 +502,7 @@
502
502
  "hooks": [
503
503
  {
504
504
  "type": "command",
505
- "command": "bash .claude/hooks/scripts/cost-cap-advisor.sh",
505
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/cost-cap-advisor.sh\"",
506
506
  "continueOnBlock": true
507
507
  }
508
508
  ],
@@ -513,7 +513,7 @@
513
513
  "hooks": [
514
514
  {
515
515
  "type": "command",
516
- "command": "bash .claude/hooks/scripts/content-hash-validator.sh"
516
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/content-hash-validator.sh\""
517
517
  }
518
518
  ],
519
519
  "description": "Store content hashes for Read operations — enables Edit staleness detection"
@@ -523,7 +523,7 @@
523
523
  "hooks": [
524
524
  {
525
525
  "type": "command",
526
- "command": "bash .claude/hooks/scripts/secret-filter.sh"
526
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/secret-filter.sh\""
527
527
  }
528
528
  ],
529
529
  "description": "Detect potential secrets in Bash/Read/Grep output — advisory warning only"
@@ -533,7 +533,7 @@
533
533
  "hooks": [
534
534
  {
535
535
  "type": "command",
536
- "command": "bash .claude/hooks/scripts/audit-log.sh"
536
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/audit-log.sh\""
537
537
  }
538
538
  ],
539
539
  "description": "Append-only audit log for state-changing tool operations"
@@ -543,7 +543,7 @@
543
543
  "hooks": [
544
544
  {
545
545
  "type": "command",
546
- "command": "bash .claude/hooks/skill-count-reminder.sh",
546
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/skill-count-reminder.sh\"",
547
547
  "continueOnBlock": true
548
548
  }
549
549
  ],
@@ -554,7 +554,7 @@
554
554
  "hooks": [
555
555
  {
556
556
  "type": "command",
557
- "command": "bash .claude/hooks/scripts/playwright-compress.sh"
557
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/playwright-compress.sh\""
558
558
  }
559
559
  ],
560
560
  "description": "Layer 4: Compress Playwright/Chrome MCP output via Haiku summarization"
@@ -564,7 +564,7 @@
564
564
  "hooks": [
565
565
  {
566
566
  "type": "command",
567
- "command": "bash .claude/hooks/scripts/auto-dev-token-tracker.sh"
567
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/auto-dev-token-tracker.sh\""
568
568
  }
569
569
  ],
570
570
  "description": "auto-dev token spend tracker — records per-phase token estimates during /pipeline auto-dev (Issue #1057, advisory)"
@@ -576,7 +576,7 @@
576
576
  "hooks": [
577
577
  {
578
578
  "type": "command",
579
- "command": "bash .claude/hooks/scripts/stop-console-audit.sh"
579
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/stop-console-audit.sh\""
580
580
  }
581
581
  ],
582
582
  "description": "Final console.log audit and session diagnostics before session ends"
@@ -586,7 +586,7 @@
586
586
  "hooks": [
587
587
  {
588
588
  "type": "command",
589
- "command": "bash .claude/hooks/scripts/eval-core-batch-save.sh"
589
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/eval-core-batch-save.sh\""
590
590
  }
591
591
  ],
592
592
  "description": "Batch-save agent outcomes to eval-core DB on session end (advisory, exit 0)"
@@ -596,7 +596,7 @@
596
596
  "hooks": [
597
597
  {
598
598
  "type": "command",
599
- "command": "bash .claude/hooks/scripts/feedback-collector.sh"
599
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/feedback-collector.sh\""
600
600
  }
601
601
  ],
602
602
  "description": "Auto-extract failure patterns from session outcomes (advisory, exit 0)"
@@ -606,7 +606,7 @@
606
606
  "hooks": [
607
607
  {
608
608
  "type": "command",
609
- "command": "bash .claude/hooks/scripts/session-reflection.sh"
609
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/session-reflection.sh\""
610
610
  }
611
611
  ],
612
612
  "description": "Transcript-based self-reflection: detect R007/R008 violations and log to .claude/outputs/reflections/ (Phase 1 MVP, advisory, exit 0)"
@@ -616,7 +616,7 @@
616
616
  "hooks": [
617
617
  {
618
618
  "type": "command",
619
- "command": "bash .claude/hooks/scripts/skill-extractor-analyzer.sh"
619
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/skill-extractor-analyzer.sh\""
620
620
  }
621
621
  ],
622
622
  "description": "Detect reusable skill candidates from session task outcomes (advisory, exit 0)"
@@ -626,7 +626,7 @@
626
626
  "hooks": [
627
627
  {
628
628
  "type": "command",
629
- "command": "bash .claude/hooks/scripts/auto-dev-token-summary.sh"
629
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/auto-dev-token-summary.sh\""
630
630
  }
631
631
  ],
632
632
  "description": "Print auto-dev token spend summary on session end (Issue #1057, advisory)"
@@ -638,7 +638,7 @@
638
638
  "hooks": [
639
639
  {
640
640
  "type": "command",
641
- "command": "bash .claude/hooks/scripts/failure-ledger.sh"
641
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/failure-ledger.sh\""
642
642
  }
643
643
  ],
644
644
  "description": "FAIL-axis instrumentation — append tool failures to the error ledger (JSONL). Never blocks; feeds fail-axis-cause-advisor.sh (#1561)"
@@ -13,7 +13,7 @@ if ! command -v jq &>/dev/null; then
13
13
  fi
14
14
 
15
15
  # Parse tool input
16
- tool=$(printf '%s\n' "$input" | jq -r '.tool // ""' 2>/dev/null) || { printf '%s\n' "$input"; exit 0; }
16
+ tool=$(printf '%s\n' "$input" | jq -r '.tool_name // .tool // ""' 2>/dev/null) || { printf '%s\n' "$input"; exit 0; }
17
17
  cmd=$(printf '%s\n' "$input" | jq -r '.tool_input.command // ""' 2>/dev/null) || { printf '%s\n' "$input"; exit 0; }
18
18
 
19
19
  # Only check Bash tool
@@ -21,7 +21,11 @@ tool_name=$(printf '%s\n' "$input" | jq -r '.tool_name? // "unknown"')
21
21
  tool_input=$(printf '%s\n' "$input" | jq -c 'if (.tool_input? | type) == "object" then .tool_input else {} end' 2>/dev/null) || tool_input='{}'
22
22
  [ -n "$tool_input" ] || tool_input='{}'
23
23
 
24
- SCHEMA_FILE=".claude/schemas/tool-inputs.json"
24
+ # Locate the repo root ourselves (#1767): hooks run in the session cwd, which may be
25
+ # a subdirectory. Prefer CLAUDE_PROJECT_DIR; fall back to this script's own location
26
+ # (<root>/.claude/hooks/scripts/ -> three levels up).
27
+ ROOT="${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)}"
28
+ SCHEMA_FILE="$ROOT/.claude/schemas/tool-inputs.json"
25
29
 
26
30
  # Skip if schema file doesn't exist
27
31
  if [ ! -f "$SCHEMA_FILE" ]; then
@@ -5,6 +5,9 @@
5
5
 
6
6
  set -euo pipefail
7
7
 
8
+ # Locate the repo root ourselves: hooks run in the session cwd, which may be a subdirectory
9
+ ROOT="${CLAUDE_PROJECT_DIR:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"
10
+
8
11
  # Read tool input from stdin
9
12
  INPUT=$(cat)
10
13
 
@@ -14,7 +17,8 @@ FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.path //
14
17
  # Only trigger for .claude/skills/ paths with SKILL.md
15
18
  if [[ "$FILE_PATH" == *".claude/skills/"*"SKILL.md"* ]]; then
16
19
  SKILL_NAME=$(echo "$FILE_PATH" | sed 's|.*\.claude/skills/||' | sed 's|/SKILL.md||')
17
- ACTUAL_COUNT=$(find .claude/skills -name 'SKILL.md' 2>/dev/null | wc -l | tr -d ' ')
20
+ # '|| true' keeps the advisory contract (exit 0) even if the skills directory is missing under pipefail
21
+ ACTUAL_COUNT=$( { find "$ROOT/.claude/skills" -name 'SKILL.md' 2>/dev/null || true; } | wc -l | tr -d ' ')
18
22
 
19
23
  cat >&2 << EOF
20
24
  ─── [Skill Sync Reminder] New/modified skill: ${SKILL_NAME} ───
@@ -33,7 +33,7 @@ oh-my-customcode uses an **advisory-first enforcement model**. Most rules are en
33
33
 
34
34
  | Anti-pattern | Required |
35
35
  |--------------|----------|
36
- | `hooks.json`만 편집하고 커밋 → settings.json `hooks` 블록 미재생성 | 편집 후 `hooks-settings.ts` 변환기(빌드) 실행 → settings.json(+local+templates) 재생성 확인 후 커밋 |
36
+ | `hooks.json`만 편집하고 커밋 → settings.json `hooks` 블록 미재생성 | 편집 후 `bun run sync:hooks`(또는 `bun run build`) 실행 → settings.json(+local+templates) 재생성 확인 후 커밋 |
37
37
 
38
38
  교훈: **배선 확인 ≠ 전달 확인 ≠ 발화 확인 ≠ 로드 확인** — R020 "actual outcome ≠ attempt"의 훅 도메인 재현 사례. 상세는 Read 도구로 열람.
39
39
 
@@ -8,7 +8,7 @@
8
8
  "hooks": [
9
9
  {
10
10
  "type": "command",
11
- "command": "bash .claude/hooks/scripts/stage-blocker.sh"
11
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/stage-blocker.sh\""
12
12
  }
13
13
  ],
14
14
  "matcher": "Write|Edit",
@@ -68,7 +68,7 @@
68
68
  "hooks": [
69
69
  {
70
70
  "type": "command",
71
- "command": "bash .claude/hooks/scripts/content-hash-validator.sh"
71
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/content-hash-validator.sh\""
72
72
  }
73
73
  ],
74
74
  "matcher": "Edit",
@@ -78,7 +78,7 @@
78
78
  "hooks": [
79
79
  {
80
80
  "type": "command",
81
- "command": "bash .claude/hooks/scripts/schema-validator.sh"
81
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/schema-validator.sh\""
82
82
  }
83
83
  ],
84
84
  "matcher": "Write|Edit|Bash",
@@ -88,7 +88,7 @@
88
88
  "hooks": [
89
89
  {
90
90
  "type": "command",
91
- "command": "bash .claude/hooks/scripts/rtk-intercept.sh"
91
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/rtk-intercept.sh\""
92
92
  }
93
93
  ],
94
94
  "matcher": "Bash",
@@ -98,7 +98,7 @@
98
98
  "hooks": [
99
99
  {
100
100
  "type": "command",
101
- "command": "bash .claude/hooks/scripts/destructive-git-guard.sh"
101
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/destructive-git-guard.sh\""
102
102
  }
103
103
  ],
104
104
  "matcher": "Bash",
@@ -108,7 +108,7 @@
108
108
  "hooks": [
109
109
  {
110
110
  "type": "command",
111
- "command": "bash .claude/hooks/scripts/rule-deletion-guard.sh"
111
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/rule-deletion-guard.sh\""
112
112
  }
113
113
  ],
114
114
  "matcher": "Bash",
@@ -122,15 +122,15 @@
122
122
  },
123
123
  {
124
124
  "type": "command",
125
- "command": "bash .claude/hooks/scripts/git-delegation-guard.sh"
125
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/git-delegation-guard.sh\""
126
126
  },
127
127
  {
128
128
  "type": "command",
129
- "command": "bash .claude/hooks/scripts/agent-teams-advisor.sh"
129
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/agent-teams-advisor.sh\""
130
130
  },
131
131
  {
132
132
  "type": "command",
133
- "command": "bash .claude/hooks/scripts/model-escalation-advisor.sh"
133
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/model-escalation-advisor.sh\""
134
134
  }
135
135
  ],
136
136
  "matcher": "Task|Agent",
@@ -142,7 +142,7 @@
142
142
  "hooks": [
143
143
  {
144
144
  "type": "command",
145
- "command": ".claude/hooks/scripts/omcustom-auto-update.sh"
145
+ "command": "\"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/omcustom-auto-update.sh\""
146
146
  }
147
147
  ],
148
148
  "matcher": "*",
@@ -152,7 +152,7 @@
152
152
  "hooks": [
153
153
  {
154
154
  "type": "command",
155
- "command": "bash .claude/hooks/scripts/session-env-check.sh"
155
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/session-env-check.sh\""
156
156
  }
157
157
  ],
158
158
  "matcher": "*",
@@ -162,7 +162,7 @@
162
162
  "hooks": [
163
163
  {
164
164
  "type": "command",
165
- "command": "bash .claude/hooks/scripts/stale-todo-scanner.sh"
165
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/stale-todo-scanner.sh\""
166
166
  }
167
167
  ],
168
168
  "matcher": "*",
@@ -172,7 +172,7 @@
172
172
  "hooks": [
173
173
  {
174
174
  "type": "command",
175
- "command": "bash .claude/hooks/scripts/adaptive-harness-scan.sh"
175
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/adaptive-harness-scan.sh\""
176
176
  }
177
177
  ],
178
178
  "matcher": "*",
@@ -182,7 +182,7 @@
182
182
  "hooks": [
183
183
  {
184
184
  "type": "command",
185
- "command": "bash .claude/hooks/scripts/session-autofix.sh"
185
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/session-autofix.sh\""
186
186
  }
187
187
  ],
188
188
  "matcher": "*",
@@ -192,7 +192,7 @@
192
192
  "hooks": [
193
193
  {
194
194
  "type": "command",
195
- "command": "bash .claude/hooks/scripts/plugin-cache-check.sh"
195
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/plugin-cache-check.sh\""
196
196
  }
197
197
  ],
198
198
  "matcher": "*",
@@ -202,7 +202,7 @@
202
202
  "hooks": [
203
203
  {
204
204
  "type": "command",
205
- "command": "bash .claude/hooks/scripts/claude-md-reinject.sh"
205
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/claude-md-reinject.sh\""
206
206
  }
207
207
  ],
208
208
  "matcher": "compact",
@@ -214,7 +214,7 @@
214
214
  "hooks": [
215
215
  {
216
216
  "type": "command",
217
- "command": "bash .claude/hooks/scripts/user-prompt-preprocessor.sh"
217
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/user-prompt-preprocessor.sh\""
218
218
  }
219
219
  ],
220
220
  "matcher": "*",
@@ -224,7 +224,7 @@
224
224
  "hooks": [
225
225
  {
226
226
  "type": "command",
227
- "command": "bash .claude/hooks/scripts/r007-r008-drift-advisor.sh"
227
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/r007-r008-drift-advisor.sh\""
228
228
  }
229
229
  ],
230
230
  "matcher": "*",
@@ -234,7 +234,7 @@
234
234
  "hooks": [
235
235
  {
236
236
  "type": "command",
237
- "command": "bash .claude/hooks/scripts/session-autofix-prompt.sh"
237
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/session-autofix-prompt.sh\""
238
238
  }
239
239
  ],
240
240
  "matcher": "*",
@@ -244,7 +244,7 @@
244
244
  "hooks": [
245
245
  {
246
246
  "type": "command",
247
- "command": "bash .claude/hooks/scripts/fail-axis-cause-advisor.sh"
247
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/fail-axis-cause-advisor.sh\""
248
248
  }
249
249
  ],
250
250
  "matcher": "*",
@@ -260,7 +260,7 @@
260
260
  },
261
261
  {
262
262
  "type": "command",
263
- "command": "bash .claude/hooks/scripts/agent-start-recorder.sh"
263
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/agent-start-recorder.sh\""
264
264
  }
265
265
  ],
266
266
  "matcher": "*",
@@ -272,23 +272,23 @@
272
272
  "hooks": [
273
273
  {
274
274
  "type": "command",
275
- "command": "bash .claude/hooks/scripts/task-outcome-recorder.sh"
275
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/task-outcome-recorder.sh\""
276
276
  },
277
277
  {
278
278
  "type": "command",
279
- "command": "bash .claude/hooks/scripts/stall-detection-advisor.sh"
279
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/stall-detection-advisor.sh\""
280
280
  },
281
281
  {
282
282
  "type": "command",
283
- "command": "bash .claude/hooks/scripts/auto-continue-guard.sh"
283
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/auto-continue-guard.sh\""
284
284
  },
285
285
  {
286
286
  "type": "command",
287
- "command": "bash .claude/hooks/scripts/r007-r008-drift-advisor.sh"
287
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/r007-r008-drift-advisor.sh\""
288
288
  },
289
289
  {
290
290
  "type": "command",
291
- "command": "bash .claude/hooks/scripts/subagent-failure-advisor.sh"
291
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/subagent-failure-advisor.sh\""
292
292
  }
293
293
  ],
294
294
  "matcher": "*",
@@ -300,7 +300,7 @@
300
300
  "hooks": [
301
301
  {
302
302
  "type": "command",
303
- "command": "bash .claude/hooks/scripts/cwd-change-detector.sh"
303
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/cwd-change-detector.sh\""
304
304
  }
305
305
  ],
306
306
  "matcher": "*",
@@ -312,7 +312,7 @@
312
312
  "hooks": [
313
313
  {
314
314
  "type": "command",
315
- "command": "bash .claude/hooks/scripts/file-change-validator.sh"
315
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/file-change-validator.sh\""
316
316
  }
317
317
  ],
318
318
  "matcher": "*",
@@ -372,7 +372,7 @@
372
372
  "hooks": [
373
373
  {
374
374
  "type": "command",
375
- "command": "bash .claude/hooks/scripts/task-state-precompact.sh"
375
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/task-state-precompact.sh\""
376
376
  }
377
377
  ],
378
378
  "matcher": "*",
@@ -470,7 +470,7 @@
470
470
  "hooks": [
471
471
  {
472
472
  "type": "command",
473
- "command": "bash .claude/hooks/scripts/context-budget-advisor.sh",
473
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/context-budget-advisor.sh\"",
474
474
  "continueOnBlock": true
475
475
  }
476
476
  ],
@@ -481,7 +481,7 @@
481
481
  "hooks": [
482
482
  {
483
483
  "type": "command",
484
- "command": "bash .claude/hooks/scripts/r007-r008-drift-advisor.sh",
484
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/r007-r008-drift-advisor.sh\"",
485
485
  "continueOnBlock": true
486
486
  }
487
487
  ],
@@ -492,7 +492,7 @@
492
492
  "hooks": [
493
493
  {
494
494
  "type": "command",
495
- "command": "bash .claude/hooks/scripts/stuck-detector.sh",
495
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/stuck-detector.sh\"",
496
496
  "continueOnBlock": true
497
497
  }
498
498
  ],
@@ -503,7 +503,7 @@
503
503
  "hooks": [
504
504
  {
505
505
  "type": "command",
506
- "command": "bash .claude/hooks/scripts/cost-cap-advisor.sh",
506
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/cost-cap-advisor.sh\"",
507
507
  "continueOnBlock": true
508
508
  }
509
509
  ],
@@ -514,7 +514,7 @@
514
514
  "hooks": [
515
515
  {
516
516
  "type": "command",
517
- "command": "bash .claude/hooks/scripts/content-hash-validator.sh"
517
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/content-hash-validator.sh\""
518
518
  }
519
519
  ],
520
520
  "matcher": "Read",
@@ -524,7 +524,7 @@
524
524
  "hooks": [
525
525
  {
526
526
  "type": "command",
527
- "command": "bash .claude/hooks/scripts/secret-filter.sh"
527
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/secret-filter.sh\""
528
528
  }
529
529
  ],
530
530
  "matcher": "Bash|Read|Grep",
@@ -534,7 +534,7 @@
534
534
  "hooks": [
535
535
  {
536
536
  "type": "command",
537
- "command": "bash .claude/hooks/scripts/audit-log.sh"
537
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/audit-log.sh\""
538
538
  }
539
539
  ],
540
540
  "matcher": "Edit|Write|Bash|Agent",
@@ -544,7 +544,7 @@
544
544
  "hooks": [
545
545
  {
546
546
  "type": "command",
547
- "command": "bash .claude/hooks/skill-count-reminder.sh",
547
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/skill-count-reminder.sh\"",
548
548
  "continueOnBlock": true
549
549
  }
550
550
  ],
@@ -555,7 +555,7 @@
555
555
  "hooks": [
556
556
  {
557
557
  "type": "command",
558
- "command": "bash .claude/hooks/scripts/playwright-compress.sh"
558
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/playwright-compress.sh\""
559
559
  }
560
560
  ],
561
561
  "matcher": "mcp__playwright__.*|mcp__claude-in-chrome__.*",
@@ -565,7 +565,7 @@
565
565
  "hooks": [
566
566
  {
567
567
  "type": "command",
568
- "command": "bash .claude/hooks/scripts/auto-dev-token-tracker.sh"
568
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/auto-dev-token-tracker.sh\""
569
569
  }
570
570
  ],
571
571
  "matcher": "Task|Agent",
@@ -577,7 +577,7 @@
577
577
  "hooks": [
578
578
  {
579
579
  "type": "command",
580
- "command": "bash .claude/hooks/scripts/stop-console-audit.sh"
580
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/stop-console-audit.sh\""
581
581
  }
582
582
  ],
583
583
  "matcher": "*",
@@ -587,7 +587,7 @@
587
587
  "hooks": [
588
588
  {
589
589
  "type": "command",
590
- "command": "bash .claude/hooks/scripts/eval-core-batch-save.sh"
590
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/eval-core-batch-save.sh\""
591
591
  }
592
592
  ],
593
593
  "matcher": "*",
@@ -597,7 +597,7 @@
597
597
  "hooks": [
598
598
  {
599
599
  "type": "command",
600
- "command": "bash .claude/hooks/scripts/feedback-collector.sh"
600
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/feedback-collector.sh\""
601
601
  }
602
602
  ],
603
603
  "matcher": "*",
@@ -607,7 +607,7 @@
607
607
  "hooks": [
608
608
  {
609
609
  "type": "command",
610
- "command": "bash .claude/hooks/scripts/session-reflection.sh"
610
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/session-reflection.sh\""
611
611
  }
612
612
  ],
613
613
  "matcher": "*",
@@ -617,7 +617,7 @@
617
617
  "hooks": [
618
618
  {
619
619
  "type": "command",
620
- "command": "bash .claude/hooks/scripts/skill-extractor-analyzer.sh"
620
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/skill-extractor-analyzer.sh\""
621
621
  }
622
622
  ],
623
623
  "matcher": "*",
@@ -627,7 +627,7 @@
627
627
  "hooks": [
628
628
  {
629
629
  "type": "command",
630
- "command": "bash .claude/hooks/scripts/auto-dev-token-summary.sh"
630
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/auto-dev-token-summary.sh\""
631
631
  }
632
632
  ],
633
633
  "matcher": "*",
@@ -639,7 +639,7 @@
639
639
  "hooks": [
640
640
  {
641
641
  "type": "command",
642
- "command": "bash .claude/hooks/scripts/failure-ledger.sh"
642
+ "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/failure-ledger.sh\""
643
643
  }
644
644
  ],
645
645
  "matcher": "*",
@@ -85,12 +85,14 @@ The SubagentStop hook array in `hooks.json` defines a strict ordering:
85
85
 
86
86
  ```json
87
87
  "SubagentStop": [
88
- { "command": "bash .claude/hooks/scripts/task-outcome-recorder.sh" },
89
- { "command": "bash .claude/hooks/scripts/stall-detection-advisor.sh" },
88
+ { "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/task-outcome-recorder.sh\"" },
89
+ { "command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/stall-detection-advisor.sh\"" },
90
90
  ...
91
91
  ]
92
92
  ```
93
93
 
94
+ Hook commands are anchored to the project root with `${CLAUDE_PROJECT_DIR:-.}`: hooks run in the session cwd, Claude Code injects `CLAUDE_PROJECT_DIR` into hook processes, and the `:-.` fallback keeps the relative-path behavior when it is unset. `hooks.json` is the source; the tracked `settings.json` hooks blocks are generated from it by `bun run sync:hooks` (also run at the end of `bun run build`) — never hand-edit them.
95
+
94
96
  **task-outcome-recorder MUST run before stall-detection-advisor.**
95
97
 
96
98
  Reason: `stall-detection-advisor.sh` removes the matching start entry from `.claude-agent-starts-$PPID` after reading it (to prevent re-matching on the next SubagentStop). If the order were reversed, `task-outcome-recorder.sh` would find no start entry for the agent and would always record `duration_seconds=0`.
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.1.91",
2
+ "version": "1.1.92",
3
3
  "lastUpdated": "2026-09-03",
4
4
  "omcustomMinClaudeCode": "2.1.121",
5
5
  "omcustomMinClaudeCodeReason": "Sensitive-path direct Write/Edit on .claude/** under bypassPermissions (R010 deprecation, #1101)",