claude-dev-env 7.0.0 → 7.2.0
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/.agents/skills/pull-request/scripts/pull_request.py +2 -0
- package/.agents/skills/pull-request/scripts/test_pull_request.py +34 -0
- package/.agents/skills/session-continuity/AGENTS.md +1 -0
- package/.agents/skills/session-continuity/CLAUDE.md +1 -0
- package/.agents/skills/session-continuity/README.md +213 -0
- package/.agents/skills/session-continuity/SKILL.md +125 -0
- package/.agents/skills/session-continuity/continuity.mjs +447 -0
- package/.agents/skills/session-continuity/continuity.test.mjs +373 -0
- package/.agents/skills/syncing-submodules/scripts/test_sync_parent_pointer.py +0 -7
- package/.agents/skills/windows-scheduled-task/SKILL.md +153 -0
- package/bin/cde.mjs +36 -8
- package/bin/cde.test.mjs +48 -0
- package/bin/ever-shipped-skills.mjs +2 -20
- package/bin/ever-shipped-skills.test.mjs +8 -0
- package/bin/expand_home_directory_tokens.mjs +4 -5
- package/bin/expand_home_directory_tokens.test.mjs +128 -0
- package/bin/git_hooks_installer.mjs +22 -3
- package/bin/git_hooks_installer.test.mjs +159 -5
- package/bin/install-session-continuity.mjs +88 -0
- package/bin/install-session-continuity.test.mjs +55 -0
- package/bin/install.codex-rules.test.mjs +5 -2
- package/bin/install.cursor-rules.test.mjs +5 -2
- package/bin/install.mjs +478 -63
- package/bin/install.policy-lint.test.mjs +56 -4
- package/bin/install.profiles.test.mjs +10 -0
- package/bin/install.prune.test.mjs +227 -17
- package/bin/install.test.mjs +500 -120
- package/bin/install.transaction.test.mjs +3 -3
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/code_rules_banned_identifiers.py +3 -0
- package/hooks/blocking/precommit_code_rules_gate.py +5 -8
- package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +7 -52
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -69
- package/hooks/blocking/test_code_rules_banned_identifiers.py +64 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -244
- package/hooks/blocking/test_shared_stdin_adoption.py +1 -47
- package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +116 -0
- package/hooks/git-hooks/post_commit.py +33 -0
- package/hooks/git-hooks/pre_commit.py +18 -90
- package/hooks/git-hooks/pre_push.py +45 -541
- package/hooks/git-hooks/test_installed_policy_lint.py +55 -6
- package/hooks/git-hooks/test_native_hook_support.py +129 -0
- package/hooks/git-hooks/test_native_policy_commit.py +39 -6
- package/hooks/git-hooks/test_post_commit.py +43 -0
- package/hooks/git-hooks/test_pre_commit.py +36 -392
- package/hooks/git-hooks/test_pre_push.py +48 -1631
- package/hooks/git-hooks/test_verification_notice.py +60 -0
- package/hooks/git-hooks/test_verification_start.py +173 -0
- package/hooks/git-hooks/verification_notice.py +192 -0
- package/hooks/git-hooks/verification_notice_context.py +253 -0
- package/hooks/git-hooks/verification_notice_state.py +237 -0
- package/hooks/git-hooks/verification_start.py +100 -0
- package/hooks/hooks.json +0 -119
- package/hooks/hooks_constants/banned_identifiers_constants.py +4 -0
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -15
- package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -4
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -50
- package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -4
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +3 -66
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +42 -185
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +3 -25
- package/hooks/validation/test_post_tool_use_dispatcher.py +35 -298
- package/hooks/validators/run_all_validators.py +5 -1
- package/hooks/validators/security_checks.py +57 -2
- package/hooks/validators/test_hook_timing_harness.py +2 -5
- package/hooks/validators/test_run_all_validators_mutation_tool_parity.py +3 -11
- package/hooks/validators/test_run_all_validators_policy_lint.py +36 -0
- package/hooks/validators/test_security_checks.py +57 -0
- package/package.json +1 -1
- package/rules/confirm-implementation-forks.md +2 -0
- package/rules/git-workflow.md +12 -0
- package/rules/pstack-host-mapping.md +48 -0
- package/rules/vault-context.md +3 -5
- package/rules/verify-runtime-state.md +7 -0
- package/scripts/audit-hooks-cli.mjs +5 -0
- package/scripts/audit-hooks.mjs +72 -16
- package/scripts/automatic_advisory/__init__.py +1 -0
- package/scripts/automatic_advisory/checkout.py +51 -0
- package/scripts/automatic_advisory/cli.py +334 -0
- package/scripts/automatic_advisory/config/__init__.py +1 -0
- package/scripts/automatic_advisory/config/constants.py +93 -0
- package/scripts/automatic_advisory/config/timing.py +4 -0
- package/scripts/automatic_advisory/configuration.py +197 -0
- package/scripts/automatic_advisory/execution.py +350 -0
- package/scripts/automatic_advisory/git.py +69 -0
- package/scripts/automatic_advisory/model.py +107 -0
- package/scripts/automatic_advisory/process_host.py +32 -0
- package/scripts/automatic_advisory/publisher.py +84 -0
- package/scripts/automatic_advisory/runner.py +399 -0
- package/scripts/automatic_advisory/state.py +116 -0
- package/scripts/automatic_advisory/window_flags.py +41 -0
- package/scripts/automatic_advisory/windows_job.py +122 -0
- package/scripts/codex_compat_materializer.py +99 -13
- package/scripts/durable_post_lint.py +30 -2
- package/scripts/durable_post_lint_config/config/constants.py +10 -0
- package/scripts/durable_post_lint_config/config/test_constants.py +13 -0
- package/scripts/local_report_cli.py +184 -0
- package/scripts/local_report_core.py +247 -0
- package/scripts/local_report_publisher.py +12 -0
- package/scripts/local_report_validation.py +357 -0
- package/scripts/local_verification/__init__.py +23 -0
- package/scripts/local_verification/check_runner.py +364 -0
- package/scripts/local_verification/check_support.py +99 -0
- package/scripts/local_verification/cli.py +158 -0
- package/scripts/local_verification/command_runner.py +84 -0
- package/scripts/local_verification/config/__init__.py +1 -0
- package/scripts/local_verification/config/constants.py +89 -0
- package/scripts/local_verification/config/timing.py +1 -0
- package/scripts/local_verification/git_state.py +146 -0
- package/scripts/local_verification/manifest.py +224 -0
- package/scripts/local_verification/model.py +174 -0
- package/scripts/local_verification/runner.py +234 -0
- package/scripts/policy_lint/adapter_pairing.py +58 -3
- package/scripts/policy_lint/config/approved_test_pairs.py +323 -0
- package/scripts/pr_verification/__init__.py +1 -0
- package/scripts/pr_verification/branch_refs.py +114 -0
- package/scripts/pr_verification/config/__init__.py +1 -0
- package/scripts/pr_verification/config/constants.py +84 -0
- package/scripts/pr_verification/config/timing.py +5 -0
- package/scripts/pr_verification/github.py +137 -0
- package/scripts/pr_verification/github_api.py +376 -0
- package/scripts/pr_verification/github_labels.py +88 -0
- package/scripts/pr_verification/github_parsing.py +182 -0
- package/scripts/pr_verification/github_transport.py +91 -0
- package/scripts/pr_verification/lock.py +122 -0
- package/scripts/pr_verification/model.py +43 -0
- package/scripts/pr_verification/requirements.txt +1 -0
- package/scripts/test_durable_post_lint.py +97 -0
- package/scripts/tests/test-audit-hooks-cli.mjs +150 -0
- package/scripts/tests/test-audit-hooks.mjs +94 -2
- package/scripts/tests/test_automatic_advisory.py +376 -0
- package/scripts/tests/test_automatic_advisory_job_cleanup.py +71 -0
- package/scripts/tests/test_automatic_advisory_polling.py +216 -0
- package/scripts/tests/test_automatic_advisory_recovery.py +320 -0
- package/scripts/tests/test_automatic_advisory_repairs.py +400 -0
- package/scripts/tests/test_automatic_advisory_settings.py +48 -0
- package/scripts/tests/test_automatic_advisory_window_flags.py +163 -0
- package/scripts/tests/test_closed_pr_label.py +157 -0
- package/scripts/tests/test_codex_compat_materializer.py +189 -6
- package/scripts/tests/test_github_labels.py +101 -0
- package/scripts/tests/test_github_transport.py +125 -0
- package/scripts/tests/test_local_report_publisher.py +396 -0
- package/scripts/tests/test_local_report_publisher_cli.py +85 -0
- package/scripts/tests/test_local_verification.py +348 -0
- package/scripts/tests/test_local_verification_cli.py +98 -0
- package/scripts/tests/test_policy_lint_rules_pairing.py +163 -0
- package/scripts/tests/test_pr_verification_github.py +392 -0
- package/scripts/tests/test_pr_verification_lock.py +168 -0
- package/scripts/tests/test_verification_notice.py +347 -0
- package/hooks/blocking/ask_user_question_shape_blocker.py +0 -83
- package/hooks/blocking/bot_mention_comment_blocker.py +0 -76
- package/hooks/blocking/claude_md_orphan_file_blocker.py +0 -129
- package/hooks/blocking/cursor_cli_python_misfire_blocker.py +0 -108
- package/hooks/blocking/destructive_command_blocker.py +0 -2073
- package/hooks/blocking/fable_spawn_gate.py +0 -187
- package/hooks/blocking/luna_fast_mode_gate.py +0 -160
- package/hooks/blocking/nas_ssh_binary_enforcer.py +0 -163
- package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +0 -260
- package/hooks/blocking/package_inventory_stale_blocker.py +0 -107
- package/hooks/blocking/pii_prevention_blocker.py +0 -365
- package/hooks/blocking/piped_pytest_blocker.py +0 -1223
- package/hooks/blocking/question_to_user_enforcer.py +0 -130
- package/hooks/blocking/send_user_file_open_locally_blocker.py +0 -71
- package/hooks/blocking/sensitive_file_protector.py +0 -167
- package/hooks/blocking/session_edit_stage_gate.py +0 -684
- package/hooks/blocking/session_handoff_blocker.py +0 -182
- package/hooks/blocking/shell_substitution_blocker.py +0 -350
- package/hooks/blocking/test_ask_user_question_shape.py +0 -69
- package/hooks/blocking/test_bot_mention_comment_blocker.py +0 -131
- package/hooks/blocking/test_claude_md_orphan_file_blocker.py +0 -1277
- package/hooks/blocking/test_cursor_cli_python_misfire_blocker.py +0 -208
- package/hooks/blocking/test_destructive_command_blocker.py +0 -2024
- package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +0 -105
- package/hooks/blocking/test_destructive_command_blocker_no_verify.py +0 -122
- package/hooks/blocking/test_destructive_command_blocker_patterns.py +0 -133
- package/hooks/blocking/test_fable_spawn_gate.py +0 -381
- package/hooks/blocking/test_inventory_deadlock_resolution.py +0 -154
- package/hooks/blocking/test_luna_fast_mode_gate.py +0 -211
- package/hooks/blocking/test_nas_ssh_binary_enforcer.py +0 -225
- package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +0 -231
- package/hooks/blocking/test_package_inventory_stale_blocker.py +0 -382
- package/hooks/blocking/test_pii_prevention_blocker.py +0 -1222
- package/hooks/blocking/test_pii_prevention_windows_git_path.py +0 -41
- package/hooks/blocking/test_pii_write_surface_ephemeral.py +0 -221
- package/hooks/blocking/test_piped_pytest_blocker.py +0 -587
- package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +0 -233
- package/hooks/blocking/test_question_to_user_enforcer.py +0 -169
- package/hooks/blocking/test_send_user_file_open_locally_blocker.py +0 -118
- package/hooks/blocking/test_sensitive_file_protector.py +0 -365
- package/hooks/blocking/test_session_edit_stage_gate.py +0 -537
- package/hooks/blocking/test_session_edit_stage_gate_staging.py +0 -64
- package/hooks/blocking/test_session_edit_stage_gate_support.py +0 -204
- package/hooks/blocking/test_session_edit_stage_gate_tracker.py +0 -116
- package/hooks/blocking/test_session_handoff_blocker.py +0 -318
- package/hooks/blocking/test_shell_substitution_blocker.py +0 -298
- package/hooks/blocking/test_unscoped_search_blocker.py +0 -102
- package/hooks/blocking/test_write_existing_file_blocker.py +0 -122
- package/hooks/blocking/tests/test_pii_prevention_blocker.py +0 -259
- package/hooks/blocking/unscoped_search_blocker.py +0 -391
- package/hooks/blocking/write_existing_file_blocker.py +0 -124
- package/hooks/hooks_constants/bot_mention_comment_blocker_constants.py +0 -19
- package/hooks/hooks_constants/cursor_cli_python_misfire_blocker_constants.py +0 -79
- package/hooks/hooks_constants/fable_spawn_gate_constants.py +0 -61
- package/hooks/hooks_constants/luna_fast_mode_gate_constants.py +0 -45
- package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +0 -20
- package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +0 -57
- package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +0 -17
- package/hooks/hooks_constants/sensitive_file_protector_constants.py +0 -40
- package/hooks/hooks_constants/session_handoff_blocker_constants.py +0 -10
- package/hooks/hooks_constants/shell_substitution_blocker_constants.py +0 -80
package/bin/install.mjs
CHANGED
|
@@ -11,7 +11,10 @@ import {
|
|
|
11
11
|
KNOWN_GIT_HOOK_NAMES,
|
|
12
12
|
} from './git_hooks_installer.mjs';
|
|
13
13
|
import { installMypyIniForClaudeHooks } from './install_mypy_ini.mjs';
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
expandHomeDirectoryTokens,
|
|
16
|
+
expandHomeDirectoryTokensInSettings,
|
|
17
|
+
} from './expand_home_directory_tokens.mjs';
|
|
15
18
|
import { EVER_SHIPPED_SKILL_NAMES } from './ever-shipped-skills.mjs';
|
|
16
19
|
import {
|
|
17
20
|
managedDenyEntriesFromPackageSettings,
|
|
@@ -175,7 +178,6 @@ export const CORE_INCLUDE_DIRECTORIES = [
|
|
|
175
178
|
export const CORE_SKILLS = [
|
|
176
179
|
'orchestrator', 'orchestrator-refresh', 'team-advisor',
|
|
177
180
|
'grok-spawn',
|
|
178
|
-
'pr-small-cl',
|
|
179
181
|
'everything-search',
|
|
180
182
|
'test-runner',
|
|
181
183
|
'privacy-hygiene',
|
|
@@ -321,10 +323,6 @@ export const INSTALL_GROUPS = {
|
|
|
321
323
|
includeAllHooks: true,
|
|
322
324
|
includeCodexRules: true,
|
|
323
325
|
},
|
|
324
|
-
journal: {
|
|
325
|
-
description: 'Session logging and memory',
|
|
326
|
-
skills: ['session-log', 'session-tidy'],
|
|
327
|
-
},
|
|
328
326
|
...dependencyDiscovery.groups,
|
|
329
327
|
};
|
|
330
328
|
|
|
@@ -1236,6 +1234,35 @@ export const RETIRED_HOOK_REGISTRATION_RELATIVE_PATHS = new Set([
|
|
|
1236
1234
|
'session/untracked_repo_detector.py',
|
|
1237
1235
|
'session/gh_pr_author_session_cleanup.py',
|
|
1238
1236
|
'observability/pr_description_writer_spawn_tracker.py',
|
|
1237
|
+
'blocking/plain_language_blocker.py',
|
|
1238
|
+
'lifecycle/config_change_guard.py',
|
|
1239
|
+
'blocking/docstring_rule_gate_count_blocker.py',
|
|
1240
|
+
'blocking/write_existing_file_blocker.py',
|
|
1241
|
+
'blocking/sensitive_file_protector.py',
|
|
1242
|
+
'blocking/pii_prevention_blocker.py',
|
|
1243
|
+
'blocking/duplicate_rmtree_helper_blocker.py',
|
|
1244
|
+
'blocking/claude_md_orphan_file_blocker.py',
|
|
1245
|
+
'blocking/package_inventory_stale_blocker.py',
|
|
1246
|
+
'blocking/env_var_table_code_drift_blocker.py',
|
|
1247
|
+
'blocking/pytest_testpaths_orphan_blocker.py',
|
|
1248
|
+
'blocking/destructive_command_blocker.py',
|
|
1249
|
+
'blocking/shell_substitution_blocker.py',
|
|
1250
|
+
'blocking/piped_pytest_blocker.py',
|
|
1251
|
+
'blocking/cursor_cli_python_misfire_blocker.py',
|
|
1252
|
+
'blocking/unscoped_search_blocker.py',
|
|
1253
|
+
'blocking/nas_ssh_binary_enforcer.py',
|
|
1254
|
+
'blocking/block_main_commit.py',
|
|
1255
|
+
'blocking/session_edit_stage_gate.py',
|
|
1256
|
+
'blocking/bash_pre_tool_use_dispatcher.py',
|
|
1257
|
+
'blocking/stop_dispatcher.py',
|
|
1258
|
+
'blocking/bot_mention_comment_blocker.py',
|
|
1259
|
+
'blocking/fable_spawn_gate.py',
|
|
1260
|
+
'blocking/luna_fast_mode_gate.py',
|
|
1261
|
+
'blocking/orchestrator_refresh_reschedule_gate.py',
|
|
1262
|
+
'blocking/ask_user_question_shape_blocker.py',
|
|
1263
|
+
'blocking/send_user_file_open_locally_blocker.py',
|
|
1264
|
+
'blocking/question_to_user_enforcer.py',
|
|
1265
|
+
'blocking/session_handoff_blocker.py',
|
|
1239
1266
|
]);
|
|
1240
1267
|
|
|
1241
1268
|
/**
|
|
@@ -1349,15 +1376,26 @@ export function commandReferencesManagedHook(commandString, managedHookRelativeP
|
|
|
1349
1376
|
* @returns {boolean} True when the managed tail ends at a path boundary.
|
|
1350
1377
|
*/
|
|
1351
1378
|
function commandTailEndsAtManagedHook(normalizedCommand, relativePath) {
|
|
1379
|
+
return commandHoldsPathAtBoundary(normalizedCommand, `/.claude/hooks/${relativePath}`);
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
/**
|
|
1383
|
+
* Report whether a command holds one path spelling that ends at a path boundary:
|
|
1384
|
+
* end of string, or an argument separator (whitespace, quote, or semicolon).
|
|
1385
|
+
*
|
|
1386
|
+
* @param {string} normalizedCommand Forward-slash-normalized hook command.
|
|
1387
|
+
* @param {string} pathText The path spelling to look for.
|
|
1388
|
+
* @returns {boolean} True when the command holds that path at a boundary.
|
|
1389
|
+
*/
|
|
1390
|
+
function commandHoldsPathAtBoundary(normalizedCommand, pathText) {
|
|
1352
1391
|
const commandArgumentBoundary = /[\s'";]/;
|
|
1353
|
-
|
|
1354
|
-
let searchStart = normalizedCommand.indexOf(managedTail);
|
|
1392
|
+
let searchStart = normalizedCommand.indexOf(pathText);
|
|
1355
1393
|
while (searchStart !== -1) {
|
|
1356
|
-
const
|
|
1357
|
-
if (
|
|
1394
|
+
const characterAfterPath = normalizedCommand[searchStart + pathText.length];
|
|
1395
|
+
if (characterAfterPath === undefined || commandArgumentBoundary.test(characterAfterPath)) {
|
|
1358
1396
|
return true;
|
|
1359
1397
|
}
|
|
1360
|
-
searchStart = normalizedCommand.indexOf(
|
|
1398
|
+
searchStart = normalizedCommand.indexOf(pathText, searchStart + 1);
|
|
1361
1399
|
}
|
|
1362
1400
|
return false;
|
|
1363
1401
|
}
|
|
@@ -1564,29 +1602,139 @@ export function retiredManagedHookRelativePaths(
|
|
|
1564
1602
|
return retiredRelativePaths;
|
|
1565
1603
|
}
|
|
1566
1604
|
|
|
1605
|
+
const MANAGED_HOOK_ROOT_PATHS = [
|
|
1606
|
+
join(CLAUDE_HOME, MANAGED_HOOKS_DIRECTORY_NAME),
|
|
1607
|
+
join(AGENTS_HOME, MANAGED_HOOKS_DIRECTORY_NAME),
|
|
1608
|
+
INSTALL_ROOT_RESOLUTION.codexHooksInstallDirectory,
|
|
1609
|
+
];
|
|
1610
|
+
|
|
1611
|
+
const CODEX_HOOKS_CONFIGURATION_FILE_NAME = 'hooks.json';
|
|
1612
|
+
|
|
1613
|
+
const CODEX_HOOKS_CONFIGURATION_PATH = join(
|
|
1614
|
+
dirname(INSTALL_ROOT_RESOLUTION.codexHooksInstallDirectory),
|
|
1615
|
+
CODEX_HOOKS_CONFIGURATION_FILE_NAME,
|
|
1616
|
+
);
|
|
1617
|
+
|
|
1567
1618
|
/**
|
|
1568
|
-
*
|
|
1569
|
-
* hook scripts.
|
|
1619
|
+
* Resolve a path through every link on it, or return null when it is missing.
|
|
1570
1620
|
*
|
|
1571
|
-
*
|
|
1572
|
-
*
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1621
|
+
* @param {string} filesystemPath The absolute path to resolve.
|
|
1622
|
+
* @returns {string|null} The real path, or null when nothing stands there.
|
|
1623
|
+
*/
|
|
1624
|
+
function realPathOrNull(filesystemPath) {
|
|
1625
|
+
try {
|
|
1626
|
+
return realpathSync(filesystemPath);
|
|
1627
|
+
} catch {
|
|
1628
|
+
return null;
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
/**
|
|
1633
|
+
* Return comparison keys for each owned hooks directory, including its real path.
|
|
1576
1634
|
*
|
|
1577
|
-
*
|
|
1578
|
-
*
|
|
1579
|
-
* it names no retired script and its entry stays.
|
|
1635
|
+
* ~/.claude/hooks, the agents-home hooks directory, and ~/.codex/hooks can
|
|
1636
|
+
* point at one directory. Both the given path and its resolved path are keys.
|
|
1580
1637
|
*
|
|
1581
|
-
* @param {
|
|
1638
|
+
* @param {string[]} allHookRootPaths The hooks directories this installer writes.
|
|
1639
|
+
* @returns {Set<string>} The comparison key of every owned spelling.
|
|
1640
|
+
*/
|
|
1641
|
+
export function ownedHookRootComparisonKeys(allHookRootPaths) {
|
|
1642
|
+
const allRootKeys = new Set();
|
|
1643
|
+
for (const hookRootPath of allHookRootPaths) {
|
|
1644
|
+
if (!hookRootPath) continue;
|
|
1645
|
+
allRootKeys.add(comparisonKeyForPath(hookRootPath));
|
|
1646
|
+
const realHookRootPath = realPathOrNull(hookRootPath);
|
|
1647
|
+
if (realHookRootPath) allRootKeys.add(comparisonKeyForPath(realHookRootPath));
|
|
1648
|
+
}
|
|
1649
|
+
return allRootKeys;
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
/**
|
|
1653
|
+
* Expand `$HOME`, `${HOME}`, `~/`, and `%USERPROFILE%` in a hook command.
|
|
1654
|
+
*
|
|
1655
|
+
* `$HOME`, `${HOME}`, and `~/` go through expandHomeDirectoryTokens, the helper
|
|
1656
|
+
* the settings rewrite already uses, so a tilde is not left in front of home.
|
|
1657
|
+
* `%USERPROFILE%` stays here because this comparison only reads a spelling.
|
|
1658
|
+
*
|
|
1659
|
+
* @param {string} normalizedCommand Forward-slash-normalized hook command.
|
|
1660
|
+
* @param {string} homeDirectory The absolute home directory to substitute.
|
|
1661
|
+
* @returns {string} The command with each home reference expanded.
|
|
1662
|
+
*/
|
|
1663
|
+
function expandHomeReferencesInCommand(normalizedCommand, homeDirectory) {
|
|
1664
|
+
const normalizedHome = homeDirectory.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
1665
|
+
const userProfileExpanded = normalizedCommand.replace(
|
|
1666
|
+
/%USERPROFILE%/gi, () => normalizedHome,
|
|
1667
|
+
);
|
|
1668
|
+
return expandHomeDirectoryTokens(userProfileExpanded, homeDirectory);
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
/**
|
|
1672
|
+
* Build the spelling a hook command is compared through: forward-slashed, home
|
|
1673
|
+
* references expanded, and case-folded wherever the filesystem ignores case.
|
|
1674
|
+
*
|
|
1675
|
+
* @param {string} commandString The hook command from a host configuration file.
|
|
1676
|
+
* @param {string} homeDirectory The absolute home directory to substitute.
|
|
1677
|
+
* @returns {string} The command spelling the owned-root test reads.
|
|
1678
|
+
*/
|
|
1679
|
+
export function hookCommandComparisonSpelling(commandString, homeDirectory) {
|
|
1680
|
+
const forwardSlashCommand = commandString.replace(/\\/g, '/');
|
|
1681
|
+
const expandedCommand = expandHomeReferencesInCommand(forwardSlashCommand, homeDirectory);
|
|
1682
|
+
return isCaseInsensitiveFilesystem() ? expandedCommand.toLowerCase() : expandedCommand;
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
/**
|
|
1686
|
+
* Report whether a hook command runs one script under a hooks directory this
|
|
1687
|
+
* installer owns.
|
|
1688
|
+
*
|
|
1689
|
+
* @param {unknown} commandString The hook command from a host configuration file.
|
|
1690
|
+
* @param {string} relativePath The script path under a hooks directory.
|
|
1691
|
+
* @param {{allOwnedHookRootKeys: Set<string>, homeDirectory: string}} ownership The owned roots.
|
|
1692
|
+
* @returns {boolean} True when the command runs that script under an owned root.
|
|
1693
|
+
*/
|
|
1694
|
+
export function commandNamesOwnedHookScript(commandString, relativePath, ownership) {
|
|
1695
|
+
if (typeof commandString !== 'string') return false;
|
|
1696
|
+
const shouldFoldCase = isCaseInsensitiveFilesystem();
|
|
1697
|
+
const comparisonCommand = hookCommandComparisonSpelling(
|
|
1698
|
+
commandString, ownership.homeDirectory,
|
|
1699
|
+
);
|
|
1700
|
+
const comparisonRelativePath = shouldFoldCase ? relativePath.toLowerCase() : relativePath;
|
|
1701
|
+
for (const ownedRootKey of ownership.allOwnedHookRootKeys) {
|
|
1702
|
+
const ownedScriptPath = `${ownedRootKey}/${comparisonRelativePath}`;
|
|
1703
|
+
if (commandHoldsPathAtBoundary(comparisonCommand, ownedScriptPath)) return true;
|
|
1704
|
+
}
|
|
1705
|
+
return false;
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
/**
|
|
1709
|
+
* Build the ownership one retirement walk reads, filling in this run's own roots.
|
|
1710
|
+
*
|
|
1711
|
+
* @param {{allOwnedHookRootKeys?: Set<string>, homeDirectory?: string}} [options] Test overrides.
|
|
1712
|
+
* @returns {{allOwnedHookRootKeys: Set<string>, homeDirectory: string}} The ownership.
|
|
1713
|
+
*/
|
|
1714
|
+
function retiredHookOwnership(options = {}) {
|
|
1715
|
+
return {
|
|
1716
|
+
allOwnedHookRootKeys: options.allOwnedHookRootKeys
|
|
1717
|
+
?? ownedHookRootComparisonKeys(MANAGED_HOOK_ROOT_PATHS),
|
|
1718
|
+
homeDirectory: options.homeDirectory ?? homedir(),
|
|
1719
|
+
};
|
|
1720
|
+
}
|
|
1721
|
+
|
|
1722
|
+
/**
|
|
1723
|
+
* Report whether a host hook command runs a retired managed script under an
|
|
1724
|
+
* owned hooks directory.
|
|
1725
|
+
*
|
|
1726
|
+
* A path-plus-suffix, a command under another tool's tree, a non-string
|
|
1727
|
+
* command, and the inline validators-runner shape (no script name) are left
|
|
1728
|
+
* in place. The merge rewrites the validators-runner entry on every run.
|
|
1729
|
+
*
|
|
1730
|
+
* @param {unknown} commandString The hook command from a host configuration file.
|
|
1582
1731
|
* @param {Set<string>} retiredHookRelativePaths Retired script paths under hooks/.
|
|
1732
|
+
* @param {{allOwnedHookRootKeys: Set<string>, homeDirectory: string}} ownership The owned roots.
|
|
1583
1733
|
* @returns {boolean} True when the command runs a retired managed script.
|
|
1584
1734
|
*/
|
|
1585
|
-
function commandReferencesRetiredHook(commandString, retiredHookRelativePaths) {
|
|
1586
|
-
if (typeof commandString !== 'string') return false;
|
|
1587
|
-
const normalizedCommand = commandString.replace(/\\/g, '/');
|
|
1735
|
+
function commandReferencesRetiredHook(commandString, retiredHookRelativePaths, ownership) {
|
|
1588
1736
|
for (const relativePath of retiredHookRelativePaths) {
|
|
1589
|
-
if (
|
|
1737
|
+
if (commandNamesOwnedHookScript(commandString, relativePath, ownership)) return true;
|
|
1590
1738
|
}
|
|
1591
1739
|
return false;
|
|
1592
1740
|
}
|
|
@@ -1646,15 +1794,18 @@ function groupHookEntries(group) {
|
|
|
1646
1794
|
*
|
|
1647
1795
|
* @param {object} settings The parsed settings.json object (mutated in place).
|
|
1648
1796
|
* @param {Set<string>} retiredHookRelativePaths Retired script paths under hooks/.
|
|
1797
|
+
* @param {{allOwnedHookRootKeys: Set<string>, homeDirectory: string}} ownership The owned roots.
|
|
1649
1798
|
* @returns {number} How many hook entries were removed.
|
|
1650
1799
|
*/
|
|
1651
|
-
function stripRetiredHookEntries(settings, retiredHookRelativePaths) {
|
|
1800
|
+
function stripRetiredHookEntries(settings, retiredHookRelativePaths, ownership) {
|
|
1652
1801
|
let removedCount = 0;
|
|
1653
1802
|
for (const [eventType, matcherGroups] of Object.entries(settings.hooks)) {
|
|
1654
1803
|
if (!Array.isArray(matcherGroups)) continue;
|
|
1655
1804
|
const eventOutcome = retainedMatcherGroups(
|
|
1656
1805
|
matcherGroups,
|
|
1657
|
-
commandString => commandReferencesRetiredHook(
|
|
1806
|
+
commandString => commandReferencesRetiredHook(
|
|
1807
|
+
commandString, retiredHookRelativePaths, ownership,
|
|
1808
|
+
),
|
|
1658
1809
|
);
|
|
1659
1810
|
removedCount += eventOutcome.removedCount;
|
|
1660
1811
|
if (eventOutcome.keptGroups.length === 0) {
|
|
@@ -1666,35 +1817,256 @@ function stripRetiredHookEntries(settings, retiredHookRelativePaths) {
|
|
|
1666
1817
|
return removedCount;
|
|
1667
1818
|
}
|
|
1668
1819
|
|
|
1820
|
+
const DEFAULT_HOST_CONFIGURATION_INDENT = ' ';
|
|
1821
|
+
|
|
1669
1822
|
/**
|
|
1670
|
-
*
|
|
1671
|
-
* writing the file only when an entry left it.
|
|
1823
|
+
* Read the indent the host configuration file already uses.
|
|
1672
1824
|
*
|
|
1673
|
-
*
|
|
1674
|
-
*
|
|
1675
|
-
*
|
|
1825
|
+
* ~/.claude/settings.json uses four spaces and ~/.codex/hooks.json uses two.
|
|
1826
|
+
* Forcing four spaces would rewrite every line of one of those files. The first
|
|
1827
|
+
* indented line supplies the indent, tabs included.
|
|
1676
1828
|
*
|
|
1677
|
-
* @param {string}
|
|
1829
|
+
* @param {string} settingsText The host configuration file as it stands on disk.
|
|
1830
|
+
* @returns {string} The indent one nesting level uses.
|
|
1831
|
+
*/
|
|
1832
|
+
function hostConfigurationIndent(settingsText) {
|
|
1833
|
+
const firstIndentedLine = /\n([ \t]+)\S/.exec(settingsText);
|
|
1834
|
+
return firstIndentedLine ? firstIndentedLine[1] : DEFAULT_HOST_CONFIGURATION_INDENT;
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
/**
|
|
1838
|
+
* Remove every entry of one host hook configuration file that runs a retired
|
|
1839
|
+
* managed hook script, writing the file only when an entry left it.
|
|
1840
|
+
*
|
|
1841
|
+
* Claude settings.json and Codex hooks.json share this hook shape. A run that
|
|
1842
|
+
* retires no hook leaves the file byte-identical. A file the installer cannot
|
|
1843
|
+
* read or cannot parse is left alone with a warning, and the prune continues
|
|
1844
|
+
* with the other host configuration file.
|
|
1845
|
+
*
|
|
1846
|
+
* @param {string} settingsPath The absolute host configuration path.
|
|
1678
1847
|
* @param {Set<string>} retiredHookRelativePaths Retired script paths under hooks/.
|
|
1848
|
+
* @param {{allOwnedHookRootKeys?: Set<string>, homeDirectory?: string}} [options] Test overrides.
|
|
1679
1849
|
* @returns {number} How many hook entries were removed.
|
|
1680
1850
|
*/
|
|
1681
|
-
export function pruneRetiredHookEntriesFromSettings(
|
|
1851
|
+
export function pruneRetiredHookEntriesFromSettings(
|
|
1852
|
+
settingsPath, retiredHookRelativePaths, options = {},
|
|
1853
|
+
) {
|
|
1682
1854
|
if (retiredHookRelativePaths.size === 0) return 0;
|
|
1683
1855
|
if (!existsSync(settingsPath)) return 0;
|
|
1856
|
+
let settingsText;
|
|
1684
1857
|
let settings;
|
|
1685
1858
|
try {
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1859
|
+
settingsText = readFileSync(settingsPath, 'utf8');
|
|
1860
|
+
settings = JSON.parse(settingsText);
|
|
1861
|
+
} catch (readError) {
|
|
1862
|
+
console.warn(` Warning: leaving ${basename(settingsPath)} as it stands — the installer cannot read the file as JSON (${readError.message})`);
|
|
1689
1863
|
return 0;
|
|
1690
1864
|
}
|
|
1691
1865
|
if (!settings.hooks || typeof settings.hooks !== 'object') return 0;
|
|
1692
|
-
const removedCount = stripRetiredHookEntries(
|
|
1866
|
+
const removedCount = stripRetiredHookEntries(
|
|
1867
|
+
settings, retiredHookRelativePaths, retiredHookOwnership(options),
|
|
1868
|
+
);
|
|
1693
1869
|
if (removedCount === 0) return 0;
|
|
1694
|
-
|
|
1870
|
+
const settingsIndent = hostConfigurationIndent(settingsText);
|
|
1871
|
+
writeFileSync(settingsPath, JSON.stringify(settings, null, settingsIndent) + '\n');
|
|
1695
1872
|
return removedCount;
|
|
1696
1873
|
}
|
|
1697
1874
|
|
|
1875
|
+
const RETIRED_HOOK_STAND_IN_MARKER = 'claude-dev-env retired hook stand-in';
|
|
1876
|
+
|
|
1877
|
+
const RETIRED_HOOK_STAND_IN_SOURCE = `"""${RETIRED_HOOK_STAND_IN_MARKER}.
|
|
1878
|
+
|
|
1879
|
+
This install retired the hook that stood here and removed its settings.json
|
|
1880
|
+
registration. A session that started before the install still holds the old
|
|
1881
|
+
registration and runs this path on every matching tool call, so the path keeps an
|
|
1882
|
+
inert module: it does nothing and exits 0, which the harness reads as "allow".
|
|
1883
|
+
Restart the session to pick up the new registration. A later install removes this
|
|
1884
|
+
file once no registration names it.
|
|
1885
|
+
"""
|
|
1886
|
+
`;
|
|
1887
|
+
|
|
1888
|
+
const PYTHON_HOOK_SUFFIX = '.py';
|
|
1889
|
+
|
|
1890
|
+
|
|
1891
|
+
/**
|
|
1892
|
+
* List every hook command a settings object holds, across every event type.
|
|
1893
|
+
*
|
|
1894
|
+
* A group with no hooks array contributes nothing. An entry whose command is
|
|
1895
|
+
* absent or is not a string still reaches the caller.
|
|
1896
|
+
*
|
|
1897
|
+
* @param {object} settings The parsed settings.json object.
|
|
1898
|
+
* @returns {unknown[]} Every hook entry's command, in the order settings holds them.
|
|
1899
|
+
*/
|
|
1900
|
+
function settingsHookCommands(settings) {
|
|
1901
|
+
const allCommands = [];
|
|
1902
|
+
for (const matcherGroups of Object.values(settings.hooks ?? {})) {
|
|
1903
|
+
if (!Array.isArray(matcherGroups)) continue;
|
|
1904
|
+
for (const group of matcherGroups) {
|
|
1905
|
+
for (const hook of groupHookEntries(group) ?? []) allCommands.push(hook?.command);
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1908
|
+
return allCommands;
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
|
|
1912
|
+
/**
|
|
1913
|
+
* Read the hook commands one host configuration file holds, or none when it
|
|
1914
|
+
* cannot be read. Claude settings.json and Codex hooks.json share this shape.
|
|
1915
|
+
*
|
|
1916
|
+
* Call this before the run mutates settings. The hook merge rewrites
|
|
1917
|
+
* settings.json early, and by prune time the file no longer names what a
|
|
1918
|
+
* session started before this install still invokes.
|
|
1919
|
+
*
|
|
1920
|
+
* @param {string} settingsPath The absolute host configuration path.
|
|
1921
|
+
* @returns {unknown[]} Every hook command the file holds.
|
|
1922
|
+
*/
|
|
1923
|
+
export function settingsHookCommandsAtPath(settingsPath) {
|
|
1924
|
+
if (!existsSync(settingsPath)) return [];
|
|
1925
|
+
let settings;
|
|
1926
|
+
try {
|
|
1927
|
+
settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
1928
|
+
} catch {
|
|
1929
|
+
return [];
|
|
1930
|
+
}
|
|
1931
|
+
if (!settings?.hooks || typeof settings.hooks !== 'object') return [];
|
|
1932
|
+
return settingsHookCommands(settings);
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
|
|
1936
|
+
/**
|
|
1937
|
+
* Name the retired hook scripts a stale registration still runs.
|
|
1938
|
+
*
|
|
1939
|
+
* The commands come from the reading taken before the run mutated settings.json.
|
|
1940
|
+
* Those paths are where a stand-in belongs.
|
|
1941
|
+
*
|
|
1942
|
+
* @param {unknown[]} allCommands Hook commands read before the run mutated settings.
|
|
1943
|
+
* @param {Set<string>} retiredHookRelativePaths Retired script paths under hooks/.
|
|
1944
|
+
* @param {{allOwnedHookRootKeys?: Set<string>, homeDirectory?: string}} [options] Test overrides.
|
|
1945
|
+
* @returns {Set<string>} The retired paths a command names.
|
|
1946
|
+
*/
|
|
1947
|
+
export function retiredHookPathsNamedByCommands(
|
|
1948
|
+
allCommands, retiredHookRelativePaths, options = {},
|
|
1949
|
+
) {
|
|
1950
|
+
const ownership = retiredHookOwnership(options);
|
|
1951
|
+
const namedPaths = new Set();
|
|
1952
|
+
for (const relativePath of retiredHookRelativePaths) {
|
|
1953
|
+
const namesThisPath = allCommands.some(
|
|
1954
|
+
command => commandNamesOwnedHookScript(command, relativePath, ownership),
|
|
1955
|
+
);
|
|
1956
|
+
if (namesThisPath) namedPaths.add(relativePath);
|
|
1957
|
+
}
|
|
1958
|
+
return namedPaths;
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
|
|
1962
|
+
/**
|
|
1963
|
+
* Write an inert Python module at each retired hook path a stale registration
|
|
1964
|
+
* still runs.
|
|
1965
|
+
*
|
|
1966
|
+
* Deleting a registered script makes python3 exit 2. The harness treats a
|
|
1967
|
+
* PreToolUse exit 2 as a block, so every matching tool call is denied until
|
|
1968
|
+
* the session restarts. An inert module with one docstring exits 0, which the
|
|
1969
|
+
* harness treats as allow.
|
|
1970
|
+
*
|
|
1971
|
+
* An existing file is left in place. Each written path is recorded in the
|
|
1972
|
+
* manifest so a later install removes the stand-in once no registration names
|
|
1973
|
+
* it.
|
|
1974
|
+
*
|
|
1975
|
+
* @param {string} hooksRoot The absolute installed hooks directory.
|
|
1976
|
+
* @param {Set<string>} registeredRetiredRelativePaths Retired paths a registration names.
|
|
1977
|
+
* @returns {string[]} The absolute paths this run wrote a stand-in to.
|
|
1978
|
+
*/
|
|
1979
|
+
export function writeRetiredHookStandIns(hooksRoot, registeredRetiredRelativePaths) {
|
|
1980
|
+
const writtenPaths = [];
|
|
1981
|
+
for (const relativePath of registeredRetiredRelativePaths) {
|
|
1982
|
+
if (!relativePath.endsWith(PYTHON_HOOK_SUFFIX)) continue;
|
|
1983
|
+
const standInPath = join(hooksRoot, ...relativePath.split('/'));
|
|
1984
|
+
if (existsSync(standInPath)) continue;
|
|
1985
|
+
mkdirSync(dirname(standInPath), { recursive: true });
|
|
1986
|
+
writeFileSync(standInPath, RETIRED_HOOK_STAND_IN_SOURCE);
|
|
1987
|
+
writtenPaths.push(standInPath);
|
|
1988
|
+
}
|
|
1989
|
+
return writtenPaths;
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
|
|
1993
|
+
/**
|
|
1994
|
+
* List owned hooks directories that exist, one path per distinct real directory.
|
|
1995
|
+
*
|
|
1996
|
+
* On a healthy install the three roots are pointers to one directory. Keying
|
|
1997
|
+
* on the real path writes the stand-in once. A missing root is skipped.
|
|
1998
|
+
*
|
|
1999
|
+
* @param {string[]} allHookRootPaths The hooks directories this installer writes.
|
|
2000
|
+
* @returns {string[]} One path per distinct hooks directory that exists.
|
|
2001
|
+
*/
|
|
2002
|
+
function distinctExistingHookRoots(allHookRootPaths) {
|
|
2003
|
+
const allRootKeys = new Set();
|
|
2004
|
+
const allDistinctRoots = [];
|
|
2005
|
+
for (const hookRootPath of allHookRootPaths) {
|
|
2006
|
+
const realHookRootPath = realPathOrNull(hookRootPath);
|
|
2007
|
+
if (!realHookRootPath) continue;
|
|
2008
|
+
const rootKey = comparisonKeyForPath(realHookRootPath);
|
|
2009
|
+
if (allRootKeys.has(rootKey)) continue;
|
|
2010
|
+
allRootKeys.add(rootKey);
|
|
2011
|
+
allDistinctRoots.push(hookRootPath);
|
|
2012
|
+
}
|
|
2013
|
+
return allDistinctRoots;
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
/**
|
|
2017
|
+
* Leave an inert stand-in at each retired path under every owned hooks directory.
|
|
2018
|
+
*
|
|
2019
|
+
* @param {string[]} allHookRootPaths The hooks directories this installer writes.
|
|
2020
|
+
* @param {Set<string>} registeredRetiredRelativePaths Retired paths a registration names.
|
|
2021
|
+
* @returns {string[]} The absolute paths this run wrote a stand-in to.
|
|
2022
|
+
*/
|
|
2023
|
+
function writeRetiredHookStandInsAcrossOwnedRoots(
|
|
2024
|
+
allHookRootPaths, registeredRetiredRelativePaths,
|
|
2025
|
+
) {
|
|
2026
|
+
const allWrittenPaths = [];
|
|
2027
|
+
for (const hooksRoot of distinctExistingHookRoots(allHookRootPaths)) {
|
|
2028
|
+
allWrittenPaths.push(
|
|
2029
|
+
...writeRetiredHookStandIns(hooksRoot, registeredRetiredRelativePaths),
|
|
2030
|
+
);
|
|
2031
|
+
}
|
|
2032
|
+
return allWrittenPaths;
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
/**
|
|
2036
|
+
* Name each retired registration a stand-in now fills, by its path under hooks/.
|
|
2037
|
+
*
|
|
2038
|
+
* @param {string[]} allHookRootPaths The hooks directories this installer writes.
|
|
2039
|
+
* @param {string[]} standInPaths The stand-ins this run wrote.
|
|
2040
|
+
* @returns {string[]} The sorted relative names, one per registration.
|
|
2041
|
+
*/
|
|
2042
|
+
function standInRegistrationNames(allHookRootPaths, standInPaths) {
|
|
2043
|
+
const allNames = new Set();
|
|
2044
|
+
for (const standInPath of standInPaths) {
|
|
2045
|
+
const owningRoot = allHookRootPaths.find(
|
|
2046
|
+
hookRootPath => isInsideDirectory(resolve(standInPath), resolve(hookRootPath)),
|
|
2047
|
+
);
|
|
2048
|
+
if (!owningRoot) continue;
|
|
2049
|
+
allNames.add(relative(owningRoot, standInPath).replace(/\\/g, '/'));
|
|
2050
|
+
}
|
|
2051
|
+
return [...allNames].sort();
|
|
2052
|
+
}
|
|
2053
|
+
|
|
2054
|
+
/**
|
|
2055
|
+
* Print the one-line restart notice naming the registrations this run retired.
|
|
2056
|
+
*
|
|
2057
|
+
* @param {string[]} allHookRootPaths The hooks directories this installer writes.
|
|
2058
|
+
* @param {string[]} standInPaths The stand-ins this run wrote.
|
|
2059
|
+
* @returns {void}
|
|
2060
|
+
*/
|
|
2061
|
+
function reportRetiredHookStandIns(allHookRootPaths, standInPaths) {
|
|
2062
|
+
if (standInPaths.length === 0) return;
|
|
2063
|
+
const allNames = standInRegistrationNames(allHookRootPaths, standInPaths);
|
|
2064
|
+
console.log(
|
|
2065
|
+
` Restart open sessions: this install retired ${allNames.join(', ')} and left an inert stand-in at each path, so a session started before it keeps working until you restart it.`,
|
|
2066
|
+
);
|
|
2067
|
+
}
|
|
2068
|
+
|
|
2069
|
+
|
|
1698
2070
|
/**
|
|
1699
2071
|
* Load ~/.claude/settings.json for an in-place merge, or `{}` when absent/empty.
|
|
1700
2072
|
*
|
|
@@ -2000,46 +2372,78 @@ function pruneStaleFilesAcrossManagedRoots(priorInstalledFiles, currentInstalled
|
|
|
2000
2372
|
return { prunedCount, skillsPrunedCount, failedPaths };
|
|
2001
2373
|
}
|
|
2002
2374
|
|
|
2375
|
+
/**
|
|
2376
|
+
* Remove every retired registration from each host hook configuration file, and
|
|
2377
|
+
* report each file that lost one.
|
|
2378
|
+
*
|
|
2379
|
+
* Claude Code reads ~/.claude/settings.json and Codex reads ~/.codex/hooks.json.
|
|
2380
|
+
* A session of either host holds the registrations its own file named at start,
|
|
2381
|
+
* so a retirement that reaches only one file leaves the other host's open session
|
|
2382
|
+
* running a script this install moved.
|
|
2383
|
+
*
|
|
2384
|
+
* @param {string} settingsPath The absolute settings.json path.
|
|
2385
|
+
* @param {Set<string>} retiredHookPaths Retired script paths under hooks/.
|
|
2386
|
+
* @returns {void}
|
|
2387
|
+
*/
|
|
2388
|
+
function pruneRetiredHookRegistrations(settingsPath, retiredHookPaths) {
|
|
2389
|
+
const allConfigurationPaths = [settingsPath, CODEX_HOOKS_CONFIGURATION_PATH];
|
|
2390
|
+
for (const configurationPath of allConfigurationPaths) {
|
|
2391
|
+
const removedCount = pruneRetiredHookEntriesFromSettings(
|
|
2392
|
+
configurationPath, retiredHookPaths,
|
|
2393
|
+
);
|
|
2394
|
+
if (removedCount === 0) continue;
|
|
2395
|
+
console.log(
|
|
2396
|
+
` Hook entries: ${removedCount} retired entry(s) removed from ${basename(configurationPath)}`,
|
|
2397
|
+
);
|
|
2398
|
+
}
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2003
2401
|
/**
|
|
2004
2402
|
* Run every prune a full install performs, in the order that keeps ~/.claude
|
|
2005
2403
|
* consistent at each step.
|
|
2006
2404
|
*
|
|
2007
|
-
*
|
|
2008
|
-
*
|
|
2009
|
-
*
|
|
2010
|
-
*
|
|
2011
|
-
*
|
|
2012
|
-
* Both prunes report how much content reached the run's backup root, and their sum
|
|
2013
|
-
* is what backup retention answers to: the sweep of older recovery points runs
|
|
2014
|
-
* only once this run holds one of its own.
|
|
2405
|
+
* Retired hook entries leave settings before the script is moved, so a live
|
|
2406
|
+
* session does not invoke a missing file. Stand-ins are written last, after
|
|
2407
|
+
* the move has emptied those paths. Paths come from `priorSettingsHookCommands`,
|
|
2408
|
+
* read before the run mutated settings. The caller records each stand-in in
|
|
2409
|
+
* the manifest so a later install removes it once no registration names it.
|
|
2015
2410
|
*
|
|
2016
2411
|
* @param {Set<string>} copiedSkillNames Skill directory names this run wrote.
|
|
2017
2412
|
* @param {string[]|null} priorManifestSkills The prior manifest's skill names, or null.
|
|
2018
2413
|
* @param {string[]|null} priorManifestFiles The prior manifest's file list, or null.
|
|
2019
2414
|
* @param {string[]} installedFiles Every file this run copied.
|
|
2020
|
-
* @
|
|
2021
|
-
*
|
|
2022
|
-
*
|
|
2415
|
+
* @param {unknown[]} priorSettingsHookCommands Hook commands read before this run mutated settings.
|
|
2416
|
+
* @returns {{prunedCount: number, skillsPrunedCount: number, failedPaths: string[],
|
|
2417
|
+
* standInPaths: string[]}} The stale files moved across all roots, the skills
|
|
2418
|
+
* root's share, every path whose move failed, and every stand-in written.
|
|
2023
2419
|
*/
|
|
2024
2420
|
function runFullInstallPrunes(
|
|
2025
|
-
copiedSkillNames,
|
|
2421
|
+
copiedSkillNames,
|
|
2422
|
+
priorManifestSkills,
|
|
2423
|
+
priorManifestFiles,
|
|
2424
|
+
installedFiles,
|
|
2425
|
+
priorSettingsHookCommands,
|
|
2026
2426
|
) {
|
|
2427
|
+
const hooksRoot = join(CLAUDE_HOME, MANAGED_HOOKS_DIRECTORY_NAME);
|
|
2428
|
+
const settingsPath = join(CLAUDE_HOME, SETTINGS_FILE_NAME);
|
|
2027
2429
|
const retiredSkillMovedCount = pruneRetiredSkills(copiedSkillNames, priorManifestSkills);
|
|
2028
|
-
const
|
|
2029
|
-
|
|
2030
|
-
retiredManagedHookRelativePaths(
|
|
2031
|
-
priorManifestFiles, installedFiles, join(CLAUDE_HOME, MANAGED_HOOKS_DIRECTORY_NAME),
|
|
2032
|
-
),
|
|
2430
|
+
const retiredHookPaths = retiredManagedHookRelativePaths(
|
|
2431
|
+
priorManifestFiles, installedFiles, hooksRoot,
|
|
2033
2432
|
);
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2433
|
+
const registeredRetiredPaths = retiredHookPathsNamedByCommands(
|
|
2434
|
+
priorSettingsHookCommands, retiredHookPaths,
|
|
2435
|
+
);
|
|
2436
|
+
pruneRetiredHookRegistrations(settingsPath, retiredHookPaths);
|
|
2037
2437
|
const staleOutcome = pruneStaleFilesAcrossManagedRoots(
|
|
2038
2438
|
priorManifestFiles, installedFiles, currentRunBackupRoot(),
|
|
2039
2439
|
);
|
|
2440
|
+
const standInPaths = writeRetiredHookStandInsAcrossOwnedRoots(
|
|
2441
|
+
MANAGED_HOOK_ROOT_PATHS, registeredRetiredPaths,
|
|
2442
|
+
);
|
|
2443
|
+
reportRetiredHookStandIns(MANAGED_HOOK_ROOT_PATHS, standInPaths);
|
|
2040
2444
|
const didRunMoveContent = retiredSkillMovedCount + staleOutcome.prunedCount > 0;
|
|
2041
2445
|
retainNewestRunBackupOnly(currentRunBackupRoot(), didRunMoveContent);
|
|
2042
|
-
return staleOutcome;
|
|
2446
|
+
return { ...staleOutcome, standInPaths };
|
|
2043
2447
|
}
|
|
2044
2448
|
|
|
2045
2449
|
/**
|
|
@@ -2140,6 +2544,10 @@ function executeInstallPlan(plan) {
|
|
|
2140
2544
|
*/
|
|
2141
2545
|
function executeInstallPlanMutations(plan, transactionHelpers) {
|
|
2142
2546
|
const { throwIfFault, syncWrittenPaths } = transactionHelpers;
|
|
2547
|
+
const priorSettingsHookCommands = [
|
|
2548
|
+
...settingsHookCommandsAtPath(join(CLAUDE_HOME, SETTINGS_FILE_NAME)),
|
|
2549
|
+
...settingsHookCommandsAtPath(CODEX_HOOKS_CONFIGURATION_PATH),
|
|
2550
|
+
];
|
|
2143
2551
|
const selectedGroups = plan.selectedGroups;
|
|
2144
2552
|
const priorManifestFiles = plan.priorManifest.files;
|
|
2145
2553
|
const priorManifestSkills = plan.priorManifest.skills;
|
|
@@ -2487,11 +2895,16 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
|
|
|
2487
2895
|
if (didPruneRun) {
|
|
2488
2896
|
try {
|
|
2489
2897
|
const prunes = runFullInstallPrunes(
|
|
2490
|
-
copiedSkillNames,
|
|
2898
|
+
copiedSkillNames,
|
|
2899
|
+
priorManifestSkills,
|
|
2900
|
+
priorManifestFiles,
|
|
2901
|
+
allInstalledFiles,
|
|
2902
|
+
priorSettingsHookCommands,
|
|
2491
2903
|
);
|
|
2492
2904
|
summary.skills.pruned = prunes.skillsPrunedCount;
|
|
2493
2905
|
stalePrunedTotal = prunes.prunedCount;
|
|
2494
2906
|
failedPrunePaths = prunes.failedPaths;
|
|
2907
|
+
allInstalledFiles.push(...prunes.standInPaths);
|
|
2495
2908
|
didPruneFinish = true;
|
|
2496
2909
|
} catch (pruneError) {
|
|
2497
2910
|
console.warn(
|
|
@@ -2835,7 +3248,6 @@ ${groupLines}
|
|
|
2835
3248
|
|
|
2836
3249
|
Examples:
|
|
2837
3250
|
npx ${PACKAGE_NAME} --only core
|
|
2838
|
-
npx ${PACKAGE_NAME} --only core,journal
|
|
2839
3251
|
npx ${PACKAGE_NAME} --profile editor --only core
|
|
2840
3252
|
npx ${PACKAGE_NAME} --profiles editor,mel --only core
|
|
2841
3253
|
|
|
@@ -2845,6 +3257,9 @@ Named profiles resolve under LLM_SETTINGS_PROFILES_ROOT or ~/.claude-profiles/<d
|
|
|
2845
3257
|
Codex exec-policy files copy into ~/.codex/rules, or CODEX_HOME/rules when CODEX_HOME is set.
|
|
2846
3258
|
Cursor rule files generate into ~/.cursor/rules as stem-named mdc files, one per Claude rule.
|
|
2847
3259
|
|
|
3260
|
+
The installer reads only flags. A bare path argument carries no meaning, so
|
|
3261
|
+
npx ${PACKAGE_NAME} . lands in the same root as a run with no argument.
|
|
3262
|
+
|
|
2848
3263
|
Root precedence: --target > CLAUDE_CONFIG_DIR > ~/.claude
|
|
2849
3264
|
Profile selection (--profile/--profiles) is mutually exclusive with --target.
|
|
2850
3265
|
Target selection finishes before any mutation. Each target writes its own manifest
|