capt-hook 12.76.1__tar.gz → 12.76.2__tar.gz

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 (230) hide show
  1. {capt_hook-12.76.1 → capt_hook-12.76.2}/PKG-INFO +1 -1
  2. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/.claude-plugin/plugin.json +1 -1
  3. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/bindings.py +62 -14
  4. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/_sessions.py +3 -3
  5. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/commands.py +4 -1
  6. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/deletions.py +17 -5
  7. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/sessions.py +121 -30
  8. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/performance/hooks/filesystem.py +11 -9
  9. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/cmd.py +1 -1
  10. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/command_schema.py +1 -1
  11. {capt_hook-12.76.1 → capt_hook-12.76.2}/pyproject.toml +1 -1
  12. {capt_hook-12.76.1 → capt_hook-12.76.2}/.gitignore +0 -0
  13. {capt_hook-12.76.1 → capt_hook-12.76.2}/LICENSE +0 -0
  14. {capt_hook-12.76.1 → capt_hook-12.76.2}/README.md +0 -0
  15. {capt_hook-12.76.1 → capt_hook-12.76.2}/capt_hook_client/__init__.py +0 -0
  16. {capt_hook-12.76.1 → capt_hook-12.76.2}/capt_hook_client/__main__.py +0 -0
  17. {capt_hook-12.76.1 → capt_hook-12.76.2}/capt_hook_client/client.py +0 -0
  18. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/.mcp.json +0 -0
  19. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/__init__.py +0 -0
  20. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/__init__.pyi +0 -0
  21. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/__main__.py +0 -0
  22. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/app.py +0 -0
  23. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/ast_grep.py +0 -0
  24. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/__init__.py +0 -0
  25. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/fixes/__init__.py +0 -0
  26. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/fixes/hooks/__init__.py +0 -0
  27. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/fixes/hooks/_lib.py +0 -0
  28. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/fixes/hooks/scratch_writes.py +0 -0
  29. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/fixes/hooks/teammate_permissions.py +0 -0
  30. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/__init__.py +0 -0
  31. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/__init__.py +0 -0
  32. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/_lib.py +0 -0
  33. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/comments.py +0 -0
  34. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/detours.py +0 -0
  35. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/docs.py +0 -0
  36. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/hook_files.py +0 -0
  37. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/models.py +0 -0
  38. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/plain_english.py +0 -0
  39. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/plans.py +0 -0
  40. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts/fragments/deliverable_rubric.md +0 -0
  41. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts/fragments/workflow_script_header.md +0 -0
  42. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts/models/browser_delegation_nudge.md +0 -0
  43. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts/models/implementation_spawn_nudge.md +0 -0
  44. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts/models/inline_edit_nudge.md +0 -0
  45. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts/models/prose_spawn_gate.md +0 -0
  46. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts/models/prose_workflow_nudge.md +0 -0
  47. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts/models/review_routing_spawn_nudge.md +0 -0
  48. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts/models/review_routing_workflow_nudge.md +0 -0
  49. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts/models/writing_docs_spawn_nudge.md +0 -0
  50. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts/models/writing_docs_workflow_nudge.md +0 -0
  51. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts/plain_english_rules.md +0 -0
  52. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts/tools/preload_nudge.md +0 -0
  53. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/prompts.py +0 -0
  54. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/questions.py +0 -0
  55. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/review.py +0 -0
  56. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/tasks.py +0 -0
  57. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/tombstones.py +0 -0
  58. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/tooling.py +0 -0
  59. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/hooks/tools.py +0 -0
  60. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/general/pack.toml +0 -0
  61. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/go/__init__.py +0 -0
  62. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/go/hooks/__init__.py +0 -0
  63. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/go/hooks/testing.py +0 -0
  64. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/go/hooks/toolchain.py +0 -0
  65. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/graphite/__init__.py +0 -0
  66. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/graphite/gitdir/.graphite_repo_config +0 -0
  67. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/graphite/hooks/__init__.py +0 -0
  68. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/graphite/hooks/_lib.py +0 -0
  69. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/graphite/hooks/queue.py +0 -0
  70. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/graphite/hooks/vcs.py +0 -0
  71. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/performance/__init__.py +0 -0
  72. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/performance/hooks/__init__.py +0 -0
  73. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/performance/hooks/pipelining.py +0 -0
  74. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/python/__init__.py +0 -0
  75. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/python/hooks/__init__.py +0 -0
  76. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/python/hooks/style.py +0 -0
  77. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/python/hooks/testing.py +0 -0
  78. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/python/hooks/toolchain.py +0 -0
  79. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/steering/__init__.py +0 -0
  80. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/steering/hooks/__init__.py +0 -0
  81. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/steering/hooks/steering.py +0 -0
  82. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/steering/hooks/teammates.py +0 -0
  83. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/steering/hooks/workarounds.py +0 -0
  84. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/builtin_packs/steering/pack.toml +0 -0
  85. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/classifiers/__init__.py +0 -0
  86. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/classifiers/conductor.py +0 -0
  87. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/classifiers/droid.py +0 -0
  88. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/classifiers/lane.py +0 -0
  89. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/classifiers/native.py +0 -0
  90. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/cli.py +0 -0
  91. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/command_schemas.py +0 -0
  92. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/conditions.py +0 -0
  93. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/context.py +0 -0
  94. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/contexts.py +0 -0
  95. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/daemon/__init__.py +0 -0
  96. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/daemon/context.py +0 -0
  97. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/daemon/decision_writer.py +0 -0
  98. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/daemon/logsink.py +0 -0
  99. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/daemon/registry.py +0 -0
  100. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/daemon/tree.py +0 -0
  101. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/decisions.py +0 -0
  102. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/desktop/__init__.py +0 -0
  103. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/desktop/cli.py +0 -0
  104. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/desktop/client.py +0 -0
  105. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/dispatch.py +0 -0
  106. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/doc_conventions.py +0 -0
  107. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/durable.py +0 -0
  108. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/events.py +0 -0
  109. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/faults.py +0 -0
  110. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/fields.py +0 -0
  111. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/file.py +0 -0
  112. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/guard_literal.py +0 -0
  113. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/guard_literal.sh +0 -0
  114. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/heartbeat.py +0 -0
  115. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/hook_lint.py +0 -0
  116. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/hooks/hooks.json +0 -0
  117. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/loader.py +0 -0
  118. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/log.py +0 -0
  119. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/mcp_server.py +0 -0
  120. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/packs/__init__.py +0 -0
  121. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/packs/manager.py +0 -0
  122. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/packs/plugins.py +0 -0
  123. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/primitives/__init__.py +0 -0
  124. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/primitives/commands.py +0 -0
  125. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/primitives/lint.py +0 -0
  126. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/primitives/llm.py +0 -0
  127. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/primitives/nudge.py +0 -0
  128. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/primitives/permissions.py +0 -0
  129. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/primitives/provision.py +0 -0
  130. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/primitives/rewrite.py +0 -0
  131. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/primitives/workflow.py +0 -0
  132. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/prompt.py +0 -0
  133. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/py.typed +0 -0
  134. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/review/__init__.py +0 -0
  135. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/review/announce.py +0 -0
  136. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/review/cli.py +0 -0
  137. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/review/dashboard.py +0 -0
  138. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/review/fix.py +0 -0
  139. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/review/formats.py +0 -0
  140. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/review/judge.py +0 -0
  141. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/review/notify.py +0 -0
  142. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/review/pipeline.py +0 -0
  143. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/review/prompts.py +0 -0
  144. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/review/repo.py +0 -0
  145. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/review/routing.py +0 -0
  146. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/review/scan.py +0 -0
  147. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/review/settings.py +0 -0
  148. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/review/snapshot.py +0 -0
  149. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/review/status.py +0 -0
  150. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/review/store.py +0 -0
  151. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/review/sync.py +0 -0
  152. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/review/triage.py +0 -0
  153. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/scripts/install-binary.sh +0 -0
  154. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/scripts/install-mcp.sh +0 -0
  155. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/session.py +0 -0
  156. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/settings.py +0 -0
  157. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/signals/__init__.py +0 -0
  158. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/signals/nlp.py +0 -0
  159. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/skills/authoring-hooks/SKILL.md +0 -0
  160. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/skills/authoring-hooks/references/capt-hook-api.md +0 -0
  161. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/skills/authoring-hooks/references/pattern-catalog.md +0 -0
  162. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/skills/authoring-hooks/references/pitfalls.md +0 -0
  163. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/skills/authoring-hooks/references/testing-hooks.md +0 -0
  164. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/skills/bootstrapping-hooks/SKILL.md +0 -0
  165. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/skills/scanning-sessions/SKILL.md +0 -0
  166. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/skills/scanning-sessions/references/pr-workflow.md +0 -0
  167. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/skills/scanning-sessions/references/review-cli.md +0 -0
  168. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/skills/translating-styleguides/SKILL.md +0 -0
  169. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/skills/translating-styleguides/references/llm-rule-patterns.md +0 -0
  170. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/skills/translating-styleguides/references/matcher-reference.md +0 -0
  171. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/skills/translating-styleguides/references/tier-rubric.md +0 -0
  172. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/snapshots/__init__.py +0 -0
  173. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/snapshots/client.py +0 -0
  174. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/snapshots/contracts.py +0 -0
  175. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/snapshots/review.py +0 -0
  176. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/snapshots/schema.py +0 -0
  177. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/snapshots/schemas/config.schema.json +0 -0
  178. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/snapshots/schemas/context.schema.json +0 -0
  179. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/snapshots/schemas/host-request.schema.json +0 -0
  180. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/snapshots/schemas/host-response.schema.json +0 -0
  181. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/snapshots/schemas/request.schema.json +0 -0
  182. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/snapshots/schemas/response.schema.json +0 -0
  183. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/snapshots/schemas/tool_registry.schema.json +0 -0
  184. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/snapshots/validation.py +0 -0
  185. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/snapshots/worker.py +0 -0
  186. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/state.py +0 -0
  187. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/style/__init__.py +0 -0
  188. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/style/ast_grep.py +0 -0
  189. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/style/matchers.py +0 -0
  190. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/style/scope.py +0 -0
  191. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/style/types.py +0 -0
  192. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/tasks.py +0 -0
  193. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/templates/example_hook.py.tmpl +0 -0
  194. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/testing/__init__.py +0 -0
  195. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/testing/fixtures.py +0 -0
  196. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/testing/helpers.py +0 -0
  197. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/testing/session_cache.py +0 -0
  198. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/testing/snapshots.py +0 -0
  199. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/testing/types.py +0 -0
  200. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/transcripts.py +0 -0
  201. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/turn.py +0 -0
  202. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/types.py +0 -0
  203. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/update/__init__.py +0 -0
  204. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/update/cli.py +0 -0
  205. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/update/settings.py +0 -0
  206. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/update/updater.py +0 -0
  207. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/util/__init__.py +0 -0
  208. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/util/automode.py +0 -0
  209. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/util/caching.py +0 -0
  210. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/util/fs.py +0 -0
  211. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/util/globbing.py +0 -0
  212. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/util/http.py +0 -0
  213. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/util/model_cache.py +0 -0
  214. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/util/paths.py +0 -0
  215. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/util/payload.py +0 -0
  216. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/util/proc.py +0 -0
  217. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/util/reqenv.py +0 -0
  218. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/util/scratch.py +0 -0
  219. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/util/shell.py +0 -0
  220. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/util/text.py +0 -0
  221. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/util/userpath.py +0 -0
  222. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/util/vcs.py +0 -0
  223. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/worker/__init__.py +0 -0
  224. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/worker/__main__.py +0 -0
  225. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/worker/fail_open.py +0 -0
  226. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/worker/protocol.py +0 -0
  227. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/worker/runtime.py +0 -0
  228. {capt_hook-12.76.1 → capt_hook-12.76.2}/captain_hook/worker/service.py +0 -0
  229. {capt_hook-12.76.1 → capt_hook-12.76.2}/hatch_build.py +0 -0
  230. {capt_hook-12.76.1 → capt_hook-12.76.2}/internal/wireproto/guard.json +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: capt-hook
