@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,708 @@
1
+ """
2
+ Bash command validator.
3
+
4
+ Primary security gate for all Bash tool invocations. With Bash(*) in the
5
+ settings.json allow list, ALL commands reach this hook -- it is the sole
6
+ enforcement layer for dangerous command detection.
7
+
8
+ Pipeline (ordered by priority):
9
+ 0. Indirect execution detection -- bash -c, eval, python -c etc. (T2 approval)
10
+ 1. blocked_commands FIRST -- permanently denied patterns (exit 2)
11
+ 2. Claude footer stripping -- transparent cleanup via updatedInput
12
+ 3. Commit message validation -- conventional commits format
13
+ 4. Cloud pipe/redirect/chain check -- corrective deny (exit 0)
14
+ 5. Mutative verb detection -- MUTATIVE -> nonce-based deny (exit 0)
15
+ 6. Everything else -> SAFE (auto-approved by elimination)
16
+ """
17
+
18
+ import re
19
+ import json
20
+ import logging
21
+ from typing import Dict, Any, Optional, List
22
+ from dataclasses import dataclass
23
+
24
+ from ..security.tiers import SecurityTier
25
+ from ..security.blocked_commands import is_blocked_command
26
+ from ..security.gitops_validator import validate_gitops_workflow
27
+ from ..security.mutative_verbs import (
28
+ detect_mutative_command,
29
+ build_t3_block_response,
30
+ )
31
+ from ..security.approval_grants import (
32
+ check_approval_grant,
33
+ confirm_grant,
34
+ find_pending_for_command,
35
+ generate_nonce,
36
+ last_check_found_expired,
37
+ write_pending_approval,
38
+ )
39
+ from ..security.approval_messages import (
40
+ build_pending_approval_unavailable_message,
41
+ build_t3_approval_instructions,
42
+ )
43
+ from .shell_parser import get_shell_parser
44
+ from .cloud_pipe_validator import validate_cloud_pipe
45
+ from .hook_response import build_hook_permission_response
46
+
47
+ logger = logging.getLogger(__name__)
48
+
49
+
50
+ @dataclass
51
+ class BashValidationResult:
52
+ """Result of Bash command validation."""
53
+ allowed: bool
54
+ tier: SecurityTier
55
+ reason: str
56
+ suggestions: List[str] = None
57
+ modified_input: Optional[Dict[str, Any]] = None
58
+ # When set, the caller should return this dict (exit 0) instead of a
59
+ # plain error string (exit 2). Used for structured block responses that
60
+ # should correct the agent rather than terminate execution.
61
+ block_response: Optional[Dict[str, Any]] = None
62
+
63
+ def __post_init__(self):
64
+ if self.suggestions is None:
65
+ self.suggestions = []
66
+
67
+
68
+ # Patterns for AI tool attribution footers (auto-stripped from commits).
69
+ # Covers Claude Code, GitHub Copilot, Aider, Windsurf, and any future
70
+ # tool using the Co-authored-by git trailer convention.
71
+ FORBIDDEN_FOOTER_PATTERNS = [
72
+ r"Generated with\s+Claude Code",
73
+ r"Generated with\s+\[?Claude Code\]?",
74
+ r"Co-Authored-By:\s+Claude\b",
75
+ r"Co-authored-by:\s+GitHub Copilot\b",
76
+ r"Co-authored-by:\s+aider\b",
77
+ r"Co-authored-by:\s+Windsurf\b",
78
+ r"Co-authored-by:\s+Cursor\b",
79
+ r"Co-authored-by:\s+Codex\b",
80
+ r"Co-authored-by:\s+Gemini\b",
81
+ ]
82
+
83
+ # ---------------------------------------------------------------------------
84
+ # Indirect execution wrappers — commands that execute arbitrary strings.
85
+ # These bypass regex-based command blocking because the real command is
86
+ # hidden inside a string argument. Classified as T2 (requires approval)
87
+ # so the user sees what will actually run.
88
+ # ---------------------------------------------------------------------------
89
+ INDIRECT_EXEC_PATTERNS = [
90
+ re.compile(r"^bash\s+-c\s+", re.IGNORECASE),
91
+ re.compile(r"^sh\s+-c\s+", re.IGNORECASE),
92
+ re.compile(r"^zsh\s+-c\s+", re.IGNORECASE),
93
+ re.compile(r"^dash\s+-c\s+", re.IGNORECASE),
94
+ re.compile(r"^\s*eval\s+", re.IGNORECASE),
95
+ re.compile(r"^python3?\s+-c\s+", re.IGNORECASE),
96
+ re.compile(r"^node\s+-e\s+", re.IGNORECASE),
97
+ re.compile(r"^perl\s+-e\s+", re.IGNORECASE),
98
+ re.compile(r"^ruby\s+-e\s+", re.IGNORECASE),
99
+ # Process substitution and heredoc piped to shell
100
+ re.compile(r"^bash\s+<\(", re.IGNORECASE),
101
+ re.compile(r"^sh\s+<\(", re.IGNORECASE),
102
+ ]
103
+
104
+ class BashValidator:
105
+ """Validator for Bash tool invocations."""
106
+
107
+ def __init__(self):
108
+ """Initialize validator."""
109
+ self.shell_parser = get_shell_parser()
110
+
111
+ def _detect_indirect_execution(self, command: str) -> Optional[BashValidationResult]:
112
+ """Detect indirect execution wrappers that can bypass regex blocking.
113
+
114
+ Commands like 'bash -c "az group delete"' hide the real command inside
115
+ a string. We classify these as T2 (mutative) so they require user
116
+ approval via the nonce workflow, giving the human a chance to inspect
117
+ what will actually run.
118
+
119
+ Returns BashValidationResult if indirect execution detected, else None.
120
+ """
121
+ for pattern in INDIRECT_EXEC_PATTERNS:
122
+ if pattern.search(command):
123
+ # Also check if the inner payload contains a blocked command.
124
+ # Extract the string argument after the wrapper.
125
+ inner = self._extract_inner_command(command)
126
+ if inner:
127
+ blocked = is_blocked_command(inner)
128
+ if blocked.is_blocked:
129
+ return BashValidationResult(
130
+ allowed=False,
131
+ tier=SecurityTier.T3_BLOCKED,
132
+ reason=(
133
+ f"Indirect execution of blocked command detected: "
134
+ f"{blocked.category} (via wrapper)"
135
+ ),
136
+ suggestions=[
137
+ blocked.suggestion or "Run the command directly instead of via a shell wrapper.",
138
+ ],
139
+ )
140
+
141
+ # Not blocked but still indirect — route through approval
142
+ logger.info("Indirect execution detected: %s", command[:80])
143
+ result = detect_mutative_command(command)
144
+ if result.is_mutative:
145
+ return None # Already mutative, will be caught by mutative_verbs
146
+
147
+ # For interpreters with inline code analysis (python3 -c),
148
+ # mutative_verbs.py has dedicated pattern scanning that
149
+ # distinguishes safe code (json.dumps, sys.version) from
150
+ # dangerous code (os.system, subprocess.run). If it classified
151
+ # the inline code as safe, trust that analysis and allow it
152
+ # through without forcing an "ask" dialog.
153
+ from ..security.mutative_verbs import _INLINE_CODE_CLIS
154
+ base_cmd = command.strip().split()[0].rsplit("/", 1)[-1].lower()
155
+ if base_cmd in _INLINE_CODE_CLIS:
156
+ logger.info(
157
+ "Inline code classified as safe by pattern scanner: %s",
158
+ command[:80],
159
+ )
160
+ return None # Safe inline code, proceed to normal validation
161
+
162
+ # Shell wrappers (bash -c, eval, etc.) hide the real command
163
+ # in a string — no dedicated scanner exists. Force "ask" so
164
+ # the user can inspect what will actually run.
165
+ hook_block = build_hook_permission_response(
166
+ "ask",
167
+ (
168
+ "Indirect execution detected. The command uses a shell "
169
+ "wrapper (bash -c, eval, etc.) that can bypass "
170
+ "security checks. Please confirm you want to run this."
171
+ ),
172
+ )
173
+ return BashValidationResult(
174
+ allowed=False,
175
+ tier=SecurityTier.T2_DRY_RUN,
176
+ reason="Indirect execution wrapper detected — requires confirmation",
177
+ block_response=hook_block,
178
+ )
179
+ return None
180
+
181
+ def _extract_inner_command(self, command: str) -> Optional[str]:
182
+ """Extract the inner command from an indirect execution wrapper.
183
+
184
+ E.g., 'bash -c "az group delete --name foo"' → 'az group delete --name foo'
185
+ """
186
+ # Match: shell -c "..." or shell -c '...'
187
+ match = re.search(r"""-[ce]\s+(['"])(.*?)\1""", command, re.DOTALL)
188
+ if match:
189
+ return match.group(2).strip()
190
+ # Match: shell -c ... (unquoted, take rest of line)
191
+ match = re.search(r"-[ce]\s+(\S+.*)", command)
192
+ if match:
193
+ return match.group(1).strip()
194
+ return None
195
+
196
+ def _has_operators(self, command: str) -> bool:
197
+ """Quick check if command has operators (before parsing)."""
198
+ # Fast check for common operators outside quotes
199
+ # This avoids expensive parsing for 70% of commands
200
+ if not any(op in command for op in ['|', '&&', '||', ';', '\n']):
201
+ return False
202
+ return True
203
+
204
+ def validate(
205
+ self,
206
+ command: str,
207
+ is_subagent: bool = False,
208
+ session_id: str = "",
209
+ ) -> BashValidationResult:
210
+ """
211
+ Validate a Bash command.
212
+
213
+ Args:
214
+ command: Command string to validate
215
+ is_subagent: True when running in subagent context
216
+ session_id: Session ID for approval scoping
217
+
218
+ Returns:
219
+ BashValidationResult with validation details
220
+ """
221
+ if not command or not command.strip():
222
+ return BashValidationResult(
223
+ allowed=False,
224
+ tier=SecurityTier.T3_BLOCKED,
225
+ reason="Empty command not allowed",
226
+ )
227
+
228
+ command = command.strip()
229
+
230
+ # ================================================================
231
+ # EARLY NORMALIZATION: Strip AI attribution footers before any
232
+ # other processing. This ensures the same normalized command
233
+ # string is used for blocked-command checks, compound parsing,
234
+ # mutative verb detection, pending approval writes, AND pending
235
+ # approval lookups. Without this, write_pending_approval() and
236
+ # find_pending_for_command() could see different strings on the
237
+ # first attempt vs. retry, causing nonce mismatch loops.
238
+ # ================================================================
239
+ command_was_modified = False
240
+ if self._detect_claude_footers(command):
241
+ command = self._strip_claude_footers(command)
242
+ command_was_modified = True
243
+ logger.info("Auto-stripped Claude Code footer from commit command")
244
+
245
+ # ================================================================
246
+ # PRIORITY 0: Indirect execution detection.
247
+ # Commands like "bash -c '...'" or "eval '...'" can hide blocked
248
+ # commands inside string arguments, bypassing regex patterns.
249
+ # Detected wrappers are routed to approval or blocked if the inner
250
+ # payload matches a blocked command.
251
+ # ================================================================
252
+ indirect_result = self._detect_indirect_execution(command)
253
+ if indirect_result is not None:
254
+ return indirect_result
255
+
256
+ # ================================================================
257
+ # PRIORITY 1: Blocked commands check on FULL command (exit 2).
258
+ # This MUST run before any other validator to ensure permanently
259
+ # blocked commands (kubectl delete namespace, etc.) are caught
260
+ # with a reliable exit 2 — even if the command also triggers
261
+ # cloud_pipe_validator or has compound operators.
262
+ # ================================================================
263
+ blocked_result = is_blocked_command(command)
264
+ if blocked_result.is_blocked:
265
+ return BashValidationResult(
266
+ allowed=False,
267
+ tier=SecurityTier.T3_BLOCKED,
268
+ reason=f"Command blocked by security policy: {blocked_result.category}",
269
+ suggestions=[blocked_result.suggestion] if blocked_result.suggestion else [],
270
+ )
271
+
272
+ # Parse compound commands once (reused for blocked-command check and validation dispatch).
273
+ # Runs AFTER footer stripping so components also use the normalized command.
274
+ has_operators = self._has_operators(command)
275
+ parsed_components = None
276
+ if has_operators:
277
+ parsed_components = self.shell_parser.parse(command)
278
+ # Check each component of compound commands against the deny list.
279
+ # This catches "ls && kubectl delete namespace prod" early.
280
+ for component in parsed_components:
281
+ comp_blocked = is_blocked_command(component.strip())
282
+ if comp_blocked.is_blocked:
283
+ return BashValidationResult(
284
+ allowed=False,
285
+ tier=SecurityTier.T3_BLOCKED,
286
+ reason=f"Command blocked by security policy: {comp_blocked.category}",
287
+ suggestions=[comp_blocked.suggestion] if comp_blocked.suggestion else [],
288
+ )
289
+
290
+ # Validate git commit messages (on the potentially cleaned command)
291
+ if "git commit" in command and "-m" in command:
292
+ commit_validation = self._validate_commit_message(command)
293
+ if not commit_validation.allowed:
294
+ return commit_validation
295
+
296
+ # Cloud pipe/redirect/chaining check -- runs AFTER blocked commands.
297
+ # Returns a structured block response dict if a violation is found.
298
+ # block_response is set so the caller emits JSON and exits 0 (corrective),
299
+ # not a plain string with exit 2 (which would terminate the agent).
300
+ pipe_block = validate_cloud_pipe(command)
301
+ if pipe_block is not None:
302
+ return BashValidationResult(
303
+ allowed=False,
304
+ tier=SecurityTier.T3_BLOCKED,
305
+ reason=pipe_block["hookSpecificOutput"]["permissionDecisionReason"],
306
+ suggestions=[],
307
+ modified_input=None,
308
+ block_response=pipe_block,
309
+ )
310
+
311
+ # Dispatch to single or compound validation using already-parsed components
312
+ if not has_operators:
313
+ result = self._validate_single_command(
314
+ command, is_subagent=is_subagent, session_id=session_id,
315
+ )
316
+ elif parsed_components is not None and len(parsed_components) > 1:
317
+ result = self._validate_compound_command(
318
+ parsed_components, is_subagent=is_subagent, session_id=session_id,
319
+ )
320
+ else:
321
+ result = self._validate_single_command(
322
+ command, is_subagent=is_subagent, session_id=session_id,
323
+ )
324
+
325
+ # Attach cleaned command for hook to emit via updatedInput.
326
+ # Set regardless of result.allowed so the ask path can include it too.
327
+ if command_was_modified:
328
+ result.modified_input = {"command": command}
329
+ # If the result is an "ask" block_response, inject updatedInput
330
+ # so the modification survives the native permission dialog.
331
+ if (
332
+ result.block_response is not None
333
+ and result.block_response.get("hookSpecificOutput", {}).get(
334
+ "permissionDecision"
335
+ ) == "ask"
336
+ ):
337
+ result.block_response["hookSpecificOutput"]["updatedInput"] = {
338
+ "command": command
339
+ }
340
+
341
+ return result
342
+
343
+ def _validate_single_command(
344
+ self,
345
+ command: str,
346
+ is_subagent: bool = False,
347
+ session_id: str = "",
348
+ ) -> BashValidationResult:
349
+ """Validate a single command (no operators).
350
+
351
+ Simplified pipeline:
352
+ 0. Indirect execution detection (for compound command components)
353
+ 1. Mutative verb detection -> block with nonce or allow with grant
354
+ 2. GitOps policy validation (for kubectl/helm/flux)
355
+ 3. Everything else -> SAFE by elimination
356
+
357
+ Args:
358
+ command: The command to validate.
359
+ is_subagent: True when running in subagent context (generates
360
+ approval_id + deny). False for orchestrator (returns ask).
361
+ session_id: Session ID for pending approval scoping.
362
+
363
+ Note: is_blocked_command() is NOT called here because validate()
364
+ already checks the full command AND each compound component against
365
+ the deny list before dispatching to this method.
366
+ """
367
+
368
+ # Indirect execution check for compound command components.
369
+ # When validate() splits "cd /tmp && python3 -c '...'" into parts,
370
+ # the python3 -c component needs the same indirect execution gate
371
+ # that the full command gets in validate().
372
+ indirect_result = self._detect_indirect_execution(command)
373
+ if indirect_result is not None:
374
+ return indirect_result
375
+
376
+ # Mutative verb detection
377
+ result = detect_mutative_command(command)
378
+ if result.is_mutative:
379
+ # Check for an active approval grant before blocking.
380
+ grant = check_approval_grant(command, session_id=session_id)
381
+ if grant is not None:
382
+ if grant.confirmed:
383
+ # Already confirmed and consumed -- should not reach
384
+ # here (single-use). But if it does, allow through.
385
+ logger.info(
386
+ "T3 command allowed via confirmed grant: %s (scope='%s')",
387
+ command[:80], grant.approved_scope,
388
+ )
389
+ return BashValidationResult(
390
+ allowed=True,
391
+ tier=SecurityTier.T3_BLOCKED,
392
+ reason="Grant confirmed",
393
+ )
394
+ else:
395
+ # Grant exists, not yet confirmed -- GAIA approved,
396
+ # let it through. PostToolUse will confirm and consume
397
+ # the grant after successful execution.
398
+ logger.info(
399
+ "T3 command passthrough via active grant: %s (scope='%s')",
400
+ command[:80], grant.approved_scope,
401
+ )
402
+ return BashValidationResult(
403
+ allowed=True,
404
+ tier=SecurityTier.T3_BLOCKED,
405
+ reason="Grant active, pending confirmation",
406
+ )
407
+ else:
408
+ if is_subagent:
409
+ # Subagent context: check for an existing pending
410
+ # approval first (retry scenario). If found, reuse
411
+ # the same nonce to prevent infinite approval_id
412
+ # generation loops while the user reviews.
413
+ existing_nonce = find_pending_for_command(
414
+ session_id or "", command,
415
+ )
416
+ if existing_nonce:
417
+ approval_id = existing_nonce
418
+ logger.info(
419
+ "Reusing pending approval_id=%s for retry: %s",
420
+ approval_id, command[:80],
421
+ )
422
+ reason = (
423
+ f"[T3_BLOCKED] This command requires user approval.\n"
424
+ f"Do NOT retry this command. Report REVIEW with this approval_id in your json:contract.\n"
425
+ f"Command: {command}\n"
426
+ f"Verb: '{result.verb}' ({result.category})\n"
427
+ f"approval_id: {approval_id}"
428
+ )
429
+ hook_deny = build_hook_permission_response("deny", reason)
430
+ return BashValidationResult(
431
+ allowed=False,
432
+ tier=SecurityTier.T3_BLOCKED,
433
+ reason=f"T3 {result.category.lower()} command: {result.reason}",
434
+ block_response=hook_deny,
435
+ )
436
+ # No existing pending -- generate a new nonce.
437
+ # The ElicitationResult hook will activate the
438
+ # grant when the user approves via AskUserQuestion.
439
+ approval_id = generate_nonce()
440
+ pending_path = write_pending_approval(
441
+ nonce=approval_id,
442
+ command=command,
443
+ danger_verb=result.verb,
444
+ danger_category=result.category,
445
+ session_id=session_id or None,
446
+ )
447
+ if pending_path is None:
448
+ # Persistence failure — fall back to ask
449
+ logger.warning(
450
+ "Failed to persist pending approval for subagent; "
451
+ "falling back to ask: %s",
452
+ command[:80],
453
+ )
454
+ reason = build_pending_approval_unavailable_message()
455
+ hook_ask = build_hook_permission_response("ask", reason)
456
+ return BashValidationResult(
457
+ allowed=False,
458
+ tier=SecurityTier.T3_BLOCKED,
459
+ reason="Pending approval persistence failed",
460
+ block_response=hook_ask,
461
+ )
462
+ reason = (
463
+ f"[T3_BLOCKED] This command requires user approval.\n"
464
+ f"Do NOT retry this command. Report REVIEW with this approval_id in your json:contract.\n"
465
+ f"Command: {command}\n"
466
+ f"Verb: '{result.verb}' ({result.category})\n"
467
+ f"approval_id: {approval_id}"
468
+ )
469
+ hook_deny = build_hook_permission_response("deny", reason)
470
+ return BashValidationResult(
471
+ allowed=False,
472
+ tier=SecurityTier.T3_BLOCKED,
473
+ reason=f"T3 {result.category.lower()} command: {result.reason}",
474
+ block_response=hook_deny,
475
+ )
476
+ else:
477
+ # Orchestrator context: route through native 'ask' dialog.
478
+ # The user sees the native permission prompt and approves
479
+ # directly. No approval_id is generated.
480
+ reason = (
481
+ f"[T3_APPROVAL_REQUIRED] {result.category} operation detected.\n"
482
+ f"Command: {command}\n"
483
+ f"Verb: '{result.verb}' ({result.category})\n"
484
+ f"Reason: {result.reason}"
485
+ )
486
+ hook_ask = build_hook_permission_response("ask", reason)
487
+ return BashValidationResult(
488
+ allowed=False,
489
+ tier=SecurityTier.T3_BLOCKED,
490
+ reason=f"Dangerous {result.category.lower()} command: {result.reason}",
491
+ block_response=hook_ask,
492
+ )
493
+
494
+ # Check GitOps policy for kubectl/helm/flux commands
495
+ if any(keyword in command for keyword in ("kubectl", "helm", "flux")):
496
+ gitops_result = validate_gitops_workflow(command)
497
+ if not gitops_result.allowed:
498
+ return BashValidationResult(
499
+ allowed=False,
500
+ tier=SecurityTier.T3_BLOCKED,
501
+ reason=f"GitOps policy violation: {gitops_result.reason}",
502
+ suggestions=gitops_result.suggestions,
503
+ )
504
+
505
+ # Not blocked, not mutative -> SAFE by elimination
506
+ return BashValidationResult(
507
+ allowed=True,
508
+ tier=SecurityTier.T0_READ_ONLY,
509
+ reason="Safe by elimination (not blocked, not mutative)",
510
+ )
511
+
512
+ def _validate_compound_command(
513
+ self,
514
+ components: List[str],
515
+ is_subagent: bool = False,
516
+ session_id: str = "",
517
+ ) -> BashValidationResult:
518
+ """Validate a compound command (multiple components)."""
519
+ logger.info(f"Compound command detected with {len(components)} components")
520
+
521
+ component_results: List[BashValidationResult] = []
522
+ for i, component in enumerate(components, 1):
523
+ result = self._validate_single_command(
524
+ component, is_subagent=is_subagent, session_id=session_id,
525
+ )
526
+
527
+ if not result.allowed:
528
+ return BashValidationResult(
529
+ allowed=False,
530
+ tier=SecurityTier.T3_BLOCKED,
531
+ reason=(
532
+ f"Compound command blocked: component {i}/{len(components)} "
533
+ f"'{component[:50]}' is not allowed\n"
534
+ f"Reason: {result.reason}"
535
+ ),
536
+ suggestions=result.suggestions,
537
+ block_response=result.block_response,
538
+ )
539
+ component_results.append(result)
540
+
541
+ # All components validated -- derive highest tier from results already
542
+ # computed by _validate_single_command (avoids redundant classification).
543
+ tier_order = ["T0", "T1", "T2", "T3"]
544
+ highest_tier = max(
545
+ (r.tier for r in component_results),
546
+ key=lambda t: tier_order.index(t.value),
547
+ )
548
+
549
+ return BashValidationResult(
550
+ allowed=True,
551
+ tier=highest_tier,
552
+ reason=f"All {len(components)} components validated",
553
+ )
554
+
555
+ def _detect_claude_footers(self, command: str) -> bool:
556
+ """Detect Claude Code attribution footers in command."""
557
+ for pattern in FORBIDDEN_FOOTER_PATTERNS:
558
+ if re.search(pattern, command, re.IGNORECASE):
559
+ return True
560
+ return False
561
+
562
+ def _strip_claude_footers(self, command: str) -> str:
563
+ """
564
+ Strip Claude Code attribution footers from a command.
565
+
566
+ Removes full lines matching forbidden footer patterns.
567
+ Works on raw command string regardless of quoting/HEREDOC format.
568
+ Preserves trailing quote/paren characters that close the commit
569
+ message (e.g., the closing " in -m "...footer").
570
+
571
+ Args:
572
+ command: Raw command string
573
+
574
+ Returns:
575
+ Command with footer lines removed
576
+ """
577
+ # Remove full lines that contain AI attribution patterns.
578
+ # Each pattern matches the newline + footer content, then uses a
579
+ # lookahead to stop before any trailing quote/paren/bracket
580
+ # sequence that closes the command structure. The captured group
581
+ # is replaced with empty string, leaving the closing chars intact.
582
+ footer_line_patterns = [
583
+ r'\n\s*Co-[Aa]uthored-[Bb]y:\s+(?:Claude|GitHub Copilot|aider|Windsurf|Cursor|Codex|Gemini)[^\n]*?(?=["\')\]]*(?:\n|$))',
584
+ r'\n\s*Generated with\s+\[?Claude Code\]?[^\n]*?(?=["\')\]]*(?:\n|$))',
585
+ r'\n\s*🤖\s*Generated with[^\n]*?(?=["\')\]]*(?:\n|$))',
586
+ ]
587
+ for pattern in footer_line_patterns:
588
+ command = re.sub(pattern, '', command, flags=re.IGNORECASE)
589
+
590
+ # Clean up trailing whitespace inside quotes/heredoc
591
+ # Collapse 3+ consecutive newlines to 2
592
+ command = re.sub(r'\n{3,}', '\n\n', command)
593
+
594
+ return command
595
+
596
+ def _validate_commit_message(self, command: str) -> BashValidationResult:
597
+ """
598
+ Validate git commit message using commit_validator.
599
+
600
+ Args:
601
+ command: Git commit command to validate
602
+
603
+ Returns:
604
+ BashValidationResult with validation status
605
+ """
606
+ # Extract commit message from command
607
+ # Handles both: git commit -m "message" and git commit -m "$(cat <<'EOF'...)"
608
+ message = self._extract_commit_message(command)
609
+
610
+ if not message:
611
+ # Could not extract message - let it pass, git will handle it
612
+ return BashValidationResult(
613
+ allowed=True,
614
+ tier=SecurityTier.T2_DRY_RUN,
615
+ reason="Could not extract commit message for validation"
616
+ )
617
+
618
+ # Import validator (lazy import to avoid startup cost)
619
+ try:
620
+ import sys
621
+ from pathlib import Path
622
+
623
+ # Import from sibling module (hooks/modules/validation)
624
+ from ..validation.commit_validator import validate_commit_message
625
+
626
+ # Validate message
627
+ validation = validate_commit_message(message)
628
+
629
+ if not validation.valid:
630
+ # Build suggestions from errors
631
+ suggestions = []
632
+ for error in validation.errors:
633
+ suggestions.append(f"{error['type']}: {error['fix']}")
634
+
635
+ return BashValidationResult(
636
+ allowed=False,
637
+ tier=SecurityTier.T3_BLOCKED,
638
+ reason=f"Commit message validation failed: {validation.errors[0]['message']}",
639
+ suggestions=suggestions[:3] # Limit to 3 suggestions
640
+ )
641
+
642
+ return BashValidationResult(
643
+ allowed=True,
644
+ tier=SecurityTier.T2_DRY_RUN,
645
+ reason="Commit message validated successfully"
646
+ )
647
+
648
+ except Exception as e:
649
+ logger.warning(f"Failed to validate commit message: {e}")
650
+ # If validation fails, allow the command (don't block on validator failure)
651
+ return BashValidationResult(
652
+ allowed=True,
653
+ tier=SecurityTier.T2_DRY_RUN,
654
+ reason=f"Commit validation skipped (validator error: {e})"
655
+ )
656
+
657
+ def _extract_commit_message(self, command: str) -> Optional[str]:
658
+ """
659
+ Extract commit message from git commit command.
660
+
661
+ Handles formats:
662
+ - git commit -m "message"
663
+ - git commit -m 'message'
664
+ - git commit -m "$(cat <<'EOF'\nmessage\nEOF\n)"
665
+ - git commit -m "$(cat <<EOF\nmessage\nEOF\n)"
666
+
667
+ Returns:
668
+ Extracted message or None if cannot extract
669
+ """
670
+ # Level 1: HEREDOC pattern (most common in Claude Code)
671
+ # Handles: <<'EOF', <<EOF, <<"EOF" with flexible whitespace
672
+ if "<<" in command:
673
+ heredoc_match = re.search(
674
+ r"<<['\"]?EOF['\"]?\s*\n(.*?)\n\s*EOF",
675
+ command, re.DOTALL
676
+ )
677
+ if heredoc_match:
678
+ return heredoc_match.group(1).strip()
679
+
680
+ # Level 2: Simple -m "message" or -m 'message' (non-heredoc)
681
+ match = re.search(r'-m\s+(["\'])(.*?)\1', command, re.DOTALL)
682
+ if match:
683
+ msg = match.group(2)
684
+ # Skip if it's a $(cat... wrapper — heredoc parse failed above
685
+ if msg.lstrip().startswith("$(cat"):
686
+ return None
687
+ return msg.strip()
688
+
689
+ return None
690
+
691
+ def validate_bash_command(
692
+ command: str,
693
+ is_subagent: bool = False,
694
+ session_id: str = "",
695
+ ) -> BashValidationResult:
696
+ """
697
+ Validate a Bash command (convenience function).
698
+
699
+ Args:
700
+ command: Command to validate
701
+ is_subagent: True when running in subagent context
702
+ session_id: Session ID for approval scoping
703
+
704
+ Returns:
705
+ BashValidationResult
706
+ """
707
+ validator = BashValidator()
708
+ return validator.validate(command, is_subagent=is_subagent, session_id=session_id)