plugin-scanner 2.0.60__tar.gz → 2.0.61__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 (317) hide show
  1. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/PKG-INFO +1 -1
  2. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/pyproject.toml +1 -1
  3. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/pyproject.toml.bak +1 -1
  4. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/adapters/claude_code.py +25 -1
  5. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/cli/commands.py +333 -12
  6. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/version.py +1 -1
  7. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_guard_claude_adapter.py +13 -2
  8. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_guard_cli.py +4 -0
  9. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_guard_runtime.py +233 -8
  10. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_guard_surface_server.py +9 -2
  11. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/.clusterfuzzlite/Dockerfile +0 -0
  12. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/.clusterfuzzlite/build.sh +0 -0
  13. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/.clusterfuzzlite/project.yaml +0 -0
  14. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/.clusterfuzzlite/requirements-atheris.txt +0 -0
  15. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/.dockerignore +0 -0
  16. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/.github/CODEOWNERS +0 -0
  17. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/.github/ISSUE_TEMPLATE/bug-report.yml +0 -0
  18. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  19. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/.github/ISSUE_TEMPLATE/feature-request.yml +0 -0
  20. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/.github/dependabot.yml +0 -0
  21. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/.github/workflows/ci.yml +0 -0
  22. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/.github/workflows/codeql.yml +0 -0
  23. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/.github/workflows/dependabot-uv-lock.yml +0 -0
  24. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/.github/workflows/fuzz.yml +0 -0
  25. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/.github/workflows/harness-smoke.yml +0 -0
  26. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/.github/workflows/publish.yml +0 -0
  27. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/.github/workflows/scorecard.yml +0 -0
  28. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/.gitignore +0 -0
  29. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/.pre-commit-hooks.yaml +0 -0
  30. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/CONTRIBUTING.md +0 -0
  31. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/Dockerfile +0 -0
  32. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/LICENSE +0 -0
  33. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/README.md +0 -0
  34. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/SECURITY.md +0 -0
  35. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/dashboard/index.html +0 -0
  36. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/dashboard/package.json +0 -0
  37. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/dashboard/pnpm-lock.yaml +0 -0
  38. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/dashboard/public/brand/Logo_Whole.png +0 -0
  39. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/dashboard/src/app.tsx +0 -0
  40. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/dashboard/src/approval-center-layout.tsx +0 -0
  41. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/dashboard/src/approval-center-primitives.tsx +0 -0
  42. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/dashboard/src/approval-center-utils.ts +0 -0
  43. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/dashboard/src/fleet-workspace.tsx +0 -0
  44. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/dashboard/src/guard-api.ts +0 -0
  45. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/dashboard/src/guard-demo.ts +0 -0
  46. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/dashboard/src/guard-types.ts +0 -0
  47. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/dashboard/src/main.tsx +0 -0
  48. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/dashboard/src/receipts-workspace.tsx +0 -0
  49. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/dashboard/src/runtime-overview.tsx +0 -0
  50. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/dashboard/src/styles.css +0 -0
  51. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/dashboard/src/vite-env.d.ts +0 -0
  52. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/dashboard/tsconfig.json +0 -0
  53. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/dashboard/vite.config.ts +0 -0
  54. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/docker-requirements.txt +0 -0
  55. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/docs/guard/approval-audit.md +0 -0
  56. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/docs/guard/architecture.md +0 -0
  57. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/docs/guard/get-started.md +0 -0
  58. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/docs/guard/harness-support.md +0 -0
  59. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/docs/guard/local-vs-cloud.md +0 -0
  60. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/docs/guard/testing-matrix.md +0 -0
  61. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/docs/trust/mcp-trust-draft.md +0 -0
  62. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/docs/trust/plugin-trust-draft.md +0 -0
  63. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/docs/trust/skill-trust-local.md +0 -0
  64. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/fuzzers/manifest_fuzzer.py +0 -0
  65. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/requirements.txt +0 -0
  66. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/schemas/plugin-quality.v1.json +0 -0
  67. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/schemas/scan-result.v1.json +0 -0
  68. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/schemas/verify-result.v1.json +0 -0
  69. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/__init__.py +0 -0
  70. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/action_runner.py +0 -0
  71. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/argparse_utils.py +0 -0
  72. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/checks/__init__.py +0 -0
  73. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/checks/best_practices.py +0 -0
  74. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/checks/claude.py +0 -0
  75. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/checks/code_quality.py +0 -0
  76. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/checks/ecosystem_common.py +0 -0
  77. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/checks/gemini.py +0 -0
  78. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/checks/manifest.py +0 -0
  79. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/checks/manifest_support.py +0 -0
  80. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/checks/marketplace.py +0 -0
  81. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/checks/mcp_security.py +0 -0
  82. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/checks/opencode.py +0 -0
  83. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/checks/operational_security.py +0 -0
  84. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/checks/security.py +0 -0
  85. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/checks/skill_security.py +0 -0
  86. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/cli.py +0 -0
  87. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/cli_ui.py +0 -0
  88. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/config.py +0 -0
  89. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/ecosystems/__init__.py +0 -0
  90. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/ecosystems/base.py +0 -0
  91. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/ecosystems/claude.py +0 -0
  92. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/ecosystems/codex.py +0 -0
  93. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/ecosystems/detect.py +0 -0
  94. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/ecosystems/gemini.py +0 -0
  95. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/ecosystems/opencode.py +0 -0
  96. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/ecosystems/registry.py +0 -0
  97. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/ecosystems/types.py +0 -0
  98. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/github_reporting.py +0 -0
  99. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/__init__.py +0 -0
  100. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/adapters/__init__.py +0 -0
  101. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/adapters/antigravity.py +0 -0
  102. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/adapters/base.py +0 -0
  103. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/adapters/codex.py +0 -0
  104. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/adapters/copilot.py +0 -0
  105. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/adapters/cursor.py +0 -0
  106. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/adapters/gemini.py +0 -0
  107. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/adapters/hermes.py +0 -0
  108. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/adapters/mcp_servers.py +0 -0
  109. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/adapters/opencode.py +0 -0
  110. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/adapters/opencode_artifacts.py +0 -0
  111. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/approvals.py +0 -0
  112. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/bridge/__init__.py +0 -0
  113. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/capabilities.py +0 -0
  114. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/cli/__init__.py +0 -0
  115. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/cli/approval_commands.py +0 -0
  116. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/cli/bootstrap.py +0 -0
  117. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/cli/connect_flow.py +0 -0
  118. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/cli/install_commands.py +0 -0
  119. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/cli/product.py +0 -0
  120. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/cli/prompt.py +0 -0
  121. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/cli/render.py +0 -0
  122. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/cli/update_commands.py +0 -0
  123. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/codex_config.py +0 -0
  124. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/config.py +0 -0
  125. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/consumer/__init__.py +0 -0
  126. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/consumer/service.py +0 -0
  127. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/daemon/__init__.py +0 -0
  128. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/daemon/client.py +0 -0
  129. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/daemon/manager.py +0 -0
  130. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/daemon/server.py +0 -0
  131. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/daemon/static/assets/guard-dashboard.js +0 -0
  132. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/daemon/static/assets/index.css +0 -0
  133. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/daemon/static/brand/Logo_Whole.png +0 -0
  134. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/daemon/static/index.html +0 -0
  135. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/incident.py +0 -0
  136. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/launcher.py +0 -0
  137. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/mcp_tool_calls.py +0 -0
  138. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/models.py +0 -0
  139. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/policy/__init__.py +0 -0
  140. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/policy/engine.py +0 -0
  141. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/protect.py +0 -0
  142. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/proxy/__init__.py +0 -0
  143. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/proxy/remote.py +0 -0
  144. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/proxy/runtime_mcp.py +0 -0
  145. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/proxy/stdio.py +0 -0
  146. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/receipts/__init__.py +0 -0
  147. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/receipts/manager.py +0 -0
  148. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/risk.py +0 -0
  149. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/runtime/__init__.py +0 -0
  150. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/runtime/runner.py +0 -0
  151. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/runtime/secret_file_requests.py +0 -0
  152. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/runtime/surface_server.py +0 -0
  153. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/schemas/__init__.py +0 -0
  154. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/schemas/consumer_mode.py +0 -0
  155. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/schemas/surface_server.py +0 -0
  156. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/shims.py +0 -0
  157. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/store.py +0 -0
  158. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/store_approvals.py +0 -0
  159. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/store_connect.py +0 -0
  160. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/guard/types.py +0 -0
  161. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/integrations/__init__.py +0 -0
  162. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/integrations/cisco_mcp_scanner.py +0 -0
  163. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/integrations/cisco_skill_scanner.py +0 -0
  164. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/lint_fixes.py +0 -0
  165. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/marketplace_support.py +0 -0
  166. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/models.py +0 -0
  167. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/path_support.py +0 -0
  168. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/policy.py +0 -0
  169. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/quality_artifact.py +0 -0
  170. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/repo_detect.py +0 -0
  171. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/reporting.py +0 -0
  172. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/rules/__init__.py +0 -0
  173. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/rules/registry.py +0 -0
  174. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/rules/specs.py +0 -0
  175. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/scanner.py +0 -0
  176. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/submission.py +0 -0
  177. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/suppressions.py +0 -0
  178. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/trust_domain_scoring.py +0 -0
  179. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/trust_helpers.py +0 -0
  180. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/trust_mcp_scoring.py +0 -0
  181. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/trust_models.py +0 -0
  182. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/trust_plugin_scoring.py +0 -0
  183. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/trust_scoring.py +0 -0
  184. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/trust_skill_scoring.py +0 -0
  185. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/trust_specs.py +0 -0
  186. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/src/codex_plugin_scanner/verification.py +0 -0
  187. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/__init__.py +0 -0
  188. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/conftest.py +0 -0
  189. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/__init__.py +0 -0
  190. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/bad-plugin/.codex-plugin/plugin.json +0 -0
  191. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/bad-plugin/.mcp.json +0 -0
  192. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/bad-plugin/secrets.js +0 -0
  193. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/claude-plugin-good/.claude-plugin/plugin.json +0 -0
  194. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/claude-plugin-good/LICENSE +0 -0
  195. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/claude-plugin-good/README.md +0 -0
  196. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/claude-plugin-good/SECURITY.md +0 -0
  197. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/claude-plugin-good/hooks/hooks.json +0 -0
  198. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/claude-plugin-good/skills/example/SKILL.md +0 -0
  199. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/code-quality-bad/evil.js +0 -0
  200. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/code-quality-bad/inject.js +0 -0
  201. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/gemini-extension-good/GEMINI.md +0 -0
  202. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/gemini-extension-good/LICENSE +0 -0
  203. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/gemini-extension-good/README.md +0 -0
  204. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/gemini-extension-good/SECURITY.md +0 -0
  205. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/gemini-extension-good/commands/hello.toml +0 -0
  206. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/gemini-extension-good/gemini-extension.json +0 -0
  207. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/good-plugin/.codex-plugin/plugin.json +0 -0
  208. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/good-plugin/.codexignore +0 -0
  209. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/good-plugin/LICENSE +0 -0
  210. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/good-plugin/README.md +0 -0
  211. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/good-plugin/SECURITY.md +0 -0
  212. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/good-plugin/assets/icon.svg +0 -0
  213. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/good-plugin/assets/logo.svg +0 -0
  214. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/good-plugin/assets/screenshot.svg +0 -0
  215. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/good-plugin/skills/example/SKILL.md +0 -0
  216. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/guard-codex-malicious-mcp/.codex/config.toml +0 -0
  217. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/hermes-plugin-evil/config.yaml +0 -0
  218. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/hermes-plugin-evil/mcp_servers.json +0 -0
  219. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/hermes-plugin-evil/skills/security/malicious/SKILL.md +0 -0
  220. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/hermes-plugin-evil/skills/stealth/sneaky/SKILL.md +0 -0
  221. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/hermes-plugin-evil/skills/stealth/sneaky/references/api-setup.md +0 -0
  222. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/hermes-plugin-evil/skills/stealth/sneaky/scripts/deploy.sh +0 -0
  223. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/hermes-plugin-evil/skills/utils/benign/SKILL.md +0 -0
  224. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/malformed-json/.codex-plugin/plugin.json +0 -0
  225. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/malicious-skill-plugin/.codex-plugin/plugin.json +0 -0
  226. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/malicious-skill-plugin/.codexignore +0 -0
  227. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/malicious-skill-plugin/LICENSE +0 -0
  228. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/malicious-skill-plugin/README.md +0 -0
  229. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/malicious-skill-plugin/SECURITY.md +0 -0
  230. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/malicious-skill-plugin/skills/leaky-skill/SKILL.md +0 -0
  231. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/mcp-canary-server.py +0 -0
  232. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/minimal-plugin/.codex-plugin/plugin.json +0 -0
  233. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/missing-fields/.codex-plugin/plugin.json +0 -0
  234. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/mit-license/LICENSE +0 -0
  235. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/multi-ecosystem-repo/codex-plugin/.codex-plugin/plugin.json +0 -0
  236. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/multi-ecosystem-repo/codex-plugin/LICENSE +0 -0
  237. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/multi-ecosystem-repo/codex-plugin/README.md +0 -0
  238. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/multi-ecosystem-repo/codex-plugin/SECURITY.md +0 -0
  239. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/multi-ecosystem-repo/gemini-ext/README.md +0 -0
  240. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/multi-ecosystem-repo/gemini-ext/gemini-extension.json +0 -0
  241. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/multi-plugin-repo/.agents/plugins/marketplace.json +0 -0
  242. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/multi-plugin-repo/plugins/alpha-plugin/.codex-plugin/plugin.json +0 -0
  243. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/multi-plugin-repo/plugins/alpha-plugin/.codexignore +0 -0
  244. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/multi-plugin-repo/plugins/alpha-plugin/LICENSE +0 -0
  245. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/multi-plugin-repo/plugins/alpha-plugin/README.md +0 -0
  246. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/multi-plugin-repo/plugins/alpha-plugin/SECURITY.md +0 -0
  247. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/multi-plugin-repo/plugins/alpha-plugin/skills/example/SKILL.md +0 -0
  248. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/multi-plugin-repo/plugins/beta-plugin/.codex-plugin/plugin.json +0 -0
  249. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/multi-plugin-repo/plugins/beta-plugin/skills/example/SKILL.md +0 -0
  250. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/no-version/.codex-plugin/plugin.json +0 -0
  251. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/opencode-good/.opencode/commands/hello.md +0 -0
  252. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/opencode-good/.opencode/plugins/example.ts +0 -0
  253. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/opencode-good/LICENSE +0 -0
  254. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/opencode-good/README.md +0 -0
  255. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/opencode-good/SECURITY.md +0 -0
  256. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/opencode-good/opencode.jsonc +0 -0
  257. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/skills-missing-dir/.codex-plugin/plugin.json +0 -0
  258. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/skills-no-frontmatter/.codex-plugin/plugin.json +0 -0
  259. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/skills-no-frontmatter/skills/bad-skill/SKILL.md +0 -0
  260. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/with-marketplace/.codex-plugin/plugin.json +0 -0
  261. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/with-marketplace/marketplace-broken.json +0 -0
  262. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/fixtures/with-marketplace/marketplace.json +0 -0
  263. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test-trust-scoring.py +0 -0
  264. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test-trust-specs.py +0 -0
  265. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_action_runner.py +0 -0
  266. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_best_practices.py +0 -0
  267. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_cisco_install_surfaces.py +0 -0
  268. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_cli.py +0 -0
  269. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_code_quality.py +0 -0
  270. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_config.py +0 -0
  271. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_coverage_remaining.py +0 -0
  272. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_ecosystems.py +0 -0
  273. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_edge_cases.py +0 -0
  274. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_final_coverage.py +0 -0
  275. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_guard_approvals.py +0 -0
  276. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_guard_bootstrap.py +0 -0
  277. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_guard_capabilities.py +0 -0
  278. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_guard_codex_e2e.py +0 -0
  279. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_guard_codex_install.py +0 -0
  280. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_guard_codex_proxy.py +0 -0
  281. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_guard_config_paths.py +0 -0
  282. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_guard_connect_flow.py +0 -0
  283. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_guard_consumer_mode.py +0 -0
  284. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_guard_copilot_adapter.py +0 -0
  285. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_guard_copilot_proxy.py +0 -0
  286. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_guard_daemon_manager.py +0 -0
  287. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_guard_events.py +0 -0
  288. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_guard_launch_env.py +0 -0
  289. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_guard_opencode_proxy.py +0 -0
  290. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_guard_product_flow.py +0 -0
  291. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_guard_protect.py +0 -0
  292. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_guard_render.py +0 -0
  293. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_guard_risk.py +0 -0
  294. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_guard_store_migrations.py +0 -0
  295. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_guard_verdicts.py +0 -0
  296. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_hermes_adapter.py +0 -0
  297. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_integration.py +0 -0
  298. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_lint_fixes.py +0 -0
  299. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_live_cisco_smoke.py +0 -0
  300. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_manifest.py +0 -0
  301. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_marketplace.py +0 -0
  302. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_mcp_security.py +0 -0
  303. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_operational_security.py +0 -0
  304. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_policy.py +0 -0
  305. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_quality_artifact.py +0 -0
  306. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_rule_registry.py +0 -0
  307. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_scanner.py +0 -0
  308. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_schema_contracts.py +0 -0
  309. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_security.py +0 -0
  310. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_security_ops.py +0 -0
  311. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_skill_security.py +0 -0
  312. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_submission.py +0 -0
  313. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_trust_scoring.py +0 -0
  314. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_trust_specs.py +0 -0
  315. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_verification.py +0 -0
  316. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/tests/test_versioning.py +0 -0
  317. {plugin_scanner-2.0.60 → plugin_scanner-2.0.61}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plugin-scanner
