@jaguilar87/gaia-ops 4.4.0 → 4.7.2

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 (371) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +12 -3
  3. package/ARCHITECTURE.md +9 -8
  4. package/CHANGELOG.md +34 -0
  5. package/README.md +43 -11
  6. package/agents/terraform-architect.md +1 -1
  7. package/bin/README.md +2 -2
  8. package/bin/gaia-doctor.js +18 -5
  9. package/bin/gaia-history.js +0 -1
  10. package/bin/gaia-metrics.js +2 -2
  11. package/bin/gaia-scan.py +23 -1
  12. package/bin/gaia-update.js +346 -54
  13. package/bin/pre-publish-validate.js +33 -10
  14. package/commands/gaia.md +37 -0
  15. package/config/README.md +3 -9
  16. package/config/context-contracts.json +47 -15
  17. package/config/surface-routing.json +9 -1
  18. package/dist/gaia-ops/.claude-plugin/plugin.json +22 -0
  19. package/dist/gaia-ops/agents/cloud-troubleshooter.md +73 -0
  20. package/dist/gaia-ops/agents/devops-developer.md +57 -0
  21. package/dist/gaia-ops/agents/gaia-system.md +58 -0
  22. package/dist/gaia-ops/agents/gitops-operator.md +60 -0
  23. package/dist/gaia-ops/agents/speckit-planner.md +71 -0
  24. package/dist/gaia-ops/agents/terraform-architect.md +60 -0
  25. package/dist/gaia-ops/commands/gaia.md +37 -0
  26. package/dist/gaia-ops/config/README.md +58 -0
  27. package/dist/gaia-ops/config/cloud/aws.json +140 -0
  28. package/dist/gaia-ops/config/cloud/gcp.json +145 -0
  29. package/dist/gaia-ops/config/context-contracts.json +131 -0
  30. package/dist/gaia-ops/config/git_standards.json +72 -0
  31. package/dist/gaia-ops/config/surface-routing.json +197 -0
  32. package/dist/gaia-ops/config/universal-rules.json +10 -0
  33. package/dist/gaia-ops/hooks/adapters/__init__.py +52 -0
  34. package/dist/gaia-ops/hooks/adapters/base.py +219 -0
  35. package/dist/gaia-ops/hooks/adapters/channel.py +17 -0
  36. package/dist/gaia-ops/hooks/adapters/claude_code.py +1477 -0
  37. package/dist/gaia-ops/hooks/adapters/types.py +194 -0
  38. package/dist/gaia-ops/hooks/adapters/utils.py +25 -0
  39. package/dist/gaia-ops/hooks/hooks.json +126 -0
  40. package/dist/gaia-ops/hooks/modules/__init__.py +15 -0
  41. package/dist/gaia-ops/hooks/modules/agents/__init__.py +29 -0
  42. package/dist/gaia-ops/hooks/modules/agents/contract_validator.py +647 -0
  43. package/dist/gaia-ops/hooks/modules/agents/response_contract.py +496 -0
  44. package/dist/gaia-ops/hooks/modules/agents/skill_injection_verifier.py +124 -0
  45. package/dist/gaia-ops/hooks/modules/agents/task_info_builder.py +74 -0
  46. package/dist/gaia-ops/hooks/modules/agents/transcript_analyzer.py +458 -0
  47. package/dist/gaia-ops/hooks/modules/agents/transcript_reader.py +152 -0
  48. package/dist/gaia-ops/hooks/modules/audit/__init__.py +28 -0
  49. package/dist/gaia-ops/hooks/modules/audit/event_detector.py +168 -0
  50. package/dist/gaia-ops/hooks/modules/audit/logger.py +131 -0
  51. package/dist/gaia-ops/hooks/modules/audit/metrics.py +134 -0
  52. package/dist/gaia-ops/hooks/modules/audit/workflow_auditor.py +576 -0
  53. package/dist/gaia-ops/hooks/modules/audit/workflow_recorder.py +296 -0
  54. package/dist/gaia-ops/hooks/modules/context/__init__.py +11 -0
  55. package/dist/gaia-ops/hooks/modules/context/anchor_tracker.py +317 -0
  56. package/dist/gaia-ops/hooks/modules/context/compact_context_builder.py +215 -0
  57. package/dist/gaia-ops/hooks/modules/context/context_cache.py +129 -0
  58. package/dist/gaia-ops/hooks/modules/context/context_freshness.py +145 -0
  59. package/dist/gaia-ops/hooks/modules/context/context_injector.py +427 -0
  60. package/dist/gaia-ops/hooks/modules/context/context_writer.py +518 -0
  61. package/dist/gaia-ops/hooks/modules/context/contracts_loader.py +161 -0
  62. package/dist/gaia-ops/hooks/modules/core/__init__.py +40 -0
  63. package/dist/gaia-ops/hooks/modules/core/hook_entry.py +78 -0
  64. package/dist/gaia-ops/hooks/modules/core/paths.py +160 -0
  65. package/dist/gaia-ops/hooks/modules/core/plugin_mode.py +149 -0
  66. package/dist/gaia-ops/hooks/modules/core/plugin_setup.py +558 -0
  67. package/dist/gaia-ops/hooks/modules/core/state.py +179 -0
  68. package/dist/gaia-ops/hooks/modules/core/stdin.py +24 -0
  69. package/dist/gaia-ops/hooks/modules/events/__init__.py +1 -0
  70. package/dist/gaia-ops/hooks/modules/events/event_writer.py +210 -0
  71. package/dist/gaia-ops/hooks/modules/identity/__init__.py +0 -0
  72. package/dist/gaia-ops/hooks/modules/identity/identity_provider.py +21 -0
  73. package/dist/gaia-ops/hooks/modules/identity/ops_identity.py +34 -0
  74. package/dist/gaia-ops/hooks/modules/identity/security_identity.py +10 -0
  75. package/dist/gaia-ops/hooks/modules/memory/__init__.py +8 -0
  76. package/dist/gaia-ops/hooks/modules/memory/episode_writer.py +227 -0
  77. package/dist/gaia-ops/hooks/modules/orchestrator/__init__.py +1 -0
  78. package/dist/gaia-ops/hooks/modules/orchestrator/delegate_mode.py +128 -0
  79. package/dist/gaia-ops/hooks/modules/scanning/__init__.py +8 -0
  80. package/dist/gaia-ops/hooks/modules/scanning/scan_trigger.py +84 -0
  81. package/dist/gaia-ops/hooks/modules/security/__init__.py +89 -0
  82. package/dist/gaia-ops/hooks/modules/security/approval_cleanup.py +87 -0
  83. package/dist/gaia-ops/hooks/modules/security/approval_constants.py +23 -0
  84. package/dist/gaia-ops/hooks/modules/security/approval_grants.py +912 -0
  85. package/dist/gaia-ops/hooks/modules/security/approval_messages.py +71 -0
  86. package/dist/gaia-ops/hooks/modules/security/approval_scopes.py +153 -0
  87. package/dist/gaia-ops/hooks/modules/security/blocked_commands.py +584 -0
  88. package/dist/gaia-ops/hooks/modules/security/blocked_message_formatter.py +86 -0
  89. package/dist/gaia-ops/hooks/modules/security/command_semantics.py +130 -0
  90. package/dist/gaia-ops/hooks/modules/security/gitops_validator.py +179 -0
  91. package/dist/gaia-ops/hooks/modules/security/mutative_verbs.py +850 -0
  92. package/dist/gaia-ops/hooks/modules/security/prompt_validator.py +40 -0
  93. package/dist/gaia-ops/hooks/modules/security/tiers.py +196 -0
  94. package/dist/gaia-ops/hooks/modules/session/__init__.py +10 -0
  95. package/dist/gaia-ops/hooks/modules/session/session_context_writer.py +100 -0
  96. package/dist/gaia-ops/hooks/modules/session/session_event_injector.py +158 -0
  97. package/dist/gaia-ops/hooks/modules/session/session_manager.py +31 -0
  98. package/dist/gaia-ops/hooks/modules/tools/__init__.py +25 -0
  99. package/dist/gaia-ops/hooks/modules/tools/bash_validator.py +708 -0
  100. package/dist/gaia-ops/hooks/modules/tools/cloud_pipe_validator.py +181 -0
  101. package/dist/gaia-ops/hooks/modules/tools/hook_response.py +55 -0
  102. package/dist/gaia-ops/hooks/modules/tools/shell_parser.py +227 -0
  103. package/dist/gaia-ops/hooks/modules/tools/task_validator.py +283 -0
  104. package/dist/gaia-ops/hooks/modules/validation/__init__.py +23 -0
  105. package/dist/gaia-ops/hooks/modules/validation/commit_validator.py +380 -0
  106. package/dist/gaia-ops/hooks/post_compact.py +43 -0
  107. package/dist/gaia-ops/hooks/post_tool_use.py +54 -0
  108. package/dist/gaia-ops/hooks/pre_tool_use.py +383 -0
  109. package/dist/gaia-ops/hooks/session_start.py +69 -0
  110. package/dist/gaia-ops/hooks/stop_hook.py +69 -0
  111. package/dist/gaia-ops/hooks/subagent_start.py +71 -0
  112. package/dist/gaia-ops/hooks/subagent_stop.py +288 -0
  113. package/dist/gaia-ops/hooks/task_completed.py +70 -0
  114. package/dist/gaia-ops/hooks/user_prompt_submit.py +177 -0
  115. package/dist/gaia-ops/settings.json +72 -0
  116. package/dist/gaia-ops/skills/README.md +109 -0
  117. package/dist/gaia-ops/skills/agent-protocol/SKILL.md +105 -0
  118. package/dist/gaia-ops/skills/agent-protocol/examples.md +170 -0
  119. package/dist/gaia-ops/skills/agent-response/SKILL.md +53 -0
  120. package/dist/gaia-ops/skills/approval/SKILL.md +85 -0
  121. package/dist/gaia-ops/skills/approval/examples.md +140 -0
  122. package/dist/gaia-ops/skills/approval/reference.md +57 -0
  123. package/dist/gaia-ops/skills/command-execution/SKILL.md +64 -0
  124. package/dist/gaia-ops/skills/command-execution/reference.md +83 -0
  125. package/dist/gaia-ops/skills/context-updater/SKILL.md +76 -0
  126. package/dist/gaia-ops/skills/context-updater/examples.md +71 -0
  127. package/dist/gaia-ops/skills/developer-patterns/SKILL.md +93 -0
  128. package/dist/gaia-ops/skills/developer-patterns/reference.md +112 -0
  129. package/dist/gaia-ops/skills/execution/SKILL.md +66 -0
  130. package/dist/gaia-ops/skills/fast-queries/SKILL.md +47 -0
  131. package/dist/gaia-ops/skills/gaia-patterns/SKILL.md +92 -0
  132. package/dist/gaia-ops/skills/gaia-patterns/reference.md +22 -0
  133. package/dist/gaia-ops/skills/git-conventions/SKILL.md +48 -0
  134. package/dist/gaia-ops/skills/gitops-patterns/SKILL.md +73 -0
  135. package/dist/gaia-ops/skills/gitops-patterns/reference.md +183 -0
  136. package/dist/gaia-ops/skills/investigation/SKILL.md +77 -0
  137. package/dist/gaia-ops/skills/orchestrator-approval/SKILL.md +64 -0
  138. package/dist/gaia-ops/skills/reference.md +134 -0
  139. package/dist/gaia-ops/skills/security-tiers/SKILL.md +61 -0
  140. package/dist/gaia-ops/skills/security-tiers/destructive-commands-reference.md +623 -0
  141. package/dist/gaia-ops/skills/security-tiers/reference.md +39 -0
  142. package/dist/gaia-ops/skills/skill-creation/SKILL.md +119 -0
  143. package/dist/gaia-ops/skills/specification/SKILL.md +186 -0
  144. package/dist/gaia-ops/skills/speckit-workflow/SKILL.md +165 -0
  145. package/dist/gaia-ops/skills/speckit-workflow/reference.md +117 -0
  146. package/dist/gaia-ops/skills/terraform-patterns/SKILL.md +63 -0
  147. package/dist/gaia-ops/skills/terraform-patterns/reference.md +93 -0
  148. package/dist/gaia-ops/speckit/README.md +516 -0
  149. package/dist/gaia-ops/speckit/scripts/.gitkeep +0 -0
  150. package/dist/gaia-ops/speckit/templates/adr-template.md +118 -0
  151. package/dist/gaia-ops/speckit/templates/agent-file-template.md +23 -0
  152. package/dist/gaia-ops/speckit/templates/plan-template.md +227 -0
  153. package/dist/gaia-ops/speckit/templates/spec-template.md +140 -0
  154. package/dist/gaia-ops/speckit/templates/tasks-template.md +257 -0
  155. package/dist/gaia-ops/tools/context/README.md +132 -0
  156. package/dist/gaia-ops/tools/context/__init__.py +42 -0
  157. package/dist/gaia-ops/tools/context/_paths.py +20 -0
  158. package/dist/gaia-ops/tools/context/context_provider.py +476 -0
  159. package/dist/gaia-ops/tools/context/context_section_reader.py +330 -0
  160. package/dist/gaia-ops/tools/context/deep_merge.py +159 -0
  161. package/dist/gaia-ops/tools/context/pending_updates.py +760 -0
  162. package/dist/gaia-ops/tools/context/surface_router.py +278 -0
  163. package/dist/gaia-ops/tools/fast-queries/README.md +65 -0
  164. package/dist/gaia-ops/tools/fast-queries/__init__.py +30 -0
  165. package/dist/gaia-ops/tools/fast-queries/appservices/quicktriage_devops_developer.sh +75 -0
  166. package/dist/gaia-ops/tools/fast-queries/cloud/aws/quicktriage_aws_troubleshooter.sh +32 -0
  167. package/dist/gaia-ops/tools/fast-queries/cloud/gcp/quicktriage_gcp_troubleshooter.sh +88 -0
  168. package/dist/gaia-ops/tools/fast-queries/gitops/quicktriage_gitops_operator.sh +48 -0
  169. package/dist/gaia-ops/tools/fast-queries/run_triage.sh +59 -0
  170. package/dist/gaia-ops/tools/fast-queries/terraform/quicktriage_terraform_architect.sh +80 -0
  171. package/dist/gaia-ops/tools/gaia_simulator/__init__.py +33 -0
  172. package/dist/gaia-ops/tools/gaia_simulator/cli.py +354 -0
  173. package/dist/gaia-ops/tools/gaia_simulator/extractor.py +457 -0
  174. package/dist/gaia-ops/tools/gaia_simulator/reporter.py +258 -0
  175. package/dist/gaia-ops/tools/gaia_simulator/routing_simulator.py +334 -0
  176. package/dist/gaia-ops/tools/gaia_simulator/runner.py +539 -0
  177. package/dist/gaia-ops/tools/gaia_simulator/skills_mapper.py +262 -0
  178. package/dist/gaia-ops/tools/memory/README.md +0 -0
  179. package/dist/gaia-ops/tools/memory/__init__.py +20 -0
  180. package/dist/gaia-ops/tools/memory/episodic.py +1196 -0
  181. package/dist/gaia-ops/tools/persist_transcript_analysis.py +85 -0
  182. package/dist/gaia-ops/tools/review/__init__.py +1 -0
  183. package/dist/gaia-ops/tools/review/review_engine.py +157 -0
  184. package/dist/gaia-ops/tools/scan/__init__.py +35 -0
  185. package/dist/gaia-ops/tools/scan/config.py +247 -0
  186. package/dist/gaia-ops/tools/scan/merge.py +212 -0
  187. package/dist/gaia-ops/tools/scan/orchestrator.py +549 -0
  188. package/dist/gaia-ops/tools/scan/registry.py +127 -0
  189. package/dist/gaia-ops/tools/scan/scanners/__init__.py +18 -0
  190. package/dist/gaia-ops/tools/scan/scanners/base.py +137 -0
  191. package/dist/gaia-ops/tools/scan/scanners/environment.py +324 -0
  192. package/dist/gaia-ops/tools/scan/scanners/git.py +570 -0
  193. package/dist/gaia-ops/tools/scan/scanners/infrastructure.py +875 -0
  194. package/dist/gaia-ops/tools/scan/scanners/orchestration.py +600 -0
  195. package/dist/gaia-ops/tools/scan/scanners/stack.py +1085 -0
  196. package/dist/gaia-ops/tools/scan/scanners/tools.py +260 -0
  197. package/dist/gaia-ops/tools/scan/setup.py +753 -0
  198. package/dist/gaia-ops/tools/scan/tests/__init__.py +1 -0
  199. package/dist/gaia-ops/tools/scan/tests/conftest.py +796 -0
  200. package/dist/gaia-ops/tools/scan/tests/test_environment.py +323 -0
  201. package/dist/gaia-ops/tools/scan/tests/test_git.py +419 -0
  202. package/dist/gaia-ops/tools/scan/tests/test_infrastructure.py +382 -0
  203. package/dist/gaia-ops/tools/scan/tests/test_integration.py +920 -0
  204. package/dist/gaia-ops/tools/scan/tests/test_merge.py +269 -0
  205. package/dist/gaia-ops/tools/scan/tests/test_orchestration.py +304 -0
  206. package/dist/gaia-ops/tools/scan/tests/test_stack.py +604 -0
  207. package/dist/gaia-ops/tools/scan/tests/test_tools.py +349 -0
  208. package/dist/gaia-ops/tools/scan/ui.py +624 -0
  209. package/dist/gaia-ops/tools/scan/verify.py +266 -0
  210. package/dist/gaia-ops/tools/scan/walk.py +118 -0
  211. package/dist/gaia-ops/tools/scan/workspace.py +85 -0
  212. package/dist/gaia-ops/tools/validation/README.md +244 -0
  213. package/dist/gaia-ops/tools/validation/__init__.py +17 -0
  214. package/dist/gaia-ops/tools/validation/approval_gate.py +321 -0
  215. package/dist/gaia-ops/tools/validation/validate_skills.py +189 -0
  216. package/dist/gaia-security/.claude-plugin/plugin.json +22 -0
  217. package/dist/gaia-security/config/universal-rules.json +10 -0
  218. package/dist/gaia-security/hooks/adapters/__init__.py +52 -0
  219. package/dist/gaia-security/hooks/adapters/base.py +219 -0
  220. package/dist/gaia-security/hooks/adapters/channel.py +17 -0
  221. package/dist/gaia-security/hooks/adapters/claude_code.py +1477 -0
  222. package/dist/gaia-security/hooks/adapters/types.py +194 -0
  223. package/dist/gaia-security/hooks/adapters/utils.py +25 -0
  224. package/dist/gaia-security/hooks/hooks.json +57 -0
  225. package/dist/gaia-security/hooks/modules/__init__.py +15 -0
  226. package/dist/gaia-security/hooks/modules/agents/__init__.py +29 -0
  227. package/dist/gaia-security/hooks/modules/agents/contract_validator.py +647 -0
  228. package/dist/gaia-security/hooks/modules/agents/response_contract.py +496 -0
  229. package/dist/gaia-security/hooks/modules/agents/skill_injection_verifier.py +124 -0
  230. package/dist/gaia-security/hooks/modules/agents/task_info_builder.py +74 -0
  231. package/dist/gaia-security/hooks/modules/agents/transcript_analyzer.py +458 -0
  232. package/dist/gaia-security/hooks/modules/agents/transcript_reader.py +152 -0
  233. package/dist/gaia-security/hooks/modules/audit/__init__.py +28 -0
  234. package/dist/gaia-security/hooks/modules/audit/event_detector.py +168 -0
  235. package/dist/gaia-security/hooks/modules/audit/logger.py +131 -0
  236. package/dist/gaia-security/hooks/modules/audit/metrics.py +134 -0
  237. package/dist/gaia-security/hooks/modules/audit/workflow_auditor.py +576 -0
  238. package/dist/gaia-security/hooks/modules/audit/workflow_recorder.py +296 -0
  239. package/dist/gaia-security/hooks/modules/context/__init__.py +11 -0
  240. package/dist/gaia-security/hooks/modules/context/anchor_tracker.py +317 -0
  241. package/dist/gaia-security/hooks/modules/context/compact_context_builder.py +215 -0
  242. package/dist/gaia-security/hooks/modules/context/context_cache.py +129 -0
  243. package/dist/gaia-security/hooks/modules/context/context_freshness.py +145 -0
  244. package/dist/gaia-security/hooks/modules/context/context_injector.py +427 -0
  245. package/dist/gaia-security/hooks/modules/context/context_writer.py +518 -0
  246. package/dist/gaia-security/hooks/modules/context/contracts_loader.py +161 -0
  247. package/dist/gaia-security/hooks/modules/core/__init__.py +40 -0
  248. package/dist/gaia-security/hooks/modules/core/hook_entry.py +78 -0
  249. package/dist/gaia-security/hooks/modules/core/paths.py +160 -0
  250. package/dist/gaia-security/hooks/modules/core/plugin_mode.py +149 -0
  251. package/dist/gaia-security/hooks/modules/core/plugin_setup.py +558 -0
  252. package/dist/gaia-security/hooks/modules/core/state.py +179 -0
  253. package/dist/gaia-security/hooks/modules/core/stdin.py +24 -0
  254. package/dist/gaia-security/hooks/modules/events/__init__.py +1 -0
  255. package/dist/gaia-security/hooks/modules/events/event_writer.py +210 -0
  256. package/dist/gaia-security/hooks/modules/identity/__init__.py +0 -0
  257. package/dist/gaia-security/hooks/modules/identity/identity_provider.py +21 -0
  258. package/dist/gaia-security/hooks/modules/identity/ops_identity.py +34 -0
  259. package/dist/gaia-security/hooks/modules/identity/security_identity.py +10 -0
  260. package/dist/gaia-security/hooks/modules/memory/__init__.py +8 -0
  261. package/dist/gaia-security/hooks/modules/memory/episode_writer.py +227 -0
  262. package/dist/gaia-security/hooks/modules/orchestrator/__init__.py +1 -0
  263. package/dist/gaia-security/hooks/modules/orchestrator/delegate_mode.py +128 -0
  264. package/dist/gaia-security/hooks/modules/scanning/__init__.py +8 -0
  265. package/dist/gaia-security/hooks/modules/scanning/scan_trigger.py +84 -0
  266. package/dist/gaia-security/hooks/modules/security/__init__.py +89 -0
  267. package/dist/gaia-security/hooks/modules/security/approval_cleanup.py +87 -0
  268. package/dist/gaia-security/hooks/modules/security/approval_constants.py +23 -0
  269. package/dist/gaia-security/hooks/modules/security/approval_grants.py +912 -0
  270. package/dist/gaia-security/hooks/modules/security/approval_messages.py +71 -0
  271. package/dist/gaia-security/hooks/modules/security/approval_scopes.py +153 -0
  272. package/dist/gaia-security/hooks/modules/security/blocked_commands.py +584 -0
  273. package/dist/gaia-security/hooks/modules/security/blocked_message_formatter.py +86 -0
  274. package/dist/gaia-security/hooks/modules/security/command_semantics.py +130 -0
  275. package/dist/gaia-security/hooks/modules/security/gitops_validator.py +179 -0
  276. package/dist/gaia-security/hooks/modules/security/mutative_verbs.py +850 -0
  277. package/dist/gaia-security/hooks/modules/security/prompt_validator.py +40 -0
  278. package/dist/gaia-security/hooks/modules/security/tiers.py +196 -0
  279. package/dist/gaia-security/hooks/modules/session/__init__.py +10 -0
  280. package/dist/gaia-security/hooks/modules/session/session_context_writer.py +100 -0
  281. package/dist/gaia-security/hooks/modules/session/session_event_injector.py +158 -0
  282. package/dist/gaia-security/hooks/modules/session/session_manager.py +31 -0
  283. package/dist/gaia-security/hooks/modules/tools/__init__.py +25 -0
  284. package/dist/gaia-security/hooks/modules/tools/bash_validator.py +708 -0
  285. package/dist/gaia-security/hooks/modules/tools/cloud_pipe_validator.py +181 -0
  286. package/dist/gaia-security/hooks/modules/tools/hook_response.py +55 -0
  287. package/dist/gaia-security/hooks/modules/tools/shell_parser.py +227 -0
  288. package/dist/gaia-security/hooks/modules/tools/task_validator.py +283 -0
  289. package/dist/gaia-security/hooks/modules/validation/__init__.py +23 -0
  290. package/dist/gaia-security/hooks/modules/validation/commit_validator.py +380 -0
  291. package/dist/gaia-security/hooks/post_tool_use.py +54 -0
  292. package/dist/gaia-security/hooks/pre_tool_use.py +383 -0
  293. package/dist/gaia-security/hooks/session_start.py +69 -0
  294. package/dist/gaia-security/hooks/stop_hook.py +69 -0
  295. package/dist/gaia-security/hooks/user_prompt_submit.py +177 -0
  296. package/dist/gaia-security/settings.json +58 -0
  297. package/git-hooks/commit-msg +41 -0
  298. package/hooks/README.md +8 -6
  299. package/hooks/adapters/channel.py +0 -25
  300. package/hooks/adapters/claude_code.py +364 -125
  301. package/hooks/elicitation_result.py +132 -0
  302. package/hooks/hooks.json +10 -1
  303. package/hooks/modules/README.md +3 -2
  304. package/hooks/modules/agents/contract_validator.py +3 -51
  305. package/hooks/modules/agents/response_contract.py +4 -8
  306. package/hooks/modules/agents/transcript_reader.py +4 -5
  307. package/hooks/modules/audit/__init__.py +4 -6
  308. package/hooks/modules/audit/event_detector.py +0 -2
  309. package/hooks/modules/audit/metrics.py +108 -187
  310. package/hooks/modules/audit/workflow_auditor.py +0 -4
  311. package/hooks/modules/audit/workflow_recorder.py +0 -5
  312. package/hooks/modules/context/compact_context_builder.py +1 -0
  313. package/hooks/modules/context/context_cache.py +129 -0
  314. package/hooks/modules/context/context_injector.py +18 -40
  315. package/hooks/modules/context/context_writer.py +1 -25
  316. package/hooks/modules/context/contracts_loader.py +7 -10
  317. package/hooks/modules/core/hook_entry.py +1 -0
  318. package/hooks/modules/core/paths.py +12 -13
  319. package/hooks/modules/core/plugin_mode.py +74 -4
  320. package/hooks/modules/core/plugin_setup.py +395 -23
  321. package/hooks/modules/events/__init__.py +1 -0
  322. package/hooks/modules/events/event_writer.py +210 -0
  323. package/hooks/modules/identity/ops_identity.py +18 -27
  324. package/hooks/modules/memory/episode_writer.py +1 -6
  325. package/hooks/modules/orchestrator/__init__.py +1 -0
  326. package/hooks/modules/orchestrator/delegate_mode.py +128 -0
  327. package/hooks/modules/security/__init__.py +2 -4
  328. package/hooks/modules/security/approval_constants.py +5 -1
  329. package/hooks/modules/security/approval_grants.py +189 -6
  330. package/hooks/modules/security/approval_messages.py +9 -21
  331. package/hooks/modules/security/blocked_commands.py +98 -34
  332. package/hooks/modules/security/command_semantics.py +0 -4
  333. package/hooks/modules/security/gitops_validator.py +1 -11
  334. package/hooks/modules/security/mutative_verbs.py +179 -38
  335. package/hooks/modules/security/tiers.py +1 -19
  336. package/hooks/modules/session/session_event_injector.py +1 -25
  337. package/hooks/modules/tools/bash_validator.py +310 -94
  338. package/hooks/modules/tools/shell_parser.py +0 -1
  339. package/hooks/modules/tools/task_validator.py +9 -29
  340. package/hooks/post_tool_use.py +0 -72
  341. package/hooks/pre_tool_use.py +42 -102
  342. package/hooks/session_start.py +4 -2
  343. package/hooks/subagent_start.py +6 -2
  344. package/hooks/subagent_stop.py +1 -13
  345. package/hooks/user_prompt_submit.py +119 -37
  346. package/index.js +1 -1
  347. package/package.json +5 -3
  348. package/skills/README.md +3 -5
  349. package/skills/agent-protocol/SKILL.md +17 -16
  350. package/skills/agent-protocol/examples.md +6 -6
  351. package/skills/agent-response/SKILL.md +11 -14
  352. package/skills/approval/SKILL.md +28 -13
  353. package/skills/approval/reference.md +2 -2
  354. package/skills/execution/SKILL.md +1 -1
  355. package/skills/gaia-patterns/SKILL.md +2 -3
  356. package/skills/orchestrator-approval/SKILL.md +22 -50
  357. package/skills/security-tiers/SKILL.md +1 -1
  358. package/templates/README.md +9 -9
  359. package/templates/managed-settings.template.json +43 -0
  360. package/tools/gaia_simulator/runner.py +34 -1
  361. package/tools/scan/orchestrator.py +13 -0
  362. package/tools/scan/scanners/base.py +8 -0
  363. package/tools/scan/scanners/git.py +78 -0
  364. package/tools/scan/scanners/infrastructure.py +65 -0
  365. package/tools/scan/scanners/stack.py +110 -0
  366. package/tools/scan/setup.py +120 -13
  367. package/tools/scan/workspace.py +85 -0
  368. package/config/context-contracts.aws.json +0 -42
  369. package/config/context-contracts.gcp.json +0 -39
  370. package/skills/project-dispatch/SKILL.md +0 -34
  371. package/templates/settings.template.json +0 -226