3
- Version: 12.76.1
3
+ Version: 12.76.2
4
4
  Summary: Stop repeating yourself to Claude.
5
5
  Project-URL: Homepage, https://github.com/yasyf/captain-hook
6
6
  Project-URL: Documentation, https://yasyf.github.io/captain-hook/
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "captain-hook",
3
- "version": "12.76.0",
3
+ "version": "12.76.1",
4
4
  "displayName": "Captain Hook",
5
5
  "description": "Skills for the capt-hook declarative hook framework: bootstrap gates and nudges from a repo's conventions, and translate style guides into enforced StyleRules.",
6
6
  "author": {
@@ -1,9 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import glob
3
4
  import re
4
5
  from dataclasses import dataclass, field
5
6
  from itertools import product
6
7
  from math import prod
8
+ from pathlib import PurePath
7
9
  from typing import TYPE_CHECKING
8
10
 
9
11
  if TYPE_CHECKING:
@@ -49,6 +51,7 @@ PREFIXES = frozenset({"!", "time", "builtin", "command"})
49
51
  DECLARERS = frozenset({"local", "declare", "typeset", "readonly", "let"})
50
52
  READERS = frozenset({"read", "mapfile", "readarray", "getopts"})
51
53
  UNREADABLE_BINDERS = frozenset({"eval", "source", ".", "case", "select", "coproc"})
54
+ MUTATORS = READERS | DECLARERS | UNREADABLE_BINDERS | frozenset({"unset", "export", "printf"})
52
55
  UNREADABLE_IN_SUBSTITUTION = ("#", "<<", "case")
53
56
  SHELL_OWNED = frozenset({"PWD", "OLDPWD", "RANDOM", "SECONDS", "LINENO", "REPLY", "IFS", "BASH_COMMAND", "PIPESTATUS"})
54
57
  ENVIRONMENT = {"HOME": "~"}
@@ -338,7 +341,7 @@ class Scope:
338
341
  def __post_init__(self) -> None:
339
342
  self.mentioned = frozenset(match.group(1) for match in ASSIGNMENT.finditer(self.text))
340
343
  self.mutated = frozenset(name for match in MUTATION.finditer(self.text) for name in match.groups() if name)
341
- if "IFS" in self.mentioned:
344
+ if "IFS" in self.mentioned or "IFS" in self.mutated:
342
345
  self.unreadable_from = 0
343
346
  Walk(self).run()
344
347
 
@@ -412,7 +415,7 @@ class Scope:
412
415
  )