3
- Version: 2.0.60
3
+ Version: 2.0.61
4
4
  Summary: Lint, verify, and gate plugin ecosystems for maintainers, CI, and publish workflows.
5
5
  Project-URL: Homepage, https://github.com/hashgraph-online/ai-plugin-scanner
6
6
  Project-URL: Repository, https://github.com/hashgraph-online/ai-plugin-scanner
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "plugin-scanner"
7
- version = "2.0.60"
7
+ version = "2.0.61"
8
8
  description = "Lint, verify, and gate plugin ecosystems for maintainers, CI, and publish workflows."
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "hol-guard"
7
- version = "2.0.60"
7
+ version = "2.0.61"
8
8
  description = "Protect local AI harnesses with HOL Guard and run scanner checks for Codex, Claude, Cursor, Gemini, and OpenCode."
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -25,6 +25,7 @@ CLAUDE_GUARD_TOOL_TIMEOUT_SECONDS = 30
25
25
  CLAUDE_GUARD_PROMPT_TIMEOUT_SECONDS = 20
26
26
  CLAUDE_GUARD_NOTIFICATION_TIMEOUT_SECONDS = 10
27
27
  CLAUDE_GUARD_SESSION_START_TIMEOUT_SECONDS = 10
28
+ CLAUDE_GUARD_STOP_TIMEOUT_SECONDS = 10
28
29
  CLAUDE_SETTINGS_FILES = ("settings.json", "settings.local.json")