@@ -0,0 +1,558 @@
1
+ """First-time plugin setup for SessionStart hook.
2
+
3
+ Detects first run via marker file in CLAUDE_PLUGIN_DATA.
4
+ On first run, merges gaia permissions into .claude/settings.local.json.
5
+ """
6
+ from __future__ import annotations
7
+
8
+ import json
9
+ import logging
10
+ from datetime import datetime
11
+ from pathlib import Path
12
+
13
+ from .paths import get_plugin_data_dir
14
+ from .plugin_mode import get_plugin_mode
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+ MARKER_FILE = ".plugin-initialized"
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # Deny list — shared across all modes. Aligned with blocked_commands.py
22
+ # (hook-level enforcement) for dual-barrier security. These rules are
23
+ # merged into settings.local.json so Claude Code's native permission system
24
+ # blocks the commands BEFORE they even reach the hook layer.
25
+ # ---------------------------------------------------------------------------
26
+ _DENY_RULES = [
27
+ # AWS — networking / data infrastructure (irreversible)
28
+ "Bash(aws ec2 delete-vpc:*)",
29
+ "Bash(aws ec2 delete-subnet:*)",
30
+ "Bash(aws ec2 delete-internet-gateway:*)",
31
+ "Bash(aws ec2 delete-route-table:*)",
32
+ "Bash(aws ec2 delete-route:*)",
33
+ "Bash(aws ec2 terminate-instances:*)",
34
+ "Bash(aws rds delete-db-instance:*)",
35
+ "Bash(aws rds delete-db-cluster:*)",
36
+ "Bash(aws dynamodb delete-table:*)",
37
+ "Bash(aws s3 rb:*)",
38
+ "Bash(aws s3api delete-bucket:*)",
39
+ "Bash(aws elasticache delete-cache-cluster:*)",
40
+ "Bash(aws elasticache delete-replication-group:*)",
41
+ "Bash(aws eks delete-cluster:*)",
42
+ # AWS — KMS / Organizations / Route53
43
+ "Bash(aws kms schedule-key-deletion:*)",
44
+ "Bash(aws organizations delete-organization:*)",
45
+ "Bash(aws route53 delete-hosted-zone:*)",
46
+ # AWS — IAM (mutative but denied at settings level too)
47
+ "Bash(aws iam delete-user:*)",
48
+ "Bash(aws iam delete-role:*)",
49
+ "Bash(aws iam delete-access-key:*)",
50
+ "Bash(aws iam delete-group:*)",
51
+ "Bash(aws iam delete-instance-profile:*)",
52
+ "Bash(aws iam delete-policy:*)",
53
+ "Bash(aws iam delete-role-policy:*)",
54
+ "Bash(aws iam delete-user-policy:*)",
55
+ "Bash(aws iam delete-group-policy:*)",
56
+ "Bash(aws iam detach-user-policy:*)",
57
+ "Bash(aws iam detach-role-policy:*)",
58
+ "Bash(aws iam detach-group-policy:*)",
59
+ "Bash(aws iam remove-user-from-group:*)",
60
+ # AWS — other destructive
61
+ "Bash(aws backup delete:*::*)",
62
+ "Bash(aws cloudformation delete-stack:*)",
63
+ "Bash(aws dynamodb delete-item:*)",
64
+ "Bash(aws ec2 delete-key-pair:*)",
65
+ "Bash(aws ec2 delete-snapshot:*)",
66
+ "Bash(aws ec2 delete-volume:*)",
67
+ "Bash(aws ec2 delete-security-group:*)",
68
+ "Bash(aws ec2 delete-network-interface:*)",
69
+ "Bash(aws lambda delete-function:*)",
70
+ "Bash(aws rds delete-db-cluster-parameter-group:*)",
71
+ "Bash(aws rds delete-db-parameter-group:*)",
72
+ "Bash(aws s3api delete-objects:*)",
73
+ "Bash(aws sns delete-topic:*)",
74
+ "Bash(aws sqs delete-queue:*)",
75
+ "Bash(aws eks delete-nodegroup:*)",
76
+ "Bash(aws eks delete-addon:*)",
77
+ # Azure — resource group / networking / data (irreversible)
78
+ "Bash(az group delete:*)",
79
+ "Bash(az network vnet delete:*)",
80
+ "Bash(az network vnet subnet delete:*)",
81
+ "Bash(az network nsg delete:*)",
82
+ "Bash(az network public-ip delete:*)",
83
+ "Bash(az network application-gateway delete:*)",
84
+ "Bash(az network lb delete:*)",
85
+ "Bash(az network dns zone delete:*)",
86
+ "Bash(az network private-dns zone delete:*)",
87
+ "Bash(az vm delete:*)",
88
+ "Bash(az vmss delete:*)",
89
+ "Bash(az disk delete:*)",
90
+ "Bash(az snapshot delete:*)",
91
+ "Bash(az image delete:*)",
92
+ # Azure — databases / storage
93
+ "Bash(az sql server delete:*)",
94
+ "Bash(az sql db delete:*)",
95
+ "Bash(az cosmosdb delete:*)",
96
+ "Bash(az redis delete:*)",
97
+ "Bash(az storage account delete:*)",
98
+ "Bash(az storage container delete:*)",
99
+ "Bash(az storage blob delete-batch:*)",
100
+ # Azure — AKS / container
101
+ "Bash(az aks delete:*)",
102
+ "Bash(az aks nodepool delete:*)",
103
+ "Bash(az acr delete:*)",
104
+ # Azure — IAM / key vault / functions
105
+ "Bash(az role assignment delete:*)",
106
+ "Bash(az role definition delete:*)",
107
+ "Bash(az ad app delete:*)",
108
+ "Bash(az ad sp delete:*)",
109
+ "Bash(az keyvault delete:*)",
110
+ "Bash(az keyvault key delete:*)",
111
+ "Bash(az keyvault secret delete:*)",
112
+ "Bash(az functionapp delete:*)",
113
+ "Bash(az webapp delete:*)",
114
+ # Azure — messaging / monitoring
115
+ "Bash(az servicebus namespace delete:*)",
116
+ "Bash(az servicebus queue delete:*)",
117
+ "Bash(az servicebus topic delete:*)",
118
+ "Bash(az eventhubs namespace delete:*)",
119
+ "Bash(az eventhubs eventhub delete:*)",
120
+ "Bash(az monitor action-group delete:*)",
121
+ # GCP — project / cluster / database (irreversible)
122
+ "Bash(gcloud projects delete:*)",
123
+ "Bash(gcloud container clusters delete:*)",
124
+ "Bash(gcloud container node-pools delete:*)",
125
+ "Bash(gcloud sql instances delete:*)",
126
+ "Bash(gcloud sql databases delete:*)",
127
+ "Bash(gcloud services disable:*)",
128
+ "Bash(gsutil rb:*)",
129
+ "Bash(gsutil rm -r:*)",
130
+ # GCP — compute / IAM / storage
131
+ "Bash(gcloud compute firewall-rules delete:*)",
132
+ "Bash(gcloud compute instances delete:*)",
133
+ "Bash(gcloud compute networks delete:*)",
134
+ "Bash(gcloud compute disks delete:*)",
135
+ "Bash(gcloud compute images delete:*)",
136
+ "Bash(gcloud compute snapshots delete:*)",
137
+ "Bash(gcloud iam roles delete:*)",
138
+ "Bash(gcloud storage rm:*)",
139
+ # Kubernetes — critical cluster operations
140
+ "Bash(kubectl delete namespace:*)",
141
+ "Bash(kubectl delete node:*)",
142
+ "Bash(kubectl delete cluster:*)",
143
+ "Bash(kubectl delete pv:*)",
144
+ "Bash(kubectl delete persistentvolume:*)",
145
+ "Bash(kubectl delete pvc:*)",
146
+ "Bash(kubectl delete persistentvolumeclaim:*)",
147
+ "Bash(kubectl delete crd:*)",
148
+ "Bash(kubectl delete customresourcedefinition:*)",
149
+ "Bash(kubectl delete mutatingwebhookconfiguration:*)",
150
+ "Bash(kubectl delete validatingwebhookconfiguration:*)",
151
+ "Bash(kubectl delete clusterrole:*)",
152
+ "Bash(kubectl delete clusterrolebinding:*)",
153
+ "Bash(kubectl drain:*)",
154
+ # Flux
155
+ "Bash(flux delete:*)",
156
+ # Git — force push (history rewrite)
157
+ "Bash(git push --force:*)",
158
+ "Bash(git push -f:*)",
159
+ "Bash(git push origin --force:*)",
160
+ "Bash(git push origin -f:*)",
161
+ # Disk / filesystem destruction
162
+ "Bash(dd:*)",
163
+ "Bash(fdisk:*)",
164
+ "Bash(mkfs:*)",
165
+ "Bash(mkfs.ext4:*)",
166
+ "Bash(mkfs.ext3:*)",
167
+ "Bash(mkfs.fat:*)",
168
+ "Bash(mkfs.ntfs:*)",
169
+ # -------------------------------------------------------------------
170
+ # Generic wildcard rules — catch ALL present and future services.
171
+ # These complement the granular rules above; if a new cloud service
172
+ # is added, these patterns block its delete operations automatically.
173
+ # -------------------------------------------------------------------
174
+ # AWS — any "delete-*" subcommand across all services
175
+ "Bash(aws * delete-*:*)",
176
+ "Bash(aws * terminate-*:*)",
177
+ # Azure — any "delete" subcommand across all services
178
+ "Bash(az * delete:*)",
179
+ # GCP — any "delete" subcommand across all services
180
+ "Bash(gcloud * delete:*)",
181
+ "Bash(gsutil rb:*)",
182
+ "Bash(gsutil rm:*)",
183
+ "Bash(gcloud storage rm:*)",
184
+ # Kubernetes — all delete and drain operations
185
+ "Bash(kubectl delete:*)",
186
+ "Bash(kubectl drain:*)",
187
+ # Terraform / Terragrunt — destroy
188
+ "Bash(terraform destroy:*)",
189
+ "Bash(terragrunt destroy:*)",
190
+ "Bash(terragrunt run-all destroy:*)",
191
+ # Helm — uninstall
192
+ "Bash(helm uninstall:*)",
193
+ "Bash(helm delete:*)",
194
+ # Flux — uninstall
195
+ "Bash(flux uninstall:*)",
196
+ # Docker — bulk prune
197
+ "Bash(docker system prune:*)",
198
+ "Bash(docker volume prune:*)",
199
+ # Git — destructive history operations
200
+ "Bash(git reset --hard:*)",
201
+ # Repo deletion
202
+ "Bash(gh repo delete:*)",
203
+ "Bash(glab project delete:*)",
204
+ ]
205
+
206
+ # Base permissions for security-only mode
207
+ SECURITY_PERMISSIONS = {
208
+ "permissions": {
209
+ "allow": [
210
+ "Bash(*)",
211
+ "Read",
212
+ "Glob",
213
+ "Grep",
214
+ "BashOutput",
215
+ "ExitPlanMode",
216
+ "KillShell",
217
+ "Skill",
218
+ "SlashCommand",
219
+ "TodoWrite",
220
+ "WebFetch",
221
+ "WebSearch",
222
+ "NotebookEdit",
223
+ ],
224
+ "deny": _DENY_RULES,
225
+ "ask": [],
226
+ }
227
+ }
228
+
229
+ # Extended permissions for ops mode (adds agent dispatch tools)
230
+ OPS_PERMISSIONS = {
231
+ "permissions": {
232
+ "allow": [
233
+ "Bash(*)",
234
+ "Read",
235
+ "Glob",
236
+ "Grep",
237
+ "BashOutput",
238
+ "ExitPlanMode",
239
+ "KillShell",
240
+ "Skill",
241
+ "SlashCommand",
242
+ "Task",
243
+ "Agent",
244
+ "SendMessage",
245
+ "AskUserQuestion",
246
+ "TodoWrite",
247
+ "WebFetch",
248
+ "WebSearch",
249
+ "NotebookEdit",
250
+ "Edit(/tmp/*)",
251
+ "Write(/tmp/*)",
252
+ ],
253
+ "deny": _DENY_RULES,
254
+ "ask": [],
255
+ }
256
+ }
257
+
258
+
259
+ def is_first_run() -> bool:
260
+ """Check if this is the first time the plugin runs."""
261
+ marker = get_plugin_data_dir() / MARKER_FILE
262
+ return not marker.exists()
263
+
264
+
265
+ def mark_initialized() -> None:
266
+ """Mark the plugin as initialized."""
267
+ marker = get_plugin_data_dir() / MARKER_FILE
268
+ marker.write_text(json.dumps({
269
+ "initialized_at": datetime.now().isoformat(),
270
+ "mode": get_plugin_mode(),
271
+ }))
272
+ logger.info("Plugin marked as initialized: %s", marker)
273
+
274
+
275
+ def setup_project_permissions() -> bool:
276
+ """Merge gaia permissions into .claude/settings.local.json.
277
+
278
+ Uses settings.local.json (highest project-level precedence) so that
279
+ /reload-plugins picks up changes mid-session without restart.
280
+ Preserves enabledPlugins and any existing user configuration.
281
+
282
+ Returns True if settings were modified (reload needed).
283
+ """
284
+ claude_dir = Path.cwd() / ".claude"
285
+ settings_path = claude_dir / "settings.local.json"
286
+
287
+ mode = get_plugin_mode()
288
+ our_perms = OPS_PERMISSIONS if mode == "ops" else SECURITY_PERMISSIONS
289
+ our_allow = set(our_perms["permissions"]["allow"])
290
+ our_deny = set(our_perms["permissions"].get("deny", []))
291
+
292
+ # Load existing settings.local.json (has enabledPlugins from install)
293
+ existing = {}
294
+ if settings_path.exists():
295
+ try:
296
+ existing = json.loads(settings_path.read_text())
297
+ except (json.JSONDecodeError, OSError):
298
+ pass
299
+
300
+ # Merge permissions — add ours without removing user's
301
+ perms = existing.get("permissions", {})
302
+ current_allow = set(perms.get("allow", []))
303
+ current_deny = set(perms.get("deny", []))
304
+
305
+ merged_allow = sorted(current_allow | our_allow)
306
+ merged_deny = sorted(current_deny | our_deny)
307
+
308
+ if current_allow == set(merged_allow) and current_deny == set(merged_deny):
309
+ logger.info("Project permissions already include gaia rules, skipping")
310
+ return False
311
+
312
+ # Update only permissions, preserve everything else (enabledPlugins, etc.)
313
+ existing.setdefault("permissions", {})
314
+ existing["permissions"]["allow"] = merged_allow
315
+ existing["permissions"]["deny"] = merged_deny
316
+ existing["permissions"].setdefault("ask", [])
317
+
318
+ # Add env vars (smart merge: add if not present, don't overwrite)
319
+ env = existing.setdefault("env", {})
320
+ if "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS" not in env:
321
+ env["CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS"] = "1"
322
+
323
+ claude_dir.mkdir(parents=True, exist_ok=True)
324
+ settings_path.write_text(json.dumps(existing, indent=2) + "\n")
325
+ logger.info("Merged gaia %s permissions and env into %s", mode, settings_path)
326
+ return True
327
+
328
+
329
+ def ensure_plugin_registry() -> None:
330
+ """Create plugin-registry.json if missing.
331
+
332
+ Detection strategies (in order):
333
+ 1. CLAUDE_PLUGIN_ROOT env var (plugin marketplace mode):
334
+ Path looks like .../cache/marketplace/gaia-ops/4.4.0-rc.2
335
+ 2. NPM package detection: resolve package name and version from
336
+ node_modules path and package.json
337
+ """
338
+ import os
339
+ data_dir = get_plugin_data_dir()
340
+ registry_path = data_dir / "plugin-registry.json"
341
+ if registry_path.exists():
342
+ return
343
+
344
+ plugin_name = None
345
+ plugin_version = None
346
+ source = None
347
+
348
+ # Strategy 1: CLAUDE_PLUGIN_ROOT (plugin marketplace or --plugin-dir)
349
+ plugin_root = os.environ.get("CLAUDE_PLUGIN_ROOT", "")
350
+ if plugin_root:
351
+ root_path = Path(plugin_root)
352
+ # First, try to read .claude-plugin/plugin.json (most reliable)
353
+ plugin_json = root_path / ".claude-plugin" / "plugin.json"
354
+ if plugin_json.exists():
355
+ try:
356
+ pdata = json.loads(plugin_json.read_text())
357
+ plugin_name = pdata.get("name")
358
+ plugin_version = pdata.get("version")
359
+ source = "plugin-mode"
360
+ except (json.JSONDecodeError, OSError):
361
+ pass
362
+ # Fallback: parse path (marketplace layout: .../name/version)
363
+ if not plugin_name:
364
+ parts = root_path.parts
365
+ if len(parts) >= 2:
366
+ plugin_name = parts[-2]
367
+ plugin_version = parts[-1]
368
+ source = "plugin-mode"
369
+
370
+ # Strategy 2: NPM package detection
371
+ if not plugin_name:
372
+ npm_info = _detect_npm_package_info()
373
+ if npm_info:
374
+ plugin_name, plugin_version = npm_info
375
+ source = "npm-mode"
376
+
377
+ if not plugin_name:
378
+ return
379
+
380
+ registry = {
381
+ "installed": [{"name": plugin_name, "version": plugin_version or "unknown"}],
382
+ "source": source,
383
+ }
384
+ data_dir.mkdir(parents=True, exist_ok=True)
385
+ registry_path.write_text(json.dumps(registry, indent=2) + "\n")
386
+ logger.info("Created plugin-registry.json: %s@%s (source: %s)", plugin_name, plugin_version, source)
387
+
388
+
389
+ def _detect_npm_package_info() -> tuple[str, str | None] | None:
390
+ """Detect plugin name and version from NPM package path.
391
+
392
+ When installed via npm, this module lives at:
393
+ .../node_modules/@jaguilar87/gaia-ops/hooks/modules/core/plugin_setup.py
394
+
395
+ Returns (plugin_name, version) or None.
396
+ """
397
+ module_path = Path(__file__).resolve()
398
+ parts = module_path.parts
399
+
400
+ # Find node_modules in path and extract package name
401
+ pkg_name = None
402
+ pkg_root = None
403
+ for i, part in enumerate(parts):
404
+ if part == "node_modules" and i + 1 < len(parts):
405
+ next_part = parts[i + 1]
406
+ if next_part.startswith("@") and i + 2 < len(parts):
407
+ # Scoped package: @scope/name
408
+ pkg_name = parts[i + 2]
409
+ pkg_root = Path(*parts[:i + 3])
410
+ else:
411
+ pkg_name = next_part
412
+ pkg_root = Path(*parts[:i + 2])
413
+ break
414
+
415
+ if not pkg_name or pkg_name not in ("gaia-ops", "gaia-security"):
416
+ return None
417
+
418
+ # Try to read version from package.json
419
+ version = None
420
+ if pkg_root:
421
+ pkg_json = Path("/") / pkg_root / "package.json"
422
+ try:
423
+ if pkg_json.exists():
424
+ data = json.loads(pkg_json.read_text())
425
+ version = data.get("version")
426
+ except Exception:
427
+ pass
428
+
429
+ return (pkg_name, version)
430
+
431
+
432
+ def setup_project_hooks() -> bool:
433
+ """Merge hooks from hooks.json into .claude/settings.local.json.
434
+
435
+ In npm mode, Claude Code reads hooks from settings files, not hooks.json.
436
+ This converts ${CLAUDE_PLUGIN_ROOT}/hooks/<script> paths to .claude/hooks/<script>
437
+ and merges them into settings.local.json with deduplication by command string.
438
+
439
+ Returns True if settings were modified.
440
+ """
441
+ import re
442
+
443
+ claude_dir = Path.cwd() / ".claude"
444
+ settings_path = claude_dir / "settings.local.json"
445
+
446
+ # Find hooks.json — try package root (npm) or plugin root
447
+ hooks_json_path = None
448
+ # Strategy 1: relative to this module (npm layout)
449
+ module_dir = Path(__file__).resolve().parent.parent.parent
450
+ candidate = module_dir / "hooks.json"
451
+ if candidate.is_file():
452
+ hooks_json_path = candidate
453
+ else:
454
+ # Strategy 2: .claude/hooks/hooks.json (symlinked)
455
+ candidate2 = claude_dir / "hooks" / "hooks.json"
456
+ if candidate2.is_file():
457
+ hooks_json_path = candidate2
458
+
459
+ if not hooks_json_path:
460
+ logger.info("hooks.json not found, skipping hooks merge")
461
+ return False
462
+
463
+ try:
464
+ hooks_data = json.loads(hooks_json_path.read_text())
465
+ except (json.JSONDecodeError, OSError):
466
+ logger.warning("hooks.json is invalid, skipping hooks merge")
467
+ return False
468
+
469
+ # Unwrap outer "hooks" key if present
470
+ source_hooks = hooks_data.get("hooks", hooks_data)
471
+
472
+ # Convert ${CLAUDE_PLUGIN_ROOT}/hooks/<script> to .claude/hooks/<script>
473
+ def convert_command(cmd: str) -> str:
474
+ return re.sub(r'\$\{CLAUDE_PLUGIN_ROOT\}/hooks/', '.claude/hooks/', cmd)
475
+
476
+ converted_hooks: dict = {}
477
+ for event, entries in source_hooks.items():
478
+ converted_hooks[event] = []
479
+ for entry in entries:
480
+ new_entry = dict(entry)
481
+ if "hooks" in new_entry:
482
+ new_entry["hooks"] = [
483
+ {**h, "command": convert_command(h["command"])} if "command" in h else h
484
+ for h in new_entry["hooks"]
485
+ ]
486
+ converted_hooks[event].append(new_entry)
487
+
488
+ # Load existing settings.local.json
489
+ existing: dict = {}
490
+ if settings_path.exists():
491
+ try:
492
+ existing = json.loads(settings_path.read_text())
493
+ except (json.JSONDecodeError, OSError):
494
+ existing = {}
495
+
496
+ # Smart merge: deduplicate by command string
497
+ existing_hooks = existing.get("hooks", {})
498
+ changed = False
499
+
500
+ for event, new_entries in converted_hooks.items():
501
+ if event not in existing_hooks:
502
+ existing_hooks[event] = new_entries
503
+ changed = True
504
+ continue
505
+
506
+ # Collect existing command strings
507
+ existing_commands: set = set()
508
+ for entry in existing_hooks[event]:
509
+ for h in entry.get("hooks", []):
510
+ if "command" in h:
511
+ existing_commands.add(h["command"])
512
+
513
+ # Add entries whose commands are not already present
514
+ for new_entry in new_entries:
515
+ new_commands = [h["command"] for h in new_entry.get("hooks", []) if "command" in h]
516
+ all_present = len(new_commands) > 0 and all(c in existing_commands for c in new_commands)
517
+ if not all_present:
518
+ existing_hooks[event].append(new_entry)
519
+ changed = True
520
+
521
+ if not changed:
522
+ logger.info("settings.local.json hooks already up to date")
523
+ return False
524
+
525
+ existing["hooks"] = existing_hooks
526
+ claude_dir.mkdir(parents=True, exist_ok=True)
527
+ settings_path.write_text(json.dumps(existing, indent=2) + "\n")
528
+ logger.info("Merged hooks into %s", settings_path)
529
+ return True
530
+
531
+
532
+ def run_first_time_setup(mark_done: bool = True) -> str | None:
533
+ """Run setup. Returns a reload message if permissions were written.
534
+
535
+ Args:
536
+ mark_done: If True, mark the plugin as initialized after setup.
537
+ Set to False when the caller wants to defer marking
538
+ (e.g., UserPromptSubmit marks after showing the welcome).
539
+ """
540
+ # Always ensure registry, permissions, and hooks exist (even on subsequent runs)
541
+ ensure_plugin_registry()
542
+ reload_needed = setup_project_permissions()
543
+ hooks_changed = setup_project_hooks()
544
+ reload_needed = reload_needed or hooks_changed
545
+
546
+ if not is_first_run():
547
+ if reload_needed:
548
+ return "Permissions updated. Run /reload-plugins to activate."
549
+ return None
550
+
551
+ if mark_done:
552
+ mark_initialized()
553
+
554
+ if reload_needed:
555
+ mode = get_plugin_mode()
556
+ return f"GAIA {mode} setup complete. Run /reload-plugins to activate permissions."
557
+
558
+ return None