413
416
  if any(len(candidate) > CANDIDATE_LENGTH for candidate in candidates):
414
417
  return Unresolved(None)
415
- splittable = any(not ref.quoted for ref in refs.values())
418
+ splittable = any(not part.quoted for part in parts if isinstance(part, Ref))
416
419
  mixed = splittable and any(part.quoted for part in parts)
417
420
  if mixed and any(candidate.split() != [candidate] for candidate in candidates):
418
421
  return Unresolved(None)
@@ -425,6 +428,18 @@ def head_of(words: list[Token]) -> tuple[str, list[Token]]:
425
428
  return (dequote(words[0].raw), words[1:]) if words else ("", [])
426
429
 
427
430
 
431
+ def literal_name(word: Token) -> str | None:
432
+ parts = segments(word.raw)
433
+ if parts is None or not all(isinstance(part, Literal) for part in parts):
434
+ return None
435
+ match = NAME.search("".join(part.text for part in parts))
436
+ return None if match is None else match.group()
437
+
438
+
439
+ def program_name(candidate: str) -> str:
440
+ return PurePath(pieces[0] if (pieces := candidate.split()) else candidate).name
441
+
442
+
428
443
  @dataclass
429
444
  class Walk:
430
445
  scope: Scope
@@ -483,15 +498,42 @@ class Walk:
483
498
  self.scope.functions.append((start, token.end))
484
499
  self.previous = token.raw
485
500
 
501
+ def bind(self, event: Event) -> None:
502
+ if event.name == "IFS":
503
+ self.scope.mark_unreadable(event.offset)
504
+ self.scope.events.append(event)
505
+
486
506
  def bind_unreadable(self, word: Token) -> None:
487
507
  if (match := ASSIGNMENT.match(word.raw)) is not None:
488
- self.scope.events.append(Event(word.start, match.group(1), Unknown(None)))
508
+ self.bind(Event(word.start, match.group(1), Unknown(None)))
509
+
510
+ def computed_mutator(self, word: Token) -> bool:
511
+ parts = segments(word.raw)
512
+ if parts is not None and all(isinstance(part, Literal) for part in parts):
513
+ return False
514
+ match self.scope.resolve(word.raw, word.start):
515
+ case Resolved(candidates, _):
516
+ return any(program_name(candidate) in MUTATORS for candidate in candidates)
517
+ case Unresolved():
518
+ return True
489
519
 
490
520
  def flush(self, *, sure: bool) -> None:
491
521
  words, self.words = self.words, []
492
522
  if not words:
493
523
  return
494
524
  head, rest = head_of(words)
525
+ head_word = next(
526
+ (
527
+ word
528
+ for word in words
529
+ if dequote(word.raw) not in PREFIXES
530
+ and ASSIGNMENT.match(word.raw) is None
531
+ and not word.raw.startswith("-")
532
+ ),
533
+ None,
534
+ )
535
+ if head_word is not None and self.computed_mutator(head_word):
536
+ self.scope.mark_unreadable(head_word.start)
495
537
  if head in BODY_OPENERS:
496
538
  if head == "else" and self.stack and self.stack[-1][0] == "then":
497
539
  self.stack.pop()
@@ -530,14 +572,18 @@ class Walk:
530
572
  self.bind_assignment(word, sure=sure)
531
573
 
532
574
  def bind_names(self, head: str, words: list[Token]) -> None:
533
- names = [
534
- match.group()
535
- for word in words
536
- if not word.raw.startswith("-") and (match := NAME.search(dequote(word.raw))) is not None
537
- ]
575
+ names: list[str] = []
576
+ for word in words:
577
+ if word.raw.startswith("-"):
578
+ continue
579
+ if (name := literal_name(word)) is None:
580
+ self.scope.mark_unreadable(word.start)
581
+ return
582
+ names.append(name)
538
583
  if head == "read" and not names:
539
584
  names = ["REPLY"]
540
- self.scope.events.extend(Event(words[0].start if words else 0, name, Unknown(None)) for name in names)
585
+ for name in names:
586
+ self.bind(Event(words[0].start if words else 0, name, Unknown(None)))
541
587
 
542
588
  def bind_exports(self, words: list[Token], sure: bool) -> None:
543
589
  options = any(word.raw.startswith("-") for word in words)
@@ -567,7 +613,7 @@ class Walk:
567
613
  binding = Unknown(" ".join(candidates))
568
614
  case Unresolved(source):
569
615
  binding = Unknown(source)
570
- self.scope.events.append(Event(word.start, name, binding))
616
+ self.bind(Event(word.start, name, binding))
571
617
 
572
618
  def bind_loop(self, words: list[Token]) -> None:
573
619
  match [word.raw for word in words]:
@@ -575,7 +621,9 @@ class Walk:
575
621
  candidates: list[str] = []
576
622
  for item in words[2:]:
577
623
  match self.scope.resolve(item.raw, item.start):
578
- case Resolved(values, splittable) if not any("{" in value for value in values):
624
+ case Resolved(values, splittable) if not any(
625
+ "{" in value or glob.has_magic(value) for value in values
626
+ ):
579
627
  candidates.extend(
580
628
  piece for value in values for piece in (value.split() if splittable else [value])
581
629
  )
@@ -588,16 +636,16 @@ class Walk:
588
636
  else Unknown(" ".join(word.raw for word in words[2:]) or None)
589
637
  )
590
638
  self.pending_loop = len(self.scope.events)
591
- self.scope.events.append(Event(words[0].start, name, binding))
639
+ self.bind(Event(words[0].start, name, binding))
592
640
  case [name, *_] if NAME.fullmatch(name) is not None:
593
- self.scope.events.append(Event(words[0].start, name, Unknown(None)))
641
+ self.bind(Event(words[0].start, name, Unknown(None)))
594
642
  case _:
595
643
  pass
596
644
 
597
645
  def close_loop(self, index: int, end: int) -> None:
598
646
  event = self.scope.events[index]
599
647
  self.scope.events[index] = Event(event.offset, event.name, event.binding, end)
600
- self.scope.events.append(Event(end, event.name, Unknown(source_of(event.binding))))
648
+ self.bind(Event(end, event.name, Unknown(source_of(event.binding))))
601
649
 
602
650
 
603
651
  def printf_destination(words: list[Token]) -> list[Token]:
@@ -341,10 +341,10 @@ def head_reason(call: Call) -> str | None:
341
341
  f"BLOCKED: `{spelling}` runs a command named at run time (`{clip(head.raw, 40)}`), so the guard "
342
342
  "cannot tell what runs. Spell the command name literally."
343
343
  )
344
- case Unresolved(source) if (named := names_guarded_word(source)) is not None:
344
+ case Unresolved(source) if source:
345
345
  return (
346
- f"BLOCKED: `{spelling}` runs a command named at run time (`{clip(head.raw, 40)}`) from text "
347
- f"that names `{named}` (`{clip(source, 40)}`). Spell the command name literally."
346
+ f"BLOCKED: `{spelling}` runs a command named at run time (`{clip(head.raw, 40)}`) built from text "
347
+ f"the guard cannot read (`{clip(source, 40)}`). Spell the command name literally."
348
348
  )
349
349
  case _:
350
350
  return None