29
30
  CLAUDE_GUARD_DAEMON_HOOK_MARKER = "HOL_GUARD_CLAUDE_DAEMON_HOOK"
30
31
 
@@ -46,6 +47,7 @@ def _sync_runtime_hook_groups(hooks: dict[str, object], hook_command: str) -> No
46
47
  ("PostToolUse", CLAUDE_GUARD_TOOL_MATCHER, CLAUDE_GUARD_TOOL_TIMEOUT_SECONDS),
47
48
  ("UserPromptSubmit", None, CLAUDE_GUARD_PROMPT_TIMEOUT_SECONDS),
48
49
  ("Notification", CLAUDE_GUARD_NOTIFICATION_MATCHER, CLAUDE_GUARD_NOTIFICATION_TIMEOUT_SECONDS),
50
+ ("Stop", None, CLAUDE_GUARD_STOP_TIMEOUT_SECONDS),
49
51
  ):
50
52
  existing_entries = hooks.get(key)
51
53
  hooks[key] = _merge_hook_group(
@@ -55,6 +57,18 @@ def _sync_runtime_hook_groups(hooks: dict[str, object], hook_command: str) -> No
55
57
  )
56
58
 
57
59
 
60
+ def _remove_unsupported_guard_hook_groups(hooks: dict[str, object]) -> None:
61
+ for key in ("PermissionDenied",):
62
+ entries = hooks.get(key)
63
+ if not isinstance(entries, list):
64
+ continue
65
+ remaining = _prune_guard_hook_entries(entries)
66
+ if remaining:
67
+ hooks[key] = remaining
68
+ else:
69
+ hooks.pop(key, None)
70
+
71
+
58
72
  def _guard_hook_group(matcher: str | None, handler: dict[str, object]) -> dict[str, object]:
