claude-dev-env 1.92.0 → 1.93.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.
Files changed (216) hide show
  1. package/CLAUDE.md +5 -39
  2. package/_shared/CLAUDE.md +2 -1
  3. package/_shared/advisor/CLAUDE.md +21 -0
  4. package/_shared/advisor/advisor-protocol.md +142 -0
  5. package/_shared/advisor/scripts/config/advisor_scripts_constants/__init__.py +1 -0
  6. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +86 -0
  7. package/_shared/advisor/scripts/model_tier_run_validator.py +246 -0
  8. package/_shared/advisor/scripts/pyproject.toml +3 -0
  9. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +302 -0
  10. package/_shared/advisor/scripts/tests/test_tier_model_ids.py +180 -0
  11. package/_shared/advisor/scripts/tier_model_ids.py +164 -0
  12. package/_shared/pr-loop/CLAUDE.md +1 -0
  13. package/_shared/pr-loop/precatch-rubric.md +65 -0
  14. package/_shared/pr-loop/scripts/CLAUDE.md +1 -1
  15. package/_shared/pr-loop/scripts/code_rules_gate.py +137 -5
  16. package/_shared/pr-loop/scripts/post_audit_thread.py +1 -1
  17. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -1
  18. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +16 -0
  19. package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +2 -1
  20. package/_shared/pr-loop/scripts/reviewer_availability.py +42 -12
  21. package/_shared/pr-loop/scripts/reviews_disabled.py +65 -21
  22. package/_shared/pr-loop/scripts/terminology_sweep.py +69 -21
  23. package/_shared/pr-loop/scripts/tests/CLAUDE.md +1 -1
  24. package/_shared/pr-loop/scripts/tests/fixtures/{copilot_internal_user_jonecho.json → copilot_internal_user_example.json} +1 -1
  25. package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +1 -1
  26. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +146 -2
  27. package/_shared/pr-loop/scripts/tests/test_copilot_quota.py +8 -8
  28. package/_shared/pr-loop/scripts/tests/test_reviewer_availability.py +18 -5
  29. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +56 -5
  30. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +104 -17
  31. package/agents/CLAUDE.md +3 -2
  32. package/agents/clasp-deployment-orchestrator.md +2 -2
  33. package/agents/code-advisor.md +7 -5
  34. package/agents/code-verifier.md +6 -2
  35. package/agents/session-advisor.md +27 -0
  36. package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +1 -1
  37. package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +1 -1
  38. package/audit-rubrics/prompts/category-a-api-contracts.md +2 -2
  39. package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
  40. package/audit-rubrics/prompts/category-c-resource-cleanup.md +2 -2
  41. package/audit-rubrics/prompts/category-d-scoping-and-ordering.md +2 -2
  42. package/audit-rubrics/prompts/category-e-dead-code.md +2 -2
  43. package/audit-rubrics/prompts/category-f-silent-failures.md +2 -2
  44. package/audit-rubrics/prompts/category-g-bounds-and-overflow.md +2 -2
  45. package/audit-rubrics/prompts/category-h-security-boundaries.md +2 -2
  46. package/audit-rubrics/prompts/category-i-concurrency.md +2 -2
  47. package/audit-rubrics/prompts/category-j-code-rules-compliance.md +2 -2
  48. package/audit-rubrics/prompts/category-k-codebase-conflicts.md +2 -2
  49. package/audit-rubrics/prompts/category-l-behavior-equivalence.md +2 -2
  50. package/audit-rubrics/prompts/category-m-producer-consumer-cardinality.md +2 -2
  51. package/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md +2 -2
  52. package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -2
  53. package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +2 -2
  54. package/bin/CLAUDE.md +1 -0
  55. package/bin/expand_home_directory_tokens.mjs +75 -0
  56. package/bin/install.mjs +21 -7
  57. package/bin/install.test.mjs +220 -10
  58. package/hooks/CLAUDE.md +1 -1
  59. package/hooks/blocking/CLAUDE.md +4 -1
  60. package/hooks/blocking/_path_setup.py +13 -0
  61. package/hooks/blocking/bash_pre_tool_use_dispatcher.py +281 -0
  62. package/hooks/blocking/code_rules_dead_module_constant.py +88 -16
  63. package/hooks/blocking/code_rules_enforcer.py +6 -0
  64. package/hooks/blocking/code_rules_imports_logging.py +34 -1
  65. package/hooks/blocking/code_rules_paths_syspath.py +1 -1
  66. package/hooks/blocking/code_rules_test_layout.py +272 -0
  67. package/hooks/blocking/nas_ssh_binary_enforcer.py +22 -12
  68. package/hooks/blocking/pii_prevention_blocker.py +781 -0
  69. package/hooks/blocking/pii_scanner.py +340 -0
  70. package/hooks/blocking/pre_tool_use_dispatcher.py +3 -3
  71. package/hooks/blocking/stop_dispatcher.py +84 -0
  72. package/hooks/blocking/test__path_setup.py +39 -0
  73. package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +61 -0
  74. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +294 -0
  75. package/hooks/blocking/test_code_rules_enforcer_cross_skill_duplicate.py +1 -1
  76. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +51 -0
  77. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_read_cap.py +103 -0
  78. package/hooks/blocking/test_code_rules_enforcer_dot_test_pattern.py +1 -1
  79. package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +1 -1
  80. package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +11 -11
  81. package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +1 -1
  82. package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +1 -1
  83. package/hooks/blocking/test_code_rules_enforcer_test_layout.py +111 -0
  84. package/hooks/blocking/test_code_rules_enforcer_type_checking_scope.py +76 -0
  85. package/hooks/blocking/test_destructive_command_blocker.py +29 -9
  86. package/hooks/blocking/test_gh_body_arg_blocker.py +3 -3
  87. package/hooks/blocking/test_nas_ssh_binary_enforcer.py +96 -39
  88. package/hooks/blocking/test_pii_prevention_blocker.py +905 -0
  89. package/hooks/blocking/test_pii_prevention_windows_git_path.py +41 -0
  90. package/hooks/blocking/test_pii_scanner.py +165 -0
  91. package/hooks/blocking/test_pr_description_enforcer_pr_number.py +1 -1
  92. package/hooks/blocking/test_pre_tool_use_dispatcher.py +14 -13
  93. package/hooks/blocking/test_stop_dispatcher.py +187 -0
  94. package/hooks/blocking/test_verdict_directory_write_blocker.py +1 -1
  95. package/hooks/blocking/test_volatile_path_in_post_blocker.py +3 -3
  96. package/hooks/blocking/volatile_path_in_post_blocker.py +54 -5
  97. package/hooks/diagnostic/migrations/README.md +25 -26
  98. package/hooks/diagnostic/test_hook_log_extractor.py +10 -10
  99. package/hooks/hooks.json +10 -120
  100. package/hooks/hooks_constants/CLAUDE.md +12 -3
  101. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +78 -0
  102. package/hooks/hooks_constants/dead_module_constant_constants.py +1 -0
  103. package/hooks/hooks_constants/hosted_hook_runner.py +73 -0
  104. package/hooks/hooks_constants/local_identity.py +182 -0
  105. package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +4 -37
  106. package/hooks/hooks_constants/pii_prevention_constants.py +299 -0
  107. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
  108. package/hooks/hooks_constants/python_style_checks_constants.py +15 -0
  109. package/hooks/hooks_constants/stop_dispatcher_constants.py +26 -0
  110. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +87 -0
  111. package/hooks/hooks_constants/test_hardcoded_user_path_constants.py +2 -2
  112. package/hooks/hooks_constants/test_hosted_hook_runner.py +101 -0
  113. package/hooks/hooks_constants/test_layout_constants.py +28 -0
  114. package/hooks/hooks_constants/test_local_identity.py +88 -0
  115. package/hooks/hooks_constants/test_session_env_cleanup_constants.py +1 -1
  116. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +32 -0
  117. package/hooks/validators/README.md +5 -6
  118. package/hooks/validators/__init__.py +2 -2
  119. package/hooks/validators/python_style_checks.py +298 -243
  120. package/hooks/validators/run_all_validators.py +6 -0
  121. package/hooks/validators/test_python_style_checks.py +278 -163
  122. package/package.json +2 -2
  123. package/rules/CLAUDE.md +7 -1
  124. package/rules/bdd.md +1 -1
  125. package/rules/nas-ssh-invocation.md +6 -4
  126. package/rules/no-justification-noise.md +61 -0
  127. package/rules/testing.md +0 -2
  128. package/scripts/test_setup_project_paths.py +1 -1
  129. package/skills/CLAUDE.md +6 -3
  130. package/skills/_shared/pr-loop/scripts/_path_resolver.py +2 -2
  131. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +1 -1
  132. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +1 -1
  133. package/skills/_shared/pr-loop/scripts/test__path_resolver.py +42 -3
  134. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +11 -11
  135. package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +26 -3
  136. package/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +49 -25
  137. package/skills/autoconverge/CLAUDE.md +3 -3
  138. package/skills/autoconverge/SKILL.md +113 -35
  139. package/skills/autoconverge/reference/CLAUDE.md +2 -2
  140. package/skills/autoconverge/reference/convergence.md +44 -22
  141. package/skills/autoconverge/reference/gotchas.md +11 -0
  142. package/skills/autoconverge/reference/stop-conditions.md +23 -13
  143. package/skills/autoconverge/workflow/CLAUDE.md +2 -1
  144. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +15 -19
  145. package/skills/autoconverge/workflow/converge.contract.test.mjs +43 -42
  146. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +141 -16
  147. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +2 -2
  148. package/skills/autoconverge/workflow/converge.mjs +329 -68
  149. package/skills/autoconverge/workflow/converge.path-aware.test.mjs +1 -1
  150. package/skills/autoconverge/workflow/converge.precatch.test.mjs +152 -0
  151. package/skills/autoconverge/workflow/converge.run-input.test.mjs +3 -3
  152. package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +15 -15
  153. package/skills/autoconverge/workflow/test_convergence_summary.py +6 -6
  154. package/skills/bdd-protocol/SKILL.md +4 -5
  155. package/skills/bdd-protocol/references/anti-patterns.md +1 -1
  156. package/skills/bugteam/SKILL.md +8 -0
  157. package/skills/bugteam/reference/copilot-gap-analysis.md +20 -20
  158. package/skills/bugteam/reference/obstacles/audit-walk-categories.md +1 -1
  159. package/skills/bugteam/reference/team-setup.md +7 -5
  160. package/skills/bugteam/scripts/reflow_skill_md.py +1 -1
  161. package/skills/copilot-finding-triage/SKILL.md +124 -0
  162. package/skills/copilot-finding-triage/reference/tier-rubric.md +49 -0
  163. package/skills/copilot-finding-triage/scripts/conftest.py +8 -0
  164. package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/__init__.py +0 -0
  165. package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/config/__init__.py +0 -0
  166. package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/config/notify_ntfy_constants.py +32 -0
  167. package/skills/copilot-finding-triage/scripts/notify_ntfy.py +192 -0
  168. package/skills/copilot-finding-triage/scripts/test_notify_ntfy.py +141 -0
  169. package/skills/copilot-finding-triage/templates/notification.md +34 -0
  170. package/skills/copilot-review/SKILL.md +8 -6
  171. package/skills/findbugs/SKILL.md +4 -0
  172. package/skills/fixbugs/SKILL.md +8 -7
  173. package/skills/gotcha/CLAUDE.md +2 -2
  174. package/skills/gotcha/SKILL.md +4 -4
  175. package/skills/log-audit/SKILL.md +4 -6
  176. package/skills/monitor-open-prs/CLAUDE.md +1 -1
  177. package/skills/monitor-open-prs/SKILL.md +6 -1
  178. package/skills/orchestrator/SKILL.md +184 -0
  179. package/skills/orchestrator-refresh/SKILL.md +37 -0
  180. package/skills/post-audit-findings/SKILL.md +5 -9
  181. package/skills/pr-consistency-audit/SKILL.md +5 -1
  182. package/skills/pr-converge/CLAUDE.md +1 -1
  183. package/skills/pr-converge/SKILL.md +86 -47
  184. package/skills/pr-converge/reference/CLAUDE.md +1 -1
  185. package/skills/pr-converge/reference/convergence-gates.md +20 -17
  186. package/skills/pr-converge/reference/examples.md +63 -47
  187. package/skills/pr-converge/reference/fix-protocol.md +15 -15
  188. package/skills/pr-converge/reference/ground-rules.md +11 -7
  189. package/skills/pr-converge/reference/multi-pr-orchestration.md +6 -5
  190. package/skills/pr-converge/reference/per-tick.md +129 -107
  191. package/skills/pr-converge/reference/state-schema.md +15 -10
  192. package/skills/pr-converge/scripts/check_convergence.py +5 -4
  193. package/skills/pr-converge/scripts/test_check_convergence.py +6 -4
  194. package/skills/pr-fix-protocol/SKILL.md +3 -8
  195. package/skills/pr-loop-cloud-transport/SKILL.md +102 -0
  196. package/skills/pr-loop-cloud-transport/reference/identity-and-hooks.md +40 -0
  197. package/skills/pr-loop-cloud-transport/reference/substitution-matrix.md +48 -0
  198. package/skills/pr-loop-lifecycle/SKILL.md +5 -9
  199. package/skills/pr-scope-resolve/SKILL.md +3 -6
  200. package/skills/privacy-hygiene/SKILL.md +114 -0
  201. package/skills/qbug/SKILL.md +8 -8
  202. package/skills/rebase/SKILL.md +5 -1
  203. package/skills/refine/SKILL.md +4 -5
  204. package/skills/reviewer-gates/SKILL.md +7 -11
  205. package/skills/session-log/SKILL.md +4 -1
  206. package/skills/skill-builder/SKILL.md +3 -6
  207. package/skills/structure-prompt/SKILL.md +4 -5
  208. package/skills/team-advisor/SKILL.md +32 -164
  209. package/skills/test_markdown_link_integrity.py +10 -6
  210. package/skills/update/SKILL.md +5 -1
  211. package/skills/usage-pause/SKILL.md +14 -5
  212. package/skills/usage-pause/scripts/resolve_usage_window.py +83 -5
  213. package/skills/usage-pause/scripts/test_resolve_usage_window.py +185 -17
  214. package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +4 -3
  215. package/skills/verified-build/SKILL.md +4 -9
  216. package/skills/team-advisor-refresh/SKILL.md +0 -25
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Install-time expansion of $HOME / ${HOME} / ~/ in settings.json commands.
3
+ *
4
+ * Hosts that require referenced env vars at hook load time (for example Grok)
5
+ * skip the hook when HOME is unset — common on Windows — so the installer
6
+ * rewrites residual tokens to absolute home paths.
7
+ */
8
+
9
+ /**
10
+ * Expands $HOME, ${HOME}, and ~/ in a command string to an absolute home directory
11
+ * using forward slashes.
12
+ *
13
+ * Replacement uses split/join and function replacers so a home path that contains
14
+ * `$` (for example `$&` or `$1`) is inserted literally and is never treated as a
15
+ * String.prototype.replace special pattern.
16
+ *
17
+ * ::
18
+ *
19
+ * expandHomeDirectoryTokens('python $HOME/a.py', 'C:\\Users\\x')
20
+ * # -> 'python C:/Users/x/a.py'
21
+ *
22
+ * expandHomeDirectoryTokens('python $HOME/a.py', 'C:/Users/$&evil')
23
+ * # -> 'python C:/Users/$&evil/a.py' (literal dollars, not regex rewrite)
24
+ *
25
+ * @param {string} commandString Hook or statusLine command text.
26
+ * @param {string} homeDirectory Absolute home directory (any path separators).
27
+ * @returns {string} Command with home tokens expanded.
28
+ */
29
+ export function expandHomeDirectoryTokens(commandString, homeDirectory) {
30
+ const normalizedHome = homeDirectory.replace(/\\/g, '/').replace(/\/+$/, '');
31
+ let expandedCommand = commandString.split('${HOME}').join(normalizedHome);
32
+ expandedCommand = expandedCommand.replace(
33
+ /(?<![A-Za-z0-9_])\$HOME\b/g,
34
+ () => normalizedHome,
35
+ );
36
+ expandedCommand = expandedCommand.replace(
37
+ /(^|[\s"'=])~\//g,
38
+ (_fullMatch, boundary) => `${boundary}${normalizedHome}/`,
39
+ );
40
+ return expandedCommand;
41
+ }
42
+
43
+ /**
44
+ * Expands home-directory tokens in every settings.json hook command and in the
45
+ * optional statusLine command so residual $HOME entries from older installs
46
+ * become absolute paths and no longer require HOME at hook load time.
47
+ *
48
+ * @param {object} settings The parsed settings.json object (mutated in place).
49
+ * @param {string} homeDirectory Absolute home directory (any path separators).
50
+ * @returns {void}
51
+ */
52
+ export function expandHomeDirectoryTokensInSettings(settings, homeDirectory) {
53
+ if (settings.hooks) {
54
+ for (const matcherGroups of Object.values(settings.hooks)) {
55
+ if (!Array.isArray(matcherGroups)) continue;
56
+ for (const eachGroup of matcherGroups) {
57
+ if (!eachGroup || !Array.isArray(eachGroup.hooks)) continue;
58
+ for (const eachHook of eachGroup.hooks) {
59
+ if (typeof eachHook.command === 'string') {
60
+ eachHook.command = expandHomeDirectoryTokens(
61
+ eachHook.command,
62
+ homeDirectory,
63
+ );
64
+ }
65
+ }
66
+ }
67
+ }
68
+ }
69
+ if (settings.statusLine && typeof settings.statusLine.command === 'string') {
70
+ settings.statusLine.command = expandHomeDirectoryTokens(
71
+ settings.statusLine.command,
72
+ homeDirectory,
73
+ );
74
+ }
75
+ }
package/bin/install.mjs CHANGED
@@ -8,6 +8,7 @@ import { fileURLToPath } from 'node:url';
8
8
  import { createRequire } from 'node:module';
9
9
  import { installAllGitHooks } from './git_hooks_installer.mjs';
10
10
  import { installMypyIniForClaudeHooks } from './install_mypy_ini.mjs';
11
+ import { expandHomeDirectoryTokensInSettings } from './expand_home_directory_tokens.mjs';
11
12
 
12
13
  const CLAUDE_HOME = join(homedir(), '.claude');
13
14
  const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..');
@@ -18,6 +19,10 @@ const packageRequire = createRequire(import.meta.url);
18
19
 
19
20
  export const CONTENT_DIRECTORIES = ['rules', 'docs', 'commands', 'agents', 'system-prompts', 'scripts', '_shared', 'audit-rubrics'];
20
21
 
22
+ export const CORE_INCLUDE_DIRECTORIES = [
23
+ 'rules', 'docs', 'commands', 'agents', 'audit-rubrics', '_shared', 'scripts',
24
+ ];
25
+
21
26
  export function collectPackageSourceConflicts(packageDirectory) {
22
27
  const gitConflictStatusCodes = new Set(['DD', 'AU', 'UD', 'UA', 'DU', 'AA', 'UU']);
23
28
  const porcelainStatusLineMinLength = 4;
@@ -147,12 +152,13 @@ const INSTALL_GROUPS = {
147
152
  core: {
148
153
  description: 'Development standards, hooks, agents, commands',
149
154
  skills: [
150
- 'team-advisor', 'team-advisor-refresh',
155
+ 'orchestrator', 'orchestrator-refresh', 'team-advisor',
151
156
  'anthropic-plan', 'everything-search',
152
157
  'pr-review-responder',
158
+ 'privacy-hygiene',
153
159
  'recall', 'remember', 'task-build', 'verified-build'
154
160
  ],
155
- includeDirectories: ['rules', 'docs', 'commands', 'agents', 'audit-rubrics'],
161
+ includeDirectories: CORE_INCLUDE_DIRECTORIES,
156
162
  includeAllHooks: true,
157
163
  },
158
164
  journal: {
@@ -507,17 +513,21 @@ function pruneManagedHooksFromEvent(settings, eventType, managedHookRelativePath
507
513
  * pruning every prior managed hook (standalone script or inline validators
508
514
  * runner) from each event's existing matcher groups before appending the freshly
509
515
  * rewritten copies so repeated merges stay idempotent and a managed hook moved to
510
- * a new matcher group does not double-run. User-authored hooks are preserved
511
- * untouched.
516
+ * a new matcher group does not double-run. User-authored hooks are preserved as
517
+ * entries, but residual $HOME / ${HOME} / ~/ tokens in every hook and statusLine
518
+ * command are expanded to absolute home paths so hosts that require referenced
519
+ * env vars at load time (Grok on Windows) can execute them.
512
520
  *
513
521
  * @param {object} settings The parsed settings.json object (mutated in place).
514
522
  * @param {{hooks: object}} hooksConfig Parsed hooks.json.
515
- * @param {string} pluginRootDir Directory ${CLAUDE_PLUGIN_ROOT} resolves to.
523
+ * @param {string} pluginRootDir Directory ${CLAUDE_PLUGIN_ROOT} resolves to
524
+ * (the installer's `~/.claude` root; home is its parent directory).
516
525
  * @param {string} pythonCommand Interpreter command that replaces python3.
517
526
  * @returns {number} Count of matcher groups merged.
518
527
  */
519
528
  export function mergeHooksIntoSettings(settings, hooksConfig, pluginRootDir, pythonCommand) {
520
529
  const managedHookRelativePaths = managedHookScriptRelativePaths(hooksConfig);
530
+ const pluginRootForward = pluginRootDir.replace(/\\/g, '/');
521
531
  if (!settings.hooks) settings.hooks = {};
522
532
  let groupCount = 0;
523
533
  for (const [eventType, matcherGroups] of Object.entries(hooksConfig.hooks)) {
@@ -526,8 +536,11 @@ export function mergeHooksIntoSettings(settings, hooksConfig, pluginRootDir, pyt
526
536
  for (const sourceGroup of matcherGroups) {
527
537
  const rewrittenHooks = sourceGroup.hooks.map(hook => {
528
538
  let command = hook.command;
529
- command = command.replace(/\$\{CLAUDE_PLUGIN_ROOT\}/g, pluginRootDir.replace(/\\/g, '/'));
530
- command = command.replace(/^python3\b/, pythonCommand);
539
+ command = command.replace(
540
+ /\$\{CLAUDE_PLUGIN_ROOT\}/g,
541
+ () => pluginRootForward,
542
+ );
543
+ command = command.replace(/^python3\b/, () => pythonCommand);
531
544
  return { ...hook, command };
532
545
  });
533
546
  const existingIndex = settings.hooks[eventType].findIndex(
@@ -548,6 +561,7 @@ export function mergeHooksIntoSettings(settings, hooksConfig, pluginRootDir, pyt
548
561
  groupCount++;
549
562
  }
550
563
  }
564
+ expandHomeDirectoryTokensInSettings(settings, dirname(pluginRootDir));
551
565
  return groupCount;
552
566
  }
553
567
 
@@ -9,6 +9,7 @@ import { pathToFileURL } from 'node:url';
9
9
  import {
10
10
  collectPackageSourceConflicts,
11
11
  CONTENT_DIRECTORIES,
12
+ CORE_INCLUDE_DIRECTORIES,
12
13
  FOLDED_HOOK_RELATIVE_PATHS,
13
14
  POST_FOLDED_HOOK_RELATIVE_PATHS,
14
15
  pythonCandidatesForPlatform,
@@ -21,6 +22,10 @@ import {
21
22
  mergeHooksIntoSettings,
22
23
  pruneManagedHooksFromSettings,
23
24
  } from './install.mjs';
25
+ import {
26
+ expandHomeDirectoryTokens,
27
+ expandHomeDirectoryTokensInSettings,
28
+ } from './expand_home_directory_tokens.mjs';
24
29
 
25
30
 
26
31
  function createTemporaryGitRepository() {
@@ -150,6 +155,18 @@ test('CONTENT_DIRECTORIES includes _shared so installer copies _shared/pr-loop/
150
155
  });
151
156
 
152
157
 
158
+ test('core includeDirectories ships _shared and scripts for advisor protocol and CLI fallback', () => {
159
+ assert.ok(
160
+ CORE_INCLUDE_DIRECTORIES.includes('_shared'),
161
+ '_shared must ship with --only core so advisor-protocol.md lands for team-advisor/orchestrator',
162
+ );
163
+ assert.ok(
164
+ CORE_INCLUDE_DIRECTORIES.includes('scripts'),
165
+ 'scripts must ship with --only core so claude_chain_runner.py is available for advisor CLI fallback',
166
+ );
167
+ });
168
+
169
+
153
170
  test('CONTENT_DIRECTORIES includes audit-rubrics so installer copies category rubrics and prompts to ~/.claude/audit-rubrics/', () => {
154
171
  assert.ok(
155
172
  CONTENT_DIRECTORIES.includes('audit-rubrics'),
@@ -214,7 +231,7 @@ test('isWindowsStorePythonStub flags the Microsoft Store WindowsApps alias paths
214
231
  true,
215
232
  );
216
233
  assert.equal(
217
- isWindowsStorePythonStub('C:/Users/jon/AppData/Local/Microsoft/WindowsApps/python.exe'),
234
+ isWindowsStorePythonStub('C:/Users/example/AppData/Local/Microsoft/WindowsApps/python.exe'),
218
235
  true,
219
236
  );
220
237
  });
@@ -239,6 +256,199 @@ test('interpreterCommandFromPath quotes an interpreter path that contains a spac
239
256
  });
240
257
 
241
258
 
259
+ test('expandHomeDirectoryTokens expands $HOME, ${HOME}, and ~/', () => {
260
+ assert.equal(
261
+ expandHomeDirectoryTokens(
262
+ 'python $HOME/.claude/hooks/session/fix_worktree_hookspath.py',
263
+ 'C:\\Users\\x',
264
+ ),
265
+ 'python C:/Users/x/.claude/hooks/session/fix_worktree_hookspath.py',
266
+ );
267
+ assert.equal(
268
+ expandHomeDirectoryTokens('python ${HOME}/.claude/hooks/a.py', '/home/x'),
269
+ 'python /home/x/.claude/hooks/a.py',
270
+ );
271
+ assert.equal(
272
+ expandHomeDirectoryTokens('python ~/.claude/hooks/a.py', '/home/x'),
273
+ 'python /home/x/.claude/hooks/a.py',
274
+ );
275
+ assert.equal(
276
+ expandHomeDirectoryTokens('echo $HOMEPATH', 'C:/Users/x'),
277
+ 'echo $HOMEPATH',
278
+ );
279
+ });
280
+
281
+
282
+ test('expandHomeDirectoryTokens inserts dollar characters in home paths literally', () => {
283
+ const homeWithReplaceMetacharacters = 'C:/Users/$&evil$1';
284
+ assert.equal(
285
+ expandHomeDirectoryTokens('python $HOME/.claude/a.py', homeWithReplaceMetacharacters),
286
+ 'python C:/Users/$&evil$1/.claude/a.py',
287
+ );
288
+ assert.equal(
289
+ expandHomeDirectoryTokens('python ${HOME}/.claude/a.py', homeWithReplaceMetacharacters),
290
+ 'python C:/Users/$&evil$1/.claude/a.py',
291
+ );
292
+ assert.equal(
293
+ expandHomeDirectoryTokens('python ~/.claude/a.py', homeWithReplaceMetacharacters),
294
+ 'python C:/Users/$&evil$1/.claude/a.py',
295
+ );
296
+ });
297
+
298
+
299
+ test('expandHomeDirectoryTokens expands ${HOME} before $HOME so braces stay intact', () => {
300
+ assert.equal(
301
+ expandHomeDirectoryTokens('python ${HOME}/.claude/a.py', '/home/x'),
302
+ 'python /home/x/.claude/a.py',
303
+ );
304
+ });
305
+
306
+
307
+ test('expandHomeDirectoryTokens strips trailing slashes from the home directory', () => {
308
+ assert.equal(
309
+ expandHomeDirectoryTokens('python $HOME/.claude/a.py', 'C:/Users/x/'),
310
+ 'python C:/Users/x/.claude/a.py',
311
+ );
312
+ });
313
+
314
+
315
+ test('expandHomeDirectoryTokensInSettings skips non-array hook event values', () => {
316
+ const settings = {
317
+ hooks: {
318
+ SessionStart: 'not-an-array',
319
+ PreToolUse: [
320
+ {
321
+ matcher: 'Write',
322
+ hooks: [{ type: 'command', command: 'python $HOME/.claude/hooks/a.py' }],
323
+ },
324
+ ],
325
+ },
326
+ };
327
+ expandHomeDirectoryTokensInSettings(settings, 'C:/Users/x');
328
+ assert.equal(settings.hooks.SessionStart, 'not-an-array');
329
+ assert.equal(
330
+ settings.hooks.PreToolUse[0].hooks[0].command,
331
+ 'python C:/Users/x/.claude/hooks/a.py',
332
+ );
333
+ });
334
+
335
+
336
+ test('expandHomeDirectoryTokensInSettings rewrites hooks and statusLine', () => {
337
+ const settings = {
338
+ hooks: {
339
+ SessionStart: [
340
+ {
341
+ matcher: '',
342
+ hooks: [
343
+ {
344
+ type: 'command',
345
+ command: 'python $HOME/.claude/hooks/session/fix_worktree_hookspath.py',
346
+ },
347
+ ],
348
+ },
349
+ ],
350
+ },
351
+ statusLine: {
352
+ type: 'command',
353
+ command: 'python "$HOME/.claude/statusline-command.py"',
354
+ },
355
+ };
356
+ expandHomeDirectoryTokensInSettings(settings, 'C:/Users/x');
357
+ assert.equal(
358
+ settings.hooks.SessionStart[0].hooks[0].command,
359
+ 'python C:/Users/x/.claude/hooks/session/fix_worktree_hookspath.py',
360
+ );
361
+ assert.equal(
362
+ settings.statusLine.command,
363
+ 'python "C:/Users/x/.claude/statusline-command.py"',
364
+ );
365
+ });
366
+
367
+
368
+ test('mergeHooksIntoSettings expands residual $HOME in preserved user hooks', () => {
369
+ const hooksConfig = {
370
+ hooks: {
371
+ SessionStart: [
372
+ {
373
+ matcher: '',
374
+ hooks: [
375
+ {
376
+ type: 'command',
377
+ command: 'python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/session_env_cleanup.py',
378
+ },
379
+ ],
380
+ },
381
+ ],
382
+ },
383
+ };
384
+ const settings = {
385
+ hooks: {
386
+ SessionStart: [
387
+ {
388
+ matcher: '',
389
+ hooks: [
390
+ {
391
+ type: 'command',
392
+ command: 'python $HOME/.claude/hooks/session/fix_worktree_hookspath.py',
393
+ },
394
+ ],
395
+ },
396
+ ],
397
+ },
398
+ };
399
+ mergeHooksIntoSettings(settings, hooksConfig, 'C:/Users/x/.claude', 'C:/Python313/python.exe');
400
+ const allCommands = settings.hooks.SessionStart[0].hooks.map(eachHook => eachHook.command);
401
+ assert.ok(
402
+ allCommands.includes(
403
+ 'python C:/Users/x/.claude/hooks/session/fix_worktree_hookspath.py',
404
+ ),
405
+ );
406
+ assert.ok(
407
+ allCommands.includes(
408
+ 'C:/Python313/python.exe C:/Users/x/.claude/hooks/session/session_env_cleanup.py',
409
+ ),
410
+ );
411
+ for (const eachCommand of allCommands) {
412
+ assert.equal(eachCommand.includes('$HOME'), false);
413
+ assert.equal(eachCommand.includes('${HOME}'), false);
414
+ }
415
+ });
416
+
417
+
418
+ test('mergeHooksIntoSettings inserts dollar characters in plugin root and interpreter literally', () => {
419
+ const pluginRootWithReplaceMetacharacters = 'C:/Users/$&evil$1/.claude';
420
+ const pythonWithReplaceMetacharacters = 'C:/Users/$&evil$1/Python/python.exe';
421
+ const hooksConfig = {
422
+ hooks: {
423
+ SessionStart: [
424
+ {
425
+ matcher: '',
426
+ hooks: [
427
+ {
428
+ type: 'command',
429
+ command: 'python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/session_env_cleanup.py',
430
+ },
431
+ ],
432
+ },
433
+ ],
434
+ },
435
+ };
436
+ const settings = {};
437
+ mergeHooksIntoSettings(
438
+ settings,
439
+ hooksConfig,
440
+ pluginRootWithReplaceMetacharacters,
441
+ pythonWithReplaceMetacharacters,
442
+ );
443
+ const rewrittenCommand = settings.hooks.SessionStart[0].hooks[0].command;
444
+ assert.equal(
445
+ rewrittenCommand,
446
+ 'C:/Users/$&evil$1/Python/python.exe C:/Users/$&evil$1/.claude/hooks/session/session_env_cleanup.py',
447
+ );
448
+ assert.equal(rewrittenCommand.includes('${CLAUDE_PLUGIN_ROOT}'), false);
449
+ });
450
+
451
+
242
452
  test('mergeHooksIntoSettings substitutes a quoted absolute interpreter path for the python3 prefix', () => {
243
453
  const hooksConfig = {
244
454
  hooks: {
@@ -374,14 +584,14 @@ test('commandReferencesManagedHook matches managed scripts written with $HOME, ~
374
584
  assert.ok(commandReferencesManagedHook('python $HOME/.claude/hooks/notification/attention_needed_notify.py', managedPaths));
375
585
  assert.ok(commandReferencesManagedHook('python ~/.claude/hooks/notification/attention_needed_notify.py', managedPaths));
376
586
  assert.ok(commandReferencesManagedHook('python ${HOME}/.claude/hooks/notification/attention_needed_notify.py', managedPaths));
377
- assert.ok(commandReferencesManagedHook('py -3 C:/Users/jonlo/.claude/hooks/notification/attention_needed_notify.py', managedPaths));
378
- assert.ok(commandReferencesManagedHook('python /Users/jon/.claude/hooks/notification/attention_needed_notify.py', managedPaths));
587
+ assert.ok(commandReferencesManagedHook('py -3 C:/Users/example/.claude/hooks/notification/attention_needed_notify.py', managedPaths));
588
+ assert.ok(commandReferencesManagedHook('python /Users/example/.claude/hooks/notification/attention_needed_notify.py', managedPaths));
379
589
  });
380
590
 
381
591
 
382
592
  test('commandReferencesManagedHook matches Windows backslash paths', () => {
383
593
  const managedPaths = new Set(['blocking/hedging_language_blocker.py']);
384
- assert.ok(commandReferencesManagedHook('py -3 C:\\Users\\jonlo\\.claude\\hooks\\blocking\\hedging_language_blocker.py', managedPaths));
594
+ assert.ok(commandReferencesManagedHook('py -3 C:\\Users\\example\\.claude\\hooks\\blocking\\hedging_language_blocker.py', managedPaths));
385
595
  });
386
596
 
387
597
 
@@ -414,7 +624,7 @@ test('commandReferencesManagedHook matches a managed script followed by a whites
414
624
  test('commandReferencesManagedHook matches the rewritten inline validators-runner hook that carries no script tail', () => {
415
625
  const managedPaths = new Set(['blocking/code_rules_enforcer.py']);
416
626
  const rewrittenInlineCommand =
417
- "py -3 -c \"import sys; sys.path.insert(0, r'C:/Users/jonlo/.claude/hooks'); from validators.run_all_validators import main; sys.exit(main())\"";
627
+ "py -3 -c \"import sys; sys.path.insert(0, r'C:/Users/example/.claude/hooks'); from validators.run_all_validators import main; sys.exit(main())\"";
418
628
  assert.ok(commandReferencesManagedHook(rewrittenInlineCommand, managedPaths));
419
629
  });
420
630
 
@@ -462,7 +672,7 @@ test('mergeHooksIntoSettings is idempotent for the inline -c validators hook acr
462
672
  },
463
673
  };
464
674
  const settings = {};
465
- const pluginRootDir = 'C:/Users/jonlo/.claude';
675
+ const pluginRootDir = 'C:/Users/example/.claude';
466
676
 
467
677
  mergeHooksIntoSettings(settings, hooksConfig, pluginRootDir, 'py -3');
468
678
  mergeHooksIntoSettings(settings, hooksConfig, pluginRootDir, 'py -3');
@@ -499,8 +709,8 @@ test('mergeHooksIntoSettings preserves user hooks in a managed matcher group acr
499
709
  },
500
710
  };
501
711
 
502
- mergeHooksIntoSettings(settings, hooksConfig, 'C:/Users/jonlo/.claude', 'py -3');
503
- mergeHooksIntoSettings(settings, hooksConfig, 'C:/Users/jonlo/.claude', 'py -3');
712
+ mergeHooksIntoSettings(settings, hooksConfig, 'C:/Users/example/.claude', 'py -3');
713
+ mergeHooksIntoSettings(settings, hooksConfig, 'C:/Users/example/.claude', 'py -3');
504
714
 
505
715
  const writeEditGroup = settings.hooks.PreToolUse.find(group => group.matcher === 'Write|Edit');
506
716
  const userHookSurvivors = writeEditGroup.hooks.filter(hook => hook.command === userHookCommand);
@@ -540,7 +750,7 @@ test('pruneManagedHooksFromSettings removes managed hooks in every home-path and
540
750
  hooks: [
541
751
  { command: 'python $HOME/.claude/hooks/notification/attention_needed_notify.py', timeout: 15 },
542
752
  { command: 'python ${HOME}/.claude/hooks/notification/attention_needed_notify.py', timeout: 15 },
543
- { command: 'py -3 C:\\Users\\jonlo\\.claude\\hooks\\notification\\attention_needed_notify.py', timeout: 15 },
753
+ { command: 'py -3 C:\\Users\\example\\.claude\\hooks\\notification\\attention_needed_notify.py', timeout: 15 },
544
754
  { command: userHookCommand, timeout: 5 },
545
755
  ],
546
756
  },
@@ -637,7 +847,7 @@ test('purge set sourced from package hooks.json prunes standalone managed script
637
847
  {
638
848
  matcher: 'Write|Edit',
639
849
  hooks: [
640
- { command: 'py -3 C:\\Users\\jonlo\\.claude\\hooks\\blocking\\code_rules_enforcer.py', timeout: 30 },
850
+ { command: 'py -3 C:\\Users\\example\\.claude\\hooks\\blocking\\code_rules_enforcer.py', timeout: 30 },
641
851
  { command: userHookCommand, timeout: 5 },
642
852
  ],
643
853
  },
package/hooks/CLAUDE.md CHANGED
@@ -14,7 +14,7 @@ Python hook scripts wired into Claude Code's lifecycle via `settings.json`. Each
14
14
  | `diagnostic/queries/` | Parameterized SQL queries for inspecting blocked commands |
15
15
  | `git-hooks/` | Native git hooks (`pre-commit`, `pre-push`, `post-commit`) installed via the git-hooks path |
16
16
  | `git-hooks/git_hooks_constants/` | Shared constants for the git-hook scripts |
17
- | `hooks_constants/` | Shared constant modules imported by multiple hooks across this tree |
17
+ | `hooks_constants/` | Shared constant modules imported by multiple hooks across this tree (includes `pii_prevention_constants.py` for personal-data and secret scan patterns) |
18
18
  | `lifecycle/` | Hooks that run at session or config-change boundaries |
19
19
  | `observability/` | PostToolUse hooks that record agent behavior for diagnostics |
20
20
  | `session/` | SessionStart and SessionEnd hooks for per-session cleanup |
@@ -50,6 +50,7 @@ The check modules it calls are the `code_rules_<concern>.py` files below.
50
50
  | `code_rules_shared.py` | Shared dataclasses and helpers used by multiple check modules |
51
51
  | `code_rules_string_magic.py` | Magic string detection with masking and f-string support; whitespace-only indentation literals in function bodies |
52
52
  | `code_rules_test_assertions.py` | Test assertion style rules |
53
+ | `code_rules_test_layout.py` | Dead scaffolding in a test module: a private module constant read by no other line, and an unused parameter on a private test helper |
53
54
  | `code_rules_test_branching_except.py` | No bare or broad `except` in test branches |
54
55
  | `code_rules_test_isolation.py` | Tests must not rely on home-dir or temp-dir side effects |
55
56
  | `code_rules_type_escape.py` | No `Any` imports, `cast()`, or `# type: ignore` outside boundary files |
@@ -78,8 +79,10 @@ The check modules it calls are the `code_rules_<concern>.py` files below.
78
79
  | `hook_prose_detector_consistency.py` | PreToolUse (Write/Edit) | Hook docstrings/messages that claim a trigger the detector cannot fire on |
79
80
  | `intent_only_ending_blocker.py` | Stop | Responses that end on a plan or intent without doing the work |
80
81
  | `open_questions_in_plans_blocker.py` | PreToolUse (Write/Edit) | Plan documents with unresolved open questions |
81
- | `nas_ssh_binary_enforcer.py` | PreToolUse (Bash) | A bare `ssh`/`scp`/`sftp` command word targeting the NAS at `192.168.1.100` (Git Bash's MSYS ssh stalls on an interactive password prompt), or the full `System32/OpenSSH` binary to that host without `-o BatchMode=yes` |
82
+ | `nas_ssh_binary_enforcer.py` | PreToolUse (Bash) | A bare `ssh`/`scp`/`sftp` command word targeting the NAS (Git Bash's MSYS ssh stalls on an interactive password prompt), or the full `System32/OpenSSH` binary to that host without `-o BatchMode=yes` |
82
83
  | `package_inventory_stale_blocker.py` | PreToolUse (Write) | A new production code file created in a directory whose `README.md`/`CLAUDE.md` inventory (or a parent skill's `SKILL.md` Layout table mapping the `scripts/` subdirectory) names two or more sibling files but no entry for the new file |
84
+ | `pii_prevention_blocker.py` | PreToolUse (Write/Edit/MultiEdit/Bash/PowerShell/MCP GitHub) | Content that carries high-confidence personal data or secrets (real emails, home-dir paths, private IPs, credential material) on write, durable GitHub posts, or staged commit paths |
85
+ | `pii_scanner.py` | library | Pure text scanners shared by `pii_prevention_blocker.py` |
83
86
  | `plain_language_blocker.py` | PreToolUse (Write/Edit/AskUserQuestion) | Heavy or jargon words in user-facing prose |
84
87
  | `pr_converge_bugteam_enforcer.py` | PreToolUse | Enforces that bugteam runs in parallel with bugbot in pr-converge loops |
85
88
  | `pr_description_enforcer.py` | PreToolUse (Bash) | `gh pr create`/`edit`/`comment` bodies that fail the Anthropic claude-code style audit, proof-shaped `gh pr comment` bodies missing proof-of-work parts, and `gh pr ready` while the PR carries no passing proof comment |
@@ -0,0 +1,13 @@
1
+ """Add the hooks directory to sys.path for sibling entry-point dispatchers.
2
+
3
+ Importing this module inserts the hooks directory (this file's parent's parent)
4
+ at the front of sys.path so a dispatcher that runs as a standalone script from
5
+ blocking/ can import hooks_constants with every import kept at module top.
6
+ """
7
+
8
+ import sys
9
+ from pathlib import Path
10
+
11
+ _hooks_directory = str(Path(__file__).resolve().parent.parent)
12
+ if _hooks_directory not in sys.path:
13
+ sys.path.insert(0, _hooks_directory)