@@ -438,7 +438,10 @@ hook(
438
438
  Input(command="cd / && find . -name '*.pyc' -exec rm {} +"): Block(),
439
439
  Input(command="find / -newermt 2020-01-01 -exec echo {} +"): Block(),
440
440
  Input(command="find . -name '*.pyc' -exec rm {} +", cwd="/Users/alice"): Block(),
441
- Input(command=r"cd /tmp/reco-box-size && find . -name '*gil*.json' -exec cat {} \;"): Allow(),
441
+ Input(command='D=$(printf /); cd "$D" && find . -exec echo {} +'): Block(),
442
+ Input(command="find . -name '*.pyc' -exec rm {} +"): Block(),
443
+ Input(command=r"find . -name '*gil*.json' -exec cat {} \;", cwd="/tmp/reco-box-size"): Allow(),
444
+ Input(command=r"cd /tmp && find . -name '*gil*.json' -exec cat {} \;"): Allow(),
442
445
  Input(command=r"find . -name '*.pyc' -exec rm {} \;", cwd="/repo"): Allow(),
443
446
  Input(command="R=~/.claude/worktrees/monorepo/x; find $R -name targets.yaml -exec cat {} +"): Allow(),
444
447
  Input(command="find . -name '*.pyc'"): Allow(),
@@ -22,6 +22,8 @@ if TYPE_CHECKING:
22
22
  from captain_hook.cmd import Call
23
23
  from captain_hook.events import PreToolUseEvent
24
24
 
25
+ SCRATCH_GLOB_LIMIT = 2000
26
+
25
27
 
26
28
  @dataclass(frozen=True)
27
29
  class Recoverable:
@@ -99,11 +101,18 @@ def check_target(
99
101
  f"The glob '{token}' is too broad to verify before deleting. "
100
102
  "Narrow the pattern or run `rm -r <dir>` on a specific directory."
101
103
  )
102
- if len(expansion) > GLOB_LIMIT and not scratch_glob(target):
103
- return evt.block(
104
- f"The glob '{token}' matches more than {GLOB_LIMIT} files. "
105
- f"Run `ls {token}`, then narrow the pattern or run `rm -r <dir>` on a named directory."
106
- )
104
+ if len(expansion) > GLOB_LIMIT:
105
+ if not scratch_glob(target):
106
+ return evt.block(
107
+ f"The glob '{token}' matches more than {GLOB_LIMIT} files. "
108
+ f"Run `ls {token}`, then narrow the pattern or run `rm -r <dir>` on a named directory."
109
+ )
110
+ expansion = target.expand(limit=SCRATCH_GLOB_LIMIT)
111
+ if expansion.exhausted or len(expansion) > SCRATCH_GLOB_LIMIT:
112
+ return evt.block(
113
+ f"The glob '{token}' matches more than {SCRATCH_GLOB_LIMIT} files, too many to check before "
114
+ "deleting. Run `rm -r <dir>` on a named scratch directory instead."
115
+ )
107
116
  recovery: Recoverable | None = None
108
117
  for match in expansion:
109
118
  result = check_resolved(evt, Target(match, match, cwd), rewritable=rewritable)
@@ -164,6 +173,9 @@ ROOT_RM: Block = Block(pattern="filesystem root") if trash_binary() else Block(p
164
173
  Input(command="rm -rf $d/*", cwd="/"): Block(pattern="built at run time"),
165
174
  Input(command='X="/tmp/x /"; rm -rf $X', cwd="/"): ROOT_RM,
166
175
  Input(command='X="/tmp/x /outside"; rm -rf $X', cwd="/"): RECOVERABLE_RM,
176
+ Input(command='X="/tmp/a /Users/yasyf /tmp/b"; rm -rf $X"$X"', cwd="/"): Block(pattern="repository"),
177
+ Input(command='read IFS <<<:; X="/tmp/x:/Users/yasyf"; rm -rf $X', cwd="/"): Block(pattern="repository"),
178
+ Input(command='IFS=:; X="/tmp/x:/Users/yasyf"; rm -rf $X', cwd="/"): Block(pattern="repository"),
167
179
  Input(command="rm -f /tmp/../etc/*", cwd="/"): Block(),
168
180
  Input(command="rm /outside/{a,b}", cwd="/"): Block(pattern="repository"),
169
181
  Input(command="rm foo\\\nbar", cwd="/"): Block(pattern="repository"),
@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING
8
8
  from cc_transcript.command import PAYLOAD_DEPTH_LIMIT
9
9
 
10
10
  from captain_hook import Allow, Block
11
- from captain_hook.bindings import Ref, segments
11
+ from captain_hook.bindings import Resolved
12
12
  from captain_hook.builtin_packs.general.hooks._sessions import (
13
13
  ARG_TOKEN_BREAK,
14
14
  GUARDED_PROGRAMS,
@@ -34,18 +34,49 @@ from captain_hook.builtin_packs.general.hooks._sessions import (
34
34
  spell,
35
35
  unresolvable,
36
36
  )
37
- from captain_hook.command_schemas import KILL, LAUNCHCTL, ORCA, PMSET, RENICE, SIGNAL_FLAGS, SOFTWAREUPDATE, TMUX
37
+ from captain_hook.command_schemas import KILL, LAUNCHCTL, ORCA, PMSET, RENICE, SOFTWAREUPDATE, TMUX
38
38
  from captain_hook.guard_literal import QUOTING_CHARS
39
39
  from captain_hook.util.shell import SHELLS
40
40
 
41
41
  if TYPE_CHECKING:
42
+ from cc_transcript.command import Word
43
+
42
44
  from captain_hook import HookResult, ToolRewriteEvent
43
45
  from captain_hook.builtin_packs.general.hooks._sessions import Facts, Unparsed
44
46
  from captain_hook.cmd import Call
45
47
  from captain_hook.command_schema import Arguments, Scalar
46
48
 
47
49
  CRITERIA_PROGRAMS = frozenset({"pkill", "killall", "killall5", "skill", "snice", "kill-port"})
48
- SIGNAL_FLAGS_SET = frozenset(SIGNAL_FLAGS)
50
+ PKILL_CRITERIA_SHORT = frozenset("fxnoiluUgGtPacvF")
51
+ PKILL_CRITERIA_LONG = frozenset(
52
+ {
53
+ "--full",
54
+ "--exact",
55
+ "--newest",
56
+ "--oldest",
57
+ "--ignore-case",
58
+ "--inverse",
59
+ "--count",
60
+ "--list-name",
61
+ "--list-full",
62
+ "--euid",
63
+ "--uid",
64
+ "--group",
65
+ "--pgroup",
66
+ "--session",
67
+ "--terminal",
68
+ "--parent",
69
+ "--ns",
70
+ "--nslist",
71
+ "--pidfile",
72
+ "--logpidfile",
73
+ "--cgroup",
74
+ "--runstates",
75
+ "--ignore-ancestors",
76
+ "--require-handler",
77
+ }
78
+ )
79
+ ORCA_VM_RUN_FLAGS = frozenset({"--provision", "--connect"})
49
80
  PACKAGE_RUNNERS = frozenset({"npx", "bunx", "pnpx", "pnpm", "yarn"})
50
81
  CRITERIA_FIX = (
51
82
  "Find the pid with `pgrep -fl <pattern>`, verify it with `ps -o pid,ppid,pgid,lstart,command -p <pid>`, and "
@@ -138,10 +169,25 @@ STDIN_SCRIPTS = frozenset({"-", "/dev/stdin", "/dev/fd/0"})
138
169
  @guard(
139
170
  tests={
140
171
  guarded(command="read p; $p sleep; ls ~/.orca"): Block(pattern="named at run time"),
141
- guarded(command="p=$(echo pkill); $p sleep"): Block(pattern="from text that names `pkill`"),
172
+ guarded(command="p=$(echo pkill); $p sleep"): Block(pattern="built from text the guard cannot read"),
142
173
  guarded(command='P=$(ls | head -1); [ -z "$P" ] && P=$(find ~ -name python); "$P" -c x'): Block(
143
- pattern="from text that names"
174
+ pattern="text the guard cannot read"
175
+ ),
176
+ guarded(command="C=$(printf '%s%s' pk ill); \"$C\" claude; orca status"): Block(
177
+ pattern="text the guard cannot read"
178
+ ),
179
+ guarded(command='X=echo; V=X; read "$V" <<<pkill; "$X" claude; ls ~/.orca'): Block(pattern="named at run time"),
180
+ guarded(command='X=echo; V=X; printf -v "$V" pkill; "$X" claude; ls ~/.orca'): Block(
181
+ pattern="named at run time"
182
+ ),
183
+ guarded(command='X=echo; R=read; $R X <<<pkill; "$X" claude; ls ~/.orca'): Block(pattern="named at run time"),
184
+ guarded(command='X=echo; R=$(echo read); $R X <<<pkill; "$X" claude; ls ~/.orca'): Block(
185
+ pattern="named at run time"
144
186
  ),
187
+ guarded(command='for P in /usr/bin/[p]kill; do "$P" -f claude; done'): Block(
188
+ pattern="text the guard cannot read"
189
+ ),
190
+ guarded(command='P=echo; sh -c "P=pkill; \\$P claude"'): Block(pattern="named at run time"),
145
191
  guarded(command="$W kill 14575"): Block(pattern="`kill` among its arguments"),
146
192
  guarded(command="W=timeout; $W 5 kill 14575"): Block(pattern="among its arguments"),
147
193
  guarded(command="$SHELL -c 'kill 14575'"): Block(pattern="among its arguments"),
@@ -149,11 +195,11 @@ STDIN_SCRIPTS = frozenset({"-", "/dev/stdin", "/dev/fd/0"})
149
195
  guarded(command='$W "$1"; ls ~/.orca'): Block(pattern='`"\\$1"` among its arguments'),
150
196
  guarded(command="W='timeout 5'; $W pkill sleep"): Block(pattern="`pkill` among its arguments"),
151
197
  guarded(command="f() { $CMD 14575; }; CMD=kill; f"): Block(pattern="named at run time"),
152
- guarded(command="p=ls; (p=kill); $p 14575"): Block(pattern="from text that names"),
153
- guarded(command="false && p=kill; $p 14575"): Block(pattern="from text that names"),
198
+ guarded(command="p=ls; (p=kill); $p 14575"): Block(pattern="text the guard cannot read"),
199
+ guarded(command="false && p=kill; $p 14575"): Block(pattern="text the guard cannot read"),
154
200
  guarded(command="eval 'p=ls'; $p 14575; ls ~/.orca"): Block(pattern="named at run time"),
155
201
  guarded(command='[ -z "$P" ] && kill -l'): Allow(),
156
- guarded(command="false && p=ls; $p 14575; ls ~/.orca"): Allow(),
202
+ guarded(command="false && p=ls; $p 14575; ls ~/.orca"): Block(pattern="text the guard cannot read"),
157
203
  guarded(command="cat <<'EOF'\np=ls\nEOF\n$p kill 14575"): Block(),
158
204
  guarded(command="{kill,14575}"): Block(pattern="expands at run time"),
159
205
  guarded(command="{pkill,-x,sleep}"): Block(),
@@ -185,6 +231,13 @@ STDIN_SCRIPTS = frozenset({"-", "/dev/stdin", "/dev/fd/0"})
185
231
  'skills/codex/1.14.0/skills/codex/../../bin/codex-ask"; "$CA" --lane review --schema findings - '
186
232
  "<<'Q' 2>&1 | tail -40\nReview the builtin packs.\nQ"
187
233
  )
234
+ ): Block(pattern="text the guard cannot read"),
235
+ guarded(
236
+ command=(
237
+ 'CA="/Users/yasyf/.claude/plugins/cache/skills/codex/1.14.0/skills/codex/../../bin/codex-ask"; '
238
+ 'cd ~/.claude/worktrees/captain-hook/hook-lint-builtins && "$CA" --lane review --schema findings - '
239
+ "<<'Q' 2>&1 | tail -40\nReview the builtin packs.\nQ"
240
+ )
188
241
  ): Allow(),
189
242
  guarded(
190
243
  command=(
@@ -203,21 +256,21 @@ STDIN_SCRIPTS = frozenset({"-", "/dev/stdin", "/dev/fd/0"})
203
256
  "cd /Users/yasyf/.orca/workspaces/monorepo/monorepo/v3-incident-api-1n80-fix-base && "
204
257
  "B=$(realpath ../v3-incident-api-1n7z-fix-base/node_modules/.bin/biome); $B --version"
205
258
  )
206
- ): Allow(),
259
+ ): Block(pattern="text the guard cannot read"),
207
260
  guarded(command='timeout 20 "$CP" outcomes --no-doc --session d0bf 2>&1; ls ~/.orca'): Allow(),
208
261
  guarded(
209
262
  command=(
210
263
  "B=$(ls /Users/yasyf/.orca/workspaces/monorepo/monorepo/sole/node_modules/.bin/biome 2>/dev/null || "
211
264
  "command -v biome) && $B check infra/ci/src/verbs/release-stacks.ts 2>&1 | tail -25"
212
265
  )
213
- ): Allow(),
266
+ ): Block(pattern="text the guard cannot read"),
214
267
  guarded(
215
268
  command=(
216
269
  'for c in "bun infra/graph.ts" "bun infra/k8s.ts delivery"; do eval $c || echo "FAILED $c"; done; '
217
270
  "ls ~/.orca"
218
271
  )
219
272
  ): Allow(),
220
- guarded(command="X=echo; X[0]=pkill; $X claude"): Block(pattern="from text that names `pkill`"),
273
+ guarded(command="X=echo; X[0]=pkill; $X claude"): Block(pattern="text the guard cannot read"),
221
274
  guarded(command="X=echo; printf -v X pkill; $X claude"): Block(pattern="named at run time"),
222
275
  guarded(command="X=echo; read 'X' <<<pkill; $X claude"): Block(pattern="named at run time"),
223
276
  guarded(command="X=echo; declare -n Y=X; Y=pkill; $X claude"): Block(pattern="named at run time"),
@@ -228,7 +281,7 @@ STDIN_SCRIPTS = frozenset({"-", "/dev/stdin", "/dev/fd/0"})
228
281
  guarded(command="X='{p,q}kill'; $X -x claude"): Block(pattern="expands at run time"),
229
282
  guarded(command='X=echo; function f() { "$X" claude; }; X=pkill; f'): Block(pattern="named at run time"),
230
283
  guarded(command='X=echo; f() { echo }; "$X" claude; }; X=pkill; f'): Block(pattern="named at run time"),
231
- guarded(command='for X in {echo,pkill}; do "$X" claude; done'): Block(pattern="from text that names"),
284
+ guarded(command='for X in {echo,pkill}; do "$X" claude; done'): Block(pattern="text the guard cannot read"),
232
285
  guarded(command="PWD=/tmp; cd /; $PWD/kill 14575"): Block(pattern="named at run time"),
233
286
  guarded(command='cd ~/.orca/workspaces/x && for g in a.ts b.ts; do eval "timeout 120 bun $g"; done'): Allow(),
234
287
  guarded(command="~/bin/kill 14575"): Allow(),
@@ -261,10 +314,12 @@ def unread_reason(call: Call, arguments: Arguments) -> str:
261
314
  return "it names an option or signal at run time"
262
315
 
263
316
 
264
- def one_variable(raw: str) -> bool:
265
- match segments(raw):
266
- case [Ref()]:
267
- return True
317
+ def probe_pid(call: Call, word: Word, value: Scalar | None) -> bool:
318
+ if literal_pid(word, value) is not None:
319
+ return True
320
+ match call.resolve(word):
321
+ case Resolved(candidates, _):
322
+ return all(candidate.isdecimal() for candidate in candidates)
268
323
  case _:
269
324
  return False
270
325
 
@@ -286,10 +341,7 @@ def kill_verdict(call: Call, facts: Facts) -> str | None:
286
341
  return None
287
342
  targets = tuple(zip(arguments.words.get("targets", ()), values.get("targets", ()), strict=True))
288
343
  probing = ("probe" in values and "signal" not in values) or values.get("signal") == ("0",)
289
- if probing and (
290
- all(literal_pid(word, value) is not None for word, value in targets)
291
- or (len(targets) == 1 and (double_quoted(targets[0][0].raw) or one_variable(targets[0][0].raw)))
292
- ):
344
+ if probing and all(probe_pid(call, word, value) for word, value in targets):
293
345
  return None
294
346
  if not targets:
295
347
  return deny("a wrapper supplies its targets") if call.wrappers else None
@@ -299,7 +351,7 @@ def kill_verdict(call: Call, facts: Facts) -> str | None:
299
351
  if probing:
300
352
  return (
301
353
  f"BLOCKED: `{spelling}` cannot be verified: {describe_target(word, value)}, so the probe could expand "
302
- f'into a real signal. Pass one double-quoted word (`"{clip(word.raw, 40)}"`) or the literal pid.'
354
+ "into a real signal. Pass the literal pid, or a variable the same line sets to one."
303
355
  )
304
356
  return deny(describe_target(word, value))
305
357
 
@@ -350,7 +402,17 @@ def kill_verdict(call: Call, facts: Facts) -> str | None:
350
402
  guarded(command="kill -STOP 14575"): Block(),
351
403
  guarded(command="kill -s KILL 14575"): Block(),
352
404
  guarded(command="kill -0 -9 14575"): Block(),
405
+ guarded(command="kill -0 14575 -9"): Block(),
353
406
  guarded(command="PID='-s 9 14575'; kill -0 $PID"): Block(pattern="an agent session"),
407
+ guarded(command='bash -c \'PID=$(printf "%s" "-s KILL 14575"); kill -0 $PID\''): Block(
408
+ pattern="probe could expand into a real signal"
409
+ ),
410
+ guarded(command='kill -0 "$worker" 2>/dev/null || break'): Block(pattern="probe could expand"),
411
+ guarded(command="kill -0 ${PID}"): Block(pattern="probe could expand"),
412
+ guarded(command="PID=$(awk '{print $2}' /tmp/apply.pid); kill -0 $PID 2>/dev/null && echo alive"): Block(
413
+ pattern="probe could expand"
414
+ ),
415
+ guarded(command="PID=14575x; kill -0 $PID"): Block(pattern="probe could expand"),
354
416
  guarded(command="X=-9; kill $X $(printf 14575)"): Block(pattern="command substitution"),
355
417
  guarded(command="kill -0 $pid 14575"): Block(pattern="probe could expand into a real signal"),
356
418
  guarded(command="kill -0 $pid$x"): Block(pattern="probe could expand into a real signal"),
@@ -369,15 +431,9 @@ def kill_verdict(call: Call, facts: Facts) -> str | None:
369
431
  guarded(tool="mcp__runner__exec", tool_input={"argv": ["kill", 14575]}): Block(pattern="an agent session"),
370
432
  guarded(tool="mcp__x__exec", tool_input={"command": "kill 14575 \udc80"}): Block(),
371
433
  guarded(command="kill -0 14575"): Allow(),
372
- guarded(command='kill -0 "$worker" 2>/dev/null || break'): Allow(),
373
- guarded(
374
- command=(
375
- "LOG=~/.claude/scratch/release-v3/sandsql-aig-migration/apply-a9922c25.log; PID=$(awk '{print $2}' "
376
- "~/.claude/scratch/release-v3/sandsql-aig-migration/apply.pid); kill -0 $PID 2>/dev/null && echo alive"
377
- )
378
- ): Allow(),
379
- guarded(command="kill -0 ${PID}"): Allow(),
380
434
  guarded(command="PID=31337; kill -0 $PID"): Allow(),
435
+ guarded(command='PID=31337; kill -0 "$PID" 2>/dev/null || break'): Allow(),
436
+ guarded(command="for p in 31337 31338; do kill -0 $p; done"): Allow(),
381
437
  guarded(command="kill -s 0 14575"): Allow(),
382
438
  guarded(command="kill -l"): Allow(),
383
439
  guarded(command="kill -l 9"): Allow(),
@@ -437,7 +493,17 @@ def renice_unverified_pid(evt: ToolRewriteEvent) -> HookResult | None:
437
493
 
438
494
 
439
495
  def probes_only(call: Call) -> bool:
440
- return call.args[:1] == ("-0",) and SIGNAL_FLAGS_SET.isdisjoint(call.args[1:])
496
+ if call.args[:1] != ("-0",):
497
+ return False
498
+ for arg in call.args[1:]:
499
+ if not arg.startswith("-"):
500
+ continue
501
+ flag = arg.partition("=")[0]
502
+ if flag in PKILL_CRITERIA_LONG:
503
+ continue
504
+ if flag.startswith("--") or len(flag) < 2 or not set(flag[1:]) <= PKILL_CRITERIA_SHORT:
505
+ return False
506
+ return True
441
507
 
442
508
 
443
509
  def criteria_program(call: Call) -> str | None:
@@ -481,6 +547,12 @@ def criteria_verdict(call: Call) -> str | None:
481
547
  guarded(command="X=pkill; cat <<EOF\n\tEOF\nX=echo\nEOF\n$X claude"): Block(pattern="signals every process"),
482
548
  guarded(command="X=pkill; cat <<-EOF\n\tEOF\nX=echo\nEOF\n$X claude"): Allow(),
483
549
  guarded(command="pkill node"): Block(),
550
+ guarded(command="pkill -0 --signal KILL -f claude"): Block(pattern="signals every process matching"),
551
+ guarded(command="pkill -0 --signal=KILL -f claude"): Block(),
552
+ guarded(command="pkill -0 -s KILL -f claude"): Block(),
553
+ guarded(command="pkill -0 -SIGKILL -f claude"): Block(),
554
+ guarded(command="pkill -0 -fs KILL claude"): Block(),
555
+ guarded(command="pkill -0 -e claude"): Block(),
484
556
  guarded(command="pkill -0 -9 x"): Block(),
485
557
  guarded(command="pkill -f -0 x"): Block(),
486
558
  guarded(command="pkill -TERM -0 x"): Block(),
@@ -516,6 +588,7 @@ def criteria_verdict(call: Call) -> str | None:
516
588
  guarded(command="fuser -v 3000/tcp"): Allow(),
517
589
  guarded(command="gh api rate_limit -q '.resources.core' ; date +%s; pkill -0 x 2>/dev/null; echo"): Allow(),
518
590
  guarded(command="pkill -0 -f 'ledger.py watch'"): Allow(),
591
+ guarded(command="pkill -0 -fx --newest claude"): Allow(),
519
592
  guarded(command="pgrep -f claude"): Allow(),
520
593
  guarded(command="echo pkill -f never"): Allow(),
521
594
  guarded(tool="mcp__runner__exec", tool_input={"cmd": "pgrep -f claude"}): Allow(),
@@ -624,10 +697,19 @@ def orca_ending_verdict(call: Call) -> str | None:
624
697
  )
625
698
  case (str() as group_name, str() as verb_name) if verb_name in ORCA_ENDINGS.get(group_name, ()):
626
699
  return orca_ending(spelling, group_name, verb_name, arguments)
700
+ case ("vm", verb_name) if (flag := orca_vm_run_flag(call)) is not None or verb_name != "recipe":
701
+ return (
702
+ f"BLOCKED: `{spelling}` passes `{flag or verb_name}`, which runs a recipe's commands on a VM, and a "
703
+ "recipe may end sessions. Run `orca vm recipe doctor <recipe-id>` without `--provision` or `--connect`."
704
+ )
627
705
  case _:
628
706
  return None
629
707
 
630
708
 
709
+ def orca_vm_run_flag(call: Call) -> str | None:
710
+ return next((arg for arg in call.args if arg.partition("=")[0] in ORCA_VM_RUN_FLAGS), None)
711
+
712
+
631
713
  @guard(
632
714
  tests={
633
715
  guarded(
@@ -678,6 +760,15 @@ def orca_ending_verdict(call: Call) -> str | None:
678
760
  guarded(command="orca nuke everything"): Block(pattern="does not know"),
679
761
  guarded(command="orca orchestration worker-release --help 2>&1 | head -15"): Block(pattern="worker-release"),
680
762
  guarded(command="orca terminal close --help"): Block(pattern="closes the current terminal"),
763
+ guarded(
764
+ command="orca vm recipe doctor guard-probe --provision --repo-path /Users/yasyf/.claude/scratch/x/vm-probe"
765
+ ): Block(pattern="passes `--provision`"),
766
+ guarded(command="orca vm recipe doctor guard-probe --connect --json"): Block(pattern="passes `--connect`"),
767
+ guarded(command="orca vm recipe doctor guard-probe --provision=true"): Block(
768
+ pattern="passes `--provision=true`"
769
+ ),
770
+ guarded(command="orca vm create guard-probe"): Block(pattern="passes `create`"),
771
+ guarded(command="orca vm recipe doctor cloud-sandbox --repo-path /path/to/repo --json"): Allow(),
681
772
  guarded(command="'orca' terminal close"): Block(),
682
773
  guarded(
683
774
  tool="mcp__runner__exec", tool_input={"command": "orca", "args": ["terminal", "close", "--terminal", "t"]}
@@ -91,6 +91,15 @@ hook(
91
91
  Input(command="cd / && for d in *; do find $d -name daemonkit; done"): Block(),
92
92
  Input(command="find ~/.claude/worktrees/*/ -name daemonkit"): Block(),
93
93
  Input(command="find /Users/* -maxdepth 3 -name daemonkit"): Block(),
94
+ Input(command='D=$(printf /); cd "$D" && find . -name daemonkit'): Block(),
95
+ Input(command="find . -name daemonkit"): Block(),
96
+ Input(
97
+ command=(
98
+ "for d in go/ci/internal/release/*/; do "
99
+ "echo \"$(find $d -name '*.go' ! -name '*_test.go' | xargs cat | wc -l) $d\"; done"
100
+ ),
101
+ cwd="/Users/yasyf/.claude/worktrees/monorepo/ethos-audit",
102
+ ): Block(),
94
103
  Input(command="find / -maxdepth 1 -type d"): Allow(),
95
104
  Input(command="find ~ -maxdepth 2 -type d -name daemonkit"): Allow(),
96
105
  Input(command="find / -maxdepth 0"): Allow(),
@@ -103,13 +112,6 @@ hook(
103
112
  "find $R -name 'targets.yaml' -not -path '*/node_modules/*' | head"
104
113
  )
105
114
  ): Allow(),
106
- Input(
107
- command=(
108
- "for d in go/ci/internal/release/*/; do "
109
- "echo \"$(find $d -name '*.go' ! -name '*_test.go' | xargs cat | wc -l) $d\"; done"
110
- ),
111
- cwd="/Users/yasyf/.claude/worktrees/monorepo/ethos-audit",
112
- ): Allow(),
113
115
  Input(
114
116
  command="for v in 0.2.9 0.2.11; do find $v -maxdepth 3; done",
115
117
  cwd="/Users/yasyf/.claude/plugins/cache/forge",
@@ -129,9 +131,9 @@ hook(
129
131
  ): Allow(),
130
132
  Input(command="find $HOME/.claude/worktrees/captain-hook/x -name daemonkit"): Allow(),
131
133
  Input(command="find ~/.claude/worktrees/captain-hook/* -name daemonkit"): Allow(),
132
- Input(command="find src -name daemonkit"): Allow(),
134
+ Input(command="find src -name daemonkit", cwd="/repo"): Allow(),
133
135
  Input(command="find src -newermt 2026-09-17", cwd="/repo"): Allow(),
134
- Input(command="find src -path / -prune"): Allow(),
136
+ Input(command="find src -path / -prune", cwd="/repo"): Allow(),
135
137
  Input(command="echo 'find / -name daemonkit'"): Allow(),
136
138
  Input(command="cat <<'EOF'\nfind / -name daemonkit\nEOF"): Allow(),
137
139
  Input(command="ccx repo locate daemonkit"): Allow(),
@@ -580,7 +580,7 @@ class Cmd:
580
580
  return self.scope, None
581
581
  quote = call.occurrence.quote_contexts[-1:]
582
582
  if quote == ('"',):
583
- return host_scope, host_offset
583
+ return (self.scope, None) if "\\$" in payload.raw else (host_scope, host_offset)
584
584
  within = None if word.span is None else self.char_offset(word.span[0]) - self.char_offset(payload.span[0])
585
585
  if quote == ("'",) and payload.value is not None:
586
586
  text, start = payload.value, 1
@@ -263,7 +263,7 @@ class PathMatches:
263
263
  path = Path(value)
264
264
  if not path.is_absolute():
265
265
  if target.cwd is None:
266
- return False
266
+ return self.unresolved
267
267
  path = target.cwd / path
268
268
  return any(self.matches(candidate) for candidate in (path, glob_prefix(path)))
269
269
 
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  name = "capt-hook"
3
3
  # Inert sentinel — the real version is stamped from the release tag (uv version --frozen).
4
- version = "12.76.1"
4
+ version = "12.76.2"
5
5
  description = "Stop repeating yourself to Claude."
6
6
  readme = "README.md"
7
7
  license = "PolyForm-Noncommercial-1.0.0"
File without changes
File without changes
File without changes