59
73
  payload: dict[str, object] = {"hooks": [handler]}
60
74
  if isinstance(matcher, str) and matcher.strip():
@@ -577,6 +591,7 @@ class ClaudeCodeHarnessAdapter(HarnessAdapter):
577
591
  if not isinstance(hooks, dict):
578
592
  return
579
593
  _sync_runtime_hook_groups(hooks, self._daemon_hook_command(context))
594
+ _remove_unsupported_guard_hook_groups(hooks)
580
595
  settings_path.parent.mkdir(parents=True, exist_ok=True)
581
596
  settings_path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
582
597
 
@@ -620,6 +635,7 @@ class ClaudeCodeHarnessAdapter(HarnessAdapter):
620
635
  session_start_entries = _merge_hook_group(session_start_entries, matcher, session_start_handler)
621
636
  hooks["SessionStart"] = session_start_entries
622
637
  _sync_runtime_hook_groups(hooks, hook_command)
638
+ _remove_unsupported_guard_hook_groups(hooks)
623
639
  settings_path.parent.mkdir(parents=True, exist_ok=True)
624
640
  settings_path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
625
641
  return {
@@ -653,9 +669,17 @@ class ClaudeCodeHarnessAdapter(HarnessAdapter):
653
669
  payload = _json_payload(settings_path)
654
670
  hooks = payload.get("hooks")
655
671
  if isinstance(hooks, dict):
656
- for key in ("SessionStart", "PreToolUse", "PostToolUse", "UserPromptSubmit", "Notification"):
672
+ for key in (
673
+ "SessionStart",
674
+ "PreToolUse",
675
+ "PostToolUse",
676
+ "UserPromptSubmit",
677
+ "Notification",
678
+ "Stop",
679
+ ):
657
680
  entries = hooks.get(key)
658
681
  hooks[key] = _prune_guard_hook_entries(entries if isinstance(entries, list) else [])
682
+ _remove_unsupported_guard_hook_groups(hooks)
659
683
  settings_path.parent.mkdir(parents=True, exist_ok=True)
660
684
  settings_path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
661
685
  return {
@@ -14,6 +14,7 @@ import sys
14
14
  import urllib.error
15
15
  import urllib.parse
16
16
  import webbrowser
17
+ from dataclasses import replace
17
18
  from datetime import datetime, timedelta, timezone
18
19
  from pathlib import Path
19
20
  from typing import TextIO
@@ -55,7 +56,7 @@ from ..mcp_tool_calls import (
55
56
  build_tool_call_hash,
56
57
  evaluate_tool_call,
57
58
  )
58
- from ..models import GuardArtifact, HarnessDetection
59
+ from ..models import GuardArtifact, HarnessDetection, PolicyDecision
59
60
  from ..policy.engine import SAFE_CHANGED_HASH_ACTION, VALID_GUARD_ACTIONS
60
61
  from ..protect import build_protect_payload
61
62
  from ..proxy import (
@@ -1215,17 +1216,40 @@ def run_guard_command(
1215
1216
  output_stream=output_stream,
1216
1217
  )
1217
1218
  return 0
1219
+ if _canonical_harness_name(args.harness) == "claude-code" and _hook_event_name(payload) == "Stop":
1220
+ denied = _persist_claude_pending_permission_denials(store, payload)
1221
+ store.add_event(
1222
+ "claude/turn_stop",
1223
+ {"session_id": payload.get("session_id"), "saved_denials": denied},
1224
+ _now(),
1225
+ )
1226
+ return 0
1218
1227
  if runtime_artifact is not None:
1219
1228
  event_name = _hook_event_name(payload) or "PreToolUse"
1220
1229
  runtime_artifact_hash = artifact_hash(runtime_artifact)
1221
1230
  artifact_id = runtime_artifact.artifact_id
1222
1231
  artifact_name = runtime_artifact.name
1232
+ policy_harness = _canonical_harness_name(args.harness)
1223
1233
  stored_policy_action = store.resolve_policy(
1224
- args.harness,
1234
+ policy_harness,
1225
1235
  artifact_id,
1226
1236
  runtime_artifact_hash,
1227
1237
  str(runtime_workspace) if runtime_workspace else None,
1228
1238
  )
1239
+ if stored_policy_action is None:
1240
+ legacy_artifact = _legacy_claude_alias_runtime_artifact(
1241
+ artifact=runtime_artifact,
1242
+ requested_harness=args.harness,
1243
+ home_dir=context.home_dir,
1244
+ workspace=runtime_workspace,
1245
+ )
1246
+ if legacy_artifact is not None:
1247
+ stored_policy_action = store.resolve_policy(
1248
+ args.harness,
1249
+ legacy_artifact.artifact_id,
1250
+ artifact_hash(legacy_artifact),
1251
+ str(runtime_workspace) if runtime_workspace else None,
1252
+ )
1229
1253
  policy_action = _coalesce_string(
1230
1254
  getattr(args, "policy_action", None),
1231
1255
  stored_policy_action,
@@ -1233,6 +1257,33 @@ def run_guard_command(
1233
1257
  )
1234
1258
  if policy_action not in VALID_GUARD_ACTIONS:
1235
1259
  policy_action = SAFE_CHANGED_HASH_ACTION
1260
+ if _canonical_harness_name(args.harness) == "claude-code" and event_name in {
1261
+ "PostToolUse",
1262
+ "PostToolUseFailure",
1263
+ }:
1264
+ saved = _persist_claude_native_permission_for_runtime_artifact(
1265
+ store=store,
1266
+ payload=payload,
1267
+ artifact=runtime_artifact,
1268
+ artifact_hash=runtime_artifact_hash,
1269
+ action="allow",
1270
+ reason="Approved in Claude native approval prompt.",
1271
+ )
1272
+ if saved:
1273
+ receipt = build_receipt(
1274
+ harness=policy_harness,
1275
+ artifact_id=artifact_id,
1276
+ artifact_hash=runtime_artifact_hash,
1277
+ policy_decision="allow",
1278
+ capabilities_summary=_runtime_capabilities_summary(runtime_artifact),
1279
+ changed_capabilities=[runtime_artifact.artifact_type, "claude-native-approved"],
1280
+ provenance_summary=f"runtime tool request approved from {runtime_artifact.config_path}",
1281
+ artifact_name=artifact_name,
1282
+ source_scope=runtime_artifact.source_scope,
1283
+ user_override="claude-native-approve",
1284
+ )
1285
+ store.add_receipt(receipt)
1286
+ return 0
1236
1287
  changed_capabilities = [runtime_artifact.artifact_type]
1237
1288
  risk_signals = list(artifact_risk_signals(runtime_artifact))
1238
1289
  risk_summary = artifact_risk_summary(runtime_artifact)
@@ -1288,16 +1339,6 @@ def run_guard_command(
1288
1339
  artifact=runtime_artifact,
1289
1340
  native_reason=native_reason,
1290
1341
  )
1291
- if (
1292
- _canonical_harness_name(args.harness) == "claude-code"
1293
- and event_name == "UserPromptSubmit"
1294
- and policy_action == "require-reapproval"
1295
- and not _prompt_requires_hard_block(runtime_artifact)
1296
- and (output_stream is not None or not getattr(args, "json", False))
1297
- ):
1298
- if getattr(args, "json", False) and output_stream is not None:
1299
- _write_json_line({}, output_stream=output_stream)
1300
- return 0
1301
1342
  if (
1302
1343
  _canonical_harness_name(args.harness) == "claude-code"
1303
1344
  and event_name == "PreToolUse"
@@ -1308,6 +1349,7 @@ def run_guard_command(
1308
1349
  payload=payload,
1309
1350
  reason=native_reason,
1310
1351
  artifact=runtime_artifact,
1352
+ artifact_hash=runtime_artifact_hash,
1311
1353
  )
1312
1354
  if _should_emit_copilot_hook_response(args):
1313
1355
  _emit_copilot_hook_response(
@@ -1668,12 +1710,62 @@ def _claude_permission_notice_state_key(session_id: str, tool_name: str | None =
1668
1710
  return f"claude_permission_notice:{session_id}"
1669
1711
 
1670
1712
 
1713
+ def _claude_pending_permission_index_key(session_id: str) -> str:
1714
+ return f"claude_pending_permissions:{session_id}"
1715
+
1716
+
1717
+ def _claude_pending_permission_state_key(session_id: str, artifact_id: str) -> str:
1718
+ fingerprint = hashlib.sha256(artifact_id.encode("utf-8")).hexdigest()[:24]
1719
+ return f"claude_pending_permission:{session_id}:{fingerprint}"
1720
+
1721
+
1722
+ def _sync_payload_list_from_row(row: sqlite3.Row | None) -> list[str]:
1723
+ if row is None:
1724
+ return []
1725
+ try:
1726
+ payload = json.loads(str(row["payload_json"]))
1727
+ except json.JSONDecodeError:
1728
+ return []
1729
+ return [str(item) for item in payload] if isinstance(payload, list) else []
1730
+
1731
+
1732
+ def _append_claude_pending_permission_key(
1733
+ store: GuardStore,
1734
+ *,
1735
+ session_id: str,
1736
+ pending_key: str,
1737
+ now: str,
1738
+ ) -> None:
1739
+ index_key = _claude_pending_permission_index_key(session_id)
1740
+ with store._connect() as connection:
1741
+ connection.execute("begin immediate")
1742
+ row = connection.execute(
1743
+ "select payload_json from sync_state where state_key = ?",
1744
+ (index_key,),
1745
+ ).fetchone()
1746
+ pending_keys = _sync_payload_list_from_row(row)
1747
+ if pending_key in pending_keys:
1748
+ return
1749
+ pending_keys.append(pending_key)
1750
+ connection.execute(
1751
+ """
1752
+ insert into sync_state (state_key, payload_json, updated_at)
1753
+ values (?, ?, ?)
1754
+ on conflict(state_key) do update set
1755
+ payload_json = excluded.payload_json,
1756
+ updated_at = excluded.updated_at
1757
+ """,
1758
+ (index_key, json.dumps(pending_keys), now),
1759
+ )
1760
+
1761
+
1671
1762
  def _record_claude_permission_notice(
1672
1763
  *,
1673
1764
  store: GuardStore,
1674
1765
  payload: dict[str, object],
1675
1766
  reason: str,
1676
1767
  artifact: GuardArtifact,
1768
+ artifact_hash: str,
1677
1769
  ) -> None:
1678
1770
  session_id = _optional_string(payload.get("session_id"))
1679
1771
  if session_id is None:
@@ -1683,12 +1775,19 @@ def _record_claude_permission_notice(
1683
1775
  "saved_at": _now(),
1684
1776
  "reason": reason,
1685
1777
  "artifact_id": artifact.artifact_id,
1778
+ "artifact_hash": artifact_hash,
1686
1779
  "artifact_name": artifact.name,
1780
+ "artifact_type": artifact.artifact_type,
1781
+ "config_path": artifact.config_path,
1782
+ "source_scope": artifact.source_scope,
1687
1783
  }
1688
1784
  if tool_name is not None:
1689
1785
  notice_payload["tool_name"] = tool_name
1690
1786
  try:
1691
1787
  store.set_sync_payload(_claude_permission_notice_state_key(session_id, tool_name), notice_payload, _now())
1788
+ pending_key = _claude_pending_permission_state_key(session_id, artifact.artifact_id)
1789
+ store.set_sync_payload(pending_key, notice_payload, _now())
1790
+ _append_claude_pending_permission_key(store, session_id=session_id, pending_key=pending_key, now=_now())
1692
1791
  except (OSError, sqlite3.Error):
1693
1792
  return
1694
1793
 
@@ -1713,6 +1812,205 @@ def _load_claude_permission_notice(store: GuardStore, payload: dict[str, object]
1713
1812
  return None
1714
1813
 
1715
1814
 
1815
+ def _load_claude_pending_permission(
1816
+ store: GuardStore,
1817
+ payload: dict[str, object],
1818
+ artifact: GuardArtifact,
1819
+ ) -> dict[str, object] | None:
1820
+ session_id = _optional_string(payload.get("session_id"))
1821
+ if session_id is None:
1822
+ return None
1823
+ pending_key = _claude_pending_permission_state_key(session_id, artifact.artifact_id)
1824
+ try:
1825
+ persisted = store.get_sync_payload(pending_key)
1826
+ except (OSError, sqlite3.Error):
1827
+ return None
1828
+ return persisted if isinstance(persisted, dict) else None
1829
+
1830
+
1831
+ def _remove_claude_pending_permission(
1832
+ store: GuardStore,
1833
+ *,
1834
+ session_id: str,
1835
+ pending_key: str,
1836
+ ) -> None:
1837
+ try:
1838
+ index_key = _claude_pending_permission_index_key(session_id)
1839
+ with store._connect() as connection:
1840
+ connection.execute("begin immediate")
1841
+ connection.execute("delete from sync_state where state_key = ?", (pending_key,))
1842
+ row = connection.execute(
1843
+ "select payload_json from sync_state where state_key = ?",
1844
+ (index_key,),
1845
+ ).fetchone()
1846
+ remaining = [key for key in _sync_payload_list_from_row(row) if key != pending_key]
1847
+ if remaining:
1848
+ connection.execute(
1849
+ """
1850
+ insert into sync_state (state_key, payload_json, updated_at)
1851
+ values (?, ?, ?)
1852
+ on conflict(state_key) do update set
1853
+ payload_json = excluded.payload_json,
1854
+ updated_at = excluded.updated_at
1855
+ """,
1856
+ (index_key, json.dumps(remaining), _now()),
1857
+ )
1858
+ else:
1859
+ connection.execute("delete from sync_state where state_key = ?", (index_key,))
1860
+ except (OSError, sqlite3.Error):
1861
+ return
1862
+
1863
+
1864
+ def _persist_claude_native_permission_policy(
1865
+ *,
1866
+ store: GuardStore,
1867
+ artifact_id: str,
1868
+ artifact_hash: str,
1869
+ action: str,
1870
+ reason: str,
1871
+ now: str,
1872
+ ) -> bool:
1873
+ try:
1874
+ store.upsert_policy(
1875
+ PolicyDecision(
1876
+ harness="claude-code",
1877
+ scope="artifact",
1878
+ action="allow" if action == "allow" else "block",
1879
+ artifact_id=artifact_id,
1880
+ artifact_hash=artifact_hash,
1881
+ reason=reason,
1882
+ source="claude-native-approval",
1883
+ ),
1884
+ now,
1885
+ )
1886
+ store.add_event(
1887
+ "claude/native_permission_saved",
1888
+ {
1889
+ "artifact_id": artifact_id,
1890
+ "artifact_hash": artifact_hash,
1891
+ "action": action,
1892
+ "reason": reason,
1893
+ },
1894
+ now,
1895
+ )
1896
+ except (OSError, sqlite3.Error):
1897
+ return False
1898
+ return True
1899
+
1900
+
1901
+ def _persist_claude_native_permission_for_runtime_artifact(
1902
+ *,
1903
+ store: GuardStore,
1904
+ payload: dict[str, object],
1905
+ artifact: GuardArtifact,
1906
+ artifact_hash: str,
1907
+ action: str,
1908
+ reason: str,
1909
+ ) -> bool:
1910
+ pending = _load_claude_pending_permission(store, payload, artifact)
1911
+ if pending is None:
1912
+ return False
1913
+ now = _now()
1914
+ saved_policy = _persist_claude_native_permission_policy(
1915
+ store=store,
1916
+ artifact_id=artifact.artifact_id,
1917
+ artifact_hash=artifact_hash,
1918
+ action=action,
1919
+ reason=reason,
1920
+ now=now,
1921
+ )
1922
+ if not saved_policy:
1923
+ return False
1924
+ try:
1925
+ store.record_inventory_artifact(
1926
+ artifact=artifact,
1927
+ artifact_hash=artifact_hash,
1928
+ policy_action="allow" if action == "allow" else "block",
1929
+ changed=False,
1930
+ now=now,
1931
+ approved=action == "allow",
1932
+ )
1933
+ except (OSError, sqlite3.Error):
1934
+ return False
1935
+ session_id = _optional_string(payload.get("session_id"))
1936
+ if session_id is not None:
1937
+ _remove_claude_pending_permission(
1938
+ store,
1939
+ session_id=session_id,
1940
+ pending_key=_claude_pending_permission_state_key(session_id, artifact.artifact_id),
1941
+ )
1942
+ return True
1943
+
1944
+
1945
+ def _persist_claude_pending_permission_denials(store: GuardStore, payload: dict[str, object]) -> int:
1946
+ session_id = _optional_string(payload.get("session_id"))
1947
+ if session_id is None:
1948
+ return 0
1949
+ index_key = _claude_pending_permission_index_key(session_id)
1950
+ try:
1951
+ index_payload = store.get_sync_payload(index_key)
1952
+ except (OSError, sqlite3.Error):
1953
+ return 0
1954
+ if not isinstance(index_payload, list):
1955
+ return 0
1956
+ pending_keys = [str(item) for item in index_payload]
1957
+ processed_keys: list[str] = []
1958
+ denied = 0
1959
+ for pending_key in pending_keys:
1960
+ try:
1961
+ pending = store.get_sync_payload(pending_key)
1962
+ except (OSError, sqlite3.Error):
1963
+ continue
1964
+ if not isinstance(pending, dict):
1965
+ continue
1966
+ artifact_id = _optional_string(pending.get("artifact_id"))
1967
+ artifact_hash_value = _optional_string(pending.get("artifact_hash"))
1968
+ if artifact_id is None or artifact_hash_value is None:
1969
+ continue
1970
+ reason = _optional_string(pending.get("reason")) or "Denied in Claude's native approval prompt."
1971
+ saved_policy = _persist_claude_native_permission_policy(
1972
+ store=store,
1973
+ artifact_id=artifact_id,
1974
+ artifact_hash=artifact_hash_value,
1975
+ action="block",
1976
+ reason=f"Denied in Claude native approval prompt. {reason}",
1977
+ now=_now(),
1978
+ )
1979
+ if not saved_policy:
1980
+ continue
1981
+ processed_keys.append(pending_key)
1982
+ denied += 1
1983
+ if processed_keys:
1984
+ processed_set = set(processed_keys)
1985
+ try:
1986
+ with store._connect() as connection:
1987
+ connection.execute("begin immediate")
1988
+ for pending_key in processed_keys:
1989
+ connection.execute("delete from sync_state where state_key = ?", (pending_key,))
1990
+ row = connection.execute(
1991
+ "select payload_json from sync_state where state_key = ?",
1992
+ (index_key,),
1993
+ ).fetchone()
1994
+ current_keys = _sync_payload_list_from_row(row)
1995
+ remaining_keys = [pending_key for pending_key in current_keys if pending_key not in processed_set]
1996
+ if remaining_keys:
1997
+ connection.execute(
1998
+ """
1999
+ insert into sync_state (state_key, payload_json, updated_at)
2000
+ values (?, ?, ?)
2001
+ on conflict(state_key) do update set
2002
+ payload_json = excluded.payload_json,
2003
+ updated_at = excluded.updated_at
2004
+ """,
2005
+ (index_key, json.dumps(remaining_keys), _now()),
2006
+ )
2007
+ else:
2008
+ connection.execute("delete from sync_state where state_key = ?", (index_key,))
2009
+ except (OSError, sqlite3.Error):
2010
+ return denied
2011
+ return denied
2012
+
2013
+
1716
2014
  def _is_claude_permission_prompt_notification(args: argparse.Namespace, payload: dict[str, object]) -> bool:
1717
2015
  return (
1718
2016
  _canonical_harness_name(args.harness) == "claude-code"
@@ -2484,6 +2782,7 @@ def _hook_runtime_artifact(
2484
2782
  guard_home: Path,
2485
2783
  workspace: Path | None,
2486
2784
  ) -> GuardArtifact | None:
2785
+ harness = _canonical_harness_name(harness)
2487
2786
  event_name = _hook_event_name(payload)
2488
2787
  if event_name == "UserPromptSubmit":
2489
2788
  prompt_text = payload.get("prompt")
@@ -2541,6 +2840,28 @@ def _hook_runtime_artifact(
2541
2840
  )
2542
2841
 
2543
2842
 
2843
+ def _legacy_claude_alias_runtime_artifact(
2844
+ *,
2845
+ artifact: GuardArtifact,
2846
+ requested_harness: str,
2847
+ home_dir: Path,
2848
+ workspace: Path | None,
2849
+ ) -> GuardArtifact | None:
2850
+ if requested_harness == artifact.harness:
2851
+ return None
2852
+ if requested_harness != "claude" or artifact.harness != "claude-code":
2853
+ return None
2854
+ legacy_prefix = "claude-code:"
2855
+ if not artifact.artifact_id.startswith(legacy_prefix):
2856
+ return None
2857
+ return replace(
2858
+ artifact,
2859
+ artifact_id=f"claude:{artifact.artifact_id[len(legacy_prefix) :]}",
2860
+ harness="claude",
2861
+ config_path=str(_runtime_policy_path("claude", home_dir, workspace)),
2862
+ )
2863
+
2864
+
2544
2865
  def _is_copilot_permission_request(payload: dict[str, object]) -> bool:
2545
2866
  for key in ("hook_name", "hook_event_name", "hookEventName"):
2546
2867
  hook_name = payload.get(key)
@@ -1,3 +1,3 @@
1
1
  """Single source of truth for tool version."""
2
2
 
3
- __version__ = "2.0.60"
3
+ __version__ = "2.0.61"
@@ -47,7 +47,7 @@ def _runtime_hook_handlers(payload: dict[str, object]) -> list[dict[str, object]
47
47
  hooks = payload["hooks"]
48
48
  assert isinstance(hooks, dict)
49
49
  handlers: list[dict[str, object]] = []
50
- for key in ("PreToolUse", "PostToolUse", "UserPromptSubmit", "Notification"):
50
+ for key in ("PreToolUse", "PostToolUse", "UserPromptSubmit", "Notification", "Stop"):
51
51
  entries = hooks[key]
52
52
  assert isinstance(entries, list)
53
53
  for entry in entries:
@@ -147,6 +147,7 @@ def test_claude_install_writes_session_start_and_command_hook_schema_and_is_idem
147
147
  post_tool_use = payload["hooks"]["PostToolUse"]
148
148
  prompt_submit = payload["hooks"]["UserPromptSubmit"]
149
149
  notification = payload["hooks"]["Notification"]
150
+ stop = payload["hooks"]["Stop"]
150
151
  assert len(session_start) == 4
151
152
  assert {entry["matcher"] for entry in session_start} == {"startup", "resume", "clear", "compact"}
152
153
  assert all(entry["hooks"][0]["type"] == "command" for entry in session_start)
@@ -165,6 +166,10 @@ def test_claude_install_writes_session_start_and_command_hook_schema_and_is_idem
165
166
  assert notification[0]["matcher"] == "permission_prompt"
166
167
  assert notification[0]["hooks"][0]["type"] == "command"
167
168
  assert notification[0]["hooks"][0]["timeout"] == 10
169
+ assert len(stop) == 1
170
+ assert "matcher" not in stop[0]
171
+ assert stop[0]["hooks"][0]["type"] == "command"
172
+ assert stop[0]["hooks"][0]["timeout"] == 10
168
173
  assert all("url" not in handler for handler in _runtime_hook_handlers(payload))
169
174
 
170
175
 
@@ -238,7 +243,13 @@ def test_claude_install_replaces_legacy_http_guard_hooks(tmp_path):
238
243
  payload = json.loads(settings_path.read_text(encoding="utf-8"))
239
244
  installed_handlers = _runtime_hook_handlers(payload)
240
245
 
241
- assert [handler["type"] for handler in installed_handlers] == ["command", "command", "command", "command"]
246
+ assert [handler["type"] for handler in installed_handlers] == [
247
+ "command",
248
+ "command",
249
+ "command",
250
+ "command",
251
+ "command",
252
+ ]
242
253
  assert all(CLAUDE_GUARD_DAEMON_HOOK_MARKER in str(handler.get("command", "")) for handler in installed_handlers)
243
254
  assert all("url" not in handler for handler in installed_handlers)
244
255
 
@@ -2129,11 +2129,13 @@ args = ["workspace-skill.js", "--changed"]
2129
2129
  assert install_settings_payload["hooks"]["Notification"][0]["hooks"][0]["type"] == "command"
2130
2130
  assert install_settings_payload["hooks"]["Notification"][0]["hooks"][0]["command"] == expected_hook_command
2131
2131
  assert "url" not in install_settings_payload["hooks"]["Notification"][0]["hooks"][0]
2132
+ assert install_settings_payload["hooks"]["Stop"][0]["hooks"][0]["command"] == expected_hook_command
2132
2133
  assert uninstall_rc == 0
2133
2134
  assert uninstall_output["managed_install"]["active"] is False
2134
2135
  assert settings_payload["hooks"]["SessionStart"] == []
2135
2136
  assert settings_payload["hooks"]["PreToolUse"] == []
2136
2137
  assert settings_payload["hooks"]["Notification"] == []
2138
+ assert settings_payload["hooks"]["Stop"] == []
2137
2139
 
2138
2140
  def test_guard_uninstall_handles_non_dict_claude_hook_entries(self, tmp_path, capsys):
2139
2141
  home_dir = tmp_path / "home"
@@ -2222,6 +2224,7 @@ args = ["workspace-skill.js", "--changed"]
2222
2224
  assert payload["hooks"]["PreToolUse"] == []
2223
2225
  assert payload["hooks"]["UserPromptSubmit"] == []
2224
2226
  assert payload["hooks"]["Notification"] == []
2227
+ assert payload["hooks"]["Stop"] == []
2225
2228
 
2226
2229
  def test_guard_install_claude_alias_persists_canonical_managed_install(self, tmp_path, capsys):
2227
2230
  home_dir = tmp_path / "home"
@@ -2352,6 +2355,7 @@ args = ["workspace-skill.js", "--changed"]
2352
2355
  assert len(payload["hooks"]["PostToolUse"]) == 1
2353
2356
  assert len(payload["hooks"]["UserPromptSubmit"]) == 1
2354
2357
  assert len(payload["hooks"]["Notification"]) == 1
2358
+ assert len(payload["hooks"]["Stop"]) == 1
2355
2359
  pretool_hook_commands = [
2356
2360
  hook["command"]
2357
2361
  for hook in payload["hooks"]["PreToolUse"][0]["hooks"]