dotagents-cli 0.9.2__tar.gz → 0.9.3__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 (160) hide show
  1. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/CHANGELOG.md +23 -1
  2. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/PKG-INFO +1 -1
  3. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/docs/guide/commands.md +1 -0
  4. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/docs/guide/overlay-hooks.md +32 -10
  5. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/docs/guide/private-sync.md +0 -2
  6. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/AGENTS.md +11 -4
  7. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/__init__.py +1 -1
  8. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/_overlay_hooks.py +87 -20
  9. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/cli/mcp.py +11 -0
  10. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/cli/overlays.py +41 -9
  11. dotagents_cli-0.9.3/tests/test_install_allows_hooks.py +238 -0
  12. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_overlay_hooks_cli.py +0 -15
  13. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tools/cloud-setup.sh +9 -13
  14. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/.gitignore +0 -0
  15. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/LICENSE +0 -0
  16. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/README.md +0 -0
  17. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/docs/api/agents.md +0 -0
  18. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/docs/api/cli.md +0 -0
  19. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/docs/api/commands.md +0 -0
  20. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/docs/api/context.md +0 -0
  21. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/docs/api/env.md +0 -0
  22. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/docs/api/fs.md +0 -0
  23. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/docs/api/hooks.md +0 -0
  24. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/docs/api/merge.md +0 -0
  25. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/docs/api/overlays.md +0 -0
  26. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/docs/api/reference.md +0 -0
  27. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/docs/api/resources.md +0 -0
  28. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/docs/api/scope.md +0 -0
  29. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/docs/api/skills.md +0 -0
  30. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/docs/api/sources.md +0 -0
  31. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/docs/api/wrappers.md +0 -0
  32. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/docs/changelog.md +0 -0
  33. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/docs/guide/authoring.md +0 -0
  34. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/docs/guide/install.md +0 -0
  35. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/docs/guide/overlays.md +0 -0
  36. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/docs/index.md +0 -0
  37. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/install.py +0 -0
  38. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/mkdocs.yml +0 -0
  39. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/pyproject.toml +0 -0
  40. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/__main__.py +0 -0
  41. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/_activate.py +0 -0
  42. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/_agents.py +0 -0
  43. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/_aos.py +0 -0
  44. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/_context.py +0 -0
  45. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/_deps.py +0 -0
  46. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/_env.py +0 -0
  47. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/_fs.py +0 -0
  48. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/_hook_dispatch.py +0 -0
  49. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/_hook_main.py +0 -0
  50. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/_hooks.py +0 -0
  51. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/_launchers.py +0 -0
  52. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/_mcp_gateway.py +0 -0
  53. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/_mcp_gateway_clients.py +0 -0
  54. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/_mcp_gateway_config.py +0 -0
  55. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/_merge.py +0 -0
  56. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/_overlay/dotagents/cmds/findings.py +0 -0
  57. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/_overlay/dotagents/cmds/launch.py +0 -0
  58. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/_overlay/dotagents/hooks/overlay_hook.py +0 -0
  59. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/_overlay/dotagents/hooks/preinvocation_antigravity_context.py +0 -0
  60. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/_overlay/dotagents/hooks/pretooluse_codex_env.py +0 -0
  61. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/_overlay/dotagents/hooks/sessionstart_codex_context.py +0 -0
  62. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/_overlay/dotagents/templates/AGENTS.md +0 -0
  63. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/_overlay/dotagents/templates/PROJECT.md +0 -0
  64. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/_overlays.py +0 -0
  65. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/_pipwheel.py +0 -0
  66. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/_policy.py +0 -0
  67. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/_pyvenv.py +0 -0
  68. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/_resources.py +0 -0
  69. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/_run.py +0 -0
  70. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/_scope.py +0 -0
  71. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/_skills.py +0 -0
  72. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/_sources.py +0 -0
  73. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/_wrappers.py +0 -0
  74. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/cli/__init__.py +0 -0
  75. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/cli/_common.py +0 -0
  76. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/cli/about.py +0 -0
  77. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/cli/build_pyz.py +0 -0
  78. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/cli/context.py +0 -0
  79. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/cli/env.py +0 -0
  80. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/cli/hook.py +0 -0
  81. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/cli/init.py +0 -0
  82. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/cli/path.py +0 -0
  83. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/src/dotagents/py.typed +0 -0
  84. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/_helpers.py +0 -0
  85. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/_hook_helpers.py +0 -0
  86. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/_shell.py +0 -0
  87. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/aos_vectors/README.md +0 -0
  88. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/aos_vectors/REVISION +0 -0
  89. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/aos_vectors/compose.json +0 -0
  90. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/aos_vectors/digest.json +0 -0
  91. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/aos_vectors/effective.json +0 -0
  92. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/aos_vectors/hooks.json +0 -0
  93. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/aos_vectors/hooks_compose.json +0 -0
  94. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/aos_vectors/hooks_validation.json +0 -0
  95. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/aos_vectors/names.json +0 -0
  96. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/aos_vectors/reading.json +0 -0
  97. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/aos_vectors/source.json +0 -0
  98. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/aos_vectors/validation.json +0 -0
  99. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/conftest.py +0 -0
  100. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_about.py +0 -0
  101. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_agents.py +0 -0
  102. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_aos_consumer.py +0 -0
  103. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_aos_hook_vectors.py +0 -0
  104. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_aos_vectors.py +0 -0
  105. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_api_header.py +0 -0
  106. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_audit_leak.py +0 -0
  107. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_build_pyz.py +0 -0
  108. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_cli_main.py +0 -0
  109. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_cloud_setup.py +0 -0
  110. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_cmds_discovery.py +0 -0
  111. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_context.py +0 -0
  112. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_deps.py +0 -0
  113. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_env.py +0 -0
  114. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_env_cli_scope.py +0 -0
  115. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_env_format.py +0 -0
  116. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_env_into.py +0 -0
  117. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_env_regressions.py +0 -0
  118. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_env_safety.py +0 -0
  119. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_findings_cmd.py +0 -0
  120. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_fix_adapters.py +0 -0
  121. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_fix_cli.py +0 -0
  122. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_fix_env.py +0 -0
  123. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_fix_hooks.py +0 -0
  124. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_fix_init.py +0 -0
  125. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_fix_overlays.py +0 -0
  126. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_fix_packaging.py +0 -0
  127. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_fix_scope_context.py +0 -0
  128. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_fix_skills_adapters.py +0 -0
  129. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_git_ssl_revoke.py +0 -0
  130. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_hooks.py +0 -0
  131. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_init_base.py +0 -0
  132. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_last_mile.py +0 -0
  133. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_launch_cmd.py +0 -0
  134. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_launchers.py +0 -0
  135. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_local_additions.py +0 -0
  136. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_mcp.py +0 -0
  137. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_no_internal_labels.py +0 -0
  138. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_overlay_hook_forwarder.py +0 -0
  139. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_overlay_hook_wiring.py +0 -0
  140. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_overlay_hooks.py +0 -0
  141. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_overlay_hooks_probes.py +0 -0
  142. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_overlay_hooks_security.py +0 -0
  143. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_overlay_hooks_snapshot.py +0 -0
  144. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_overlay_libs.py +0 -0
  145. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_overlays_cli.py +0 -0
  146. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_overlays_command.py +0 -0
  147. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_path_timeout_frontmatter.py +0 -0
  148. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_pipwheel.py +0 -0
  149. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_powershell_env_hook.py +0 -0
  150. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_pre_env_hook.py +0 -0
  151. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_pyvenv_core.py +0 -0
  152. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_quoting_injection_safety.py +0 -0
  153. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_run_hook.py +0 -0
  154. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_scope.py +0 -0
  155. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_sources.py +0 -0
  156. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_sources_uri.py +0 -0
  157. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_wire_hooks.py +0 -0
  158. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tests/test_wrappers.py +0 -0
  159. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tools/audit.py +0 -0
  160. {dotagents_cli-0.9.2 → dotagents_cli-0.9.3}/tools/pyz_smoke.sh +0 -0
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.9.3] - 2026-11-11
11
+
12
+ ### Changed
13
+
14
+ - Installing an overlay allows its hooks: `overlays add` and `overlays sync` allow the overlay's hooks
15
+ at the digest they install, for that store, and log which are on (event and script). Before, hooks
16
+ stayed off until `overlays allow-hooks` at a terminal. `--no-hooks` on both installs without
17
+ allowing, and a sync then leaves them off. A sync that changes the digest allows the new one, so
18
+ an overlay installed by 0.9.2 with its hooks off is allowed by its next `overlays sync`. A
19
+ `revoke-hooks` is remembered: syncs keep the hooks off until `allow-hooks` or a fresh `add`. An
20
+ install through the default `dotagents mcp` server allows like the command line. `allow-hooks` stays
21
+ (not an MCP tool, asks at the terminal) for overlays installed with `--no-hooks` or revoked.
22
+ `tools/cloud-setup.sh` no longer has a separate allow step.
23
+
24
+ ### Added
25
+
26
+ - `dotagents mcp --strict`, and the user-store setting `overlays/.dotagents-state/mcp.json`
27
+ `{"strict": true}` (read only from the user store, for a server started by `DOTAGENTS_MCP`): an
28
+ `overlays add` or `sync` through a strict MCP server installs without allowing hooks, says they are
29
+ off and that `allow-hooks` turns them on, and writes no allowance.
30
+
10
31
  ## [0.9.2] - 2026-11-11
11
32
 
12
33
  ### Added
@@ -1793,7 +1814,8 @@ Patch release: the PATH/POSIX-conversion fix above (the only change since 0.3.0)
1793
1814
  affected module sources (`dotagents.cli`, `duho.presets`) to extracted temp files
1794
1815
  before dispatch; a no-op for a plain install.
1795
1816
 
1796
- [Unreleased]: https://github.com/jose-pr/dotagents/compare/v0.9.2...HEAD
1817
+ [Unreleased]: https://github.com/jose-pr/dotagents/compare/v0.9.3...HEAD
1818
+ [0.9.3]: https://github.com/jose-pr/dotagents/compare/v0.9.2...v0.9.3
1797
1819
  [0.9.2]: https://github.com/jose-pr/dotagents/compare/v0.9.1...v0.9.2
1798
1820
  [0.9.1]: https://github.com/jose-pr/dotagents/compare/v0.9.0...v0.9.1
1799
1821
  [0.9.0]: https://github.com/jose-pr/dotagents/compare/v0.8.3...v0.9.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: dotagents-cli
3
- Version: 0.9.2
3
+ Version: 0.9.3
4
4
  Summary: The dotagents CLI: install and manage a portable ~/.agents config for AI coding agents
5
5
  Project-URL: Homepage, https://github.com/jose-pr/dotagents/
6
6
  Project-URL: Repository, https://github.com/jose-pr/dotagents
@@ -217,6 +217,7 @@ dotagents overlays list # installed (discovered) + available
217
217
  dotagents overlays sync 'py*' # refresh installed overlays matching a glob
218
218
  dotagents overlays remove python # delete the overlay dir, unpublish its skills, un-merge its rules
219
219
  dotagents overlays show python # describe one: declarations, digest, pin, weakenings, source (--json)
220
+ dotagents mcp --strict # serve MCP tools; an add/sync through it allows no hooks
220
221
  dotagents overlays hooks private-sync # its hooks: what runs where, and whether they are allowed
221
222
  dotagents overlays allow-hooks private-sync --digest sha256:... # allow them at that digest (see overlay-hooks)
222
223
  ```
@@ -27,25 +27,47 @@ event takes, and answers the harness itself, in the harness's words. A `decision
27
27
  fails (`grant`) and nothing of it applies. dotagents never states an approval and never rewrites
28
28
  a tool's input.
29
29
 
30
- ## Off until you allow them
30
+ ## Allowed by installing
31
31
 
32
32
  A hook is the overlay's code, running at every session with your access, outside the harness's
33
- sandbox. An overlay's hooks do not run until you allow them at the overlay's digest:
33
+ sandbox. Installing an overlay is choosing to trust it, hooks included: `overlays add` and
34
+ `overlays sync` allow the overlay's hooks at the digest they install and log which are on (event and
35
+ script). A sync that changes the digest allows the new one, since you chose the source.
34
36
 
35
37
  ```
38
+ dotagents overlays add NAME [--no-hooks] # installs, and allows the hooks unless --no-hooks
39
+ dotagents overlays sync [NAME] [--no-hooks]
36
40
  dotagents overlays hooks NAME # what runs, where, and whether it is allowed
37
- dotagents overlays allow-hooks NAME # shows the same and the digest
41
+ dotagents overlays revoke-hooks NAME # off, and a sync keeps them off
42
+ dotagents overlays allow-hooks NAME # shows the same and the digest, then asks at the terminal
38
43
  dotagents overlays allow-hooks NAME --digest sha256:...
39
- dotagents overlays revoke-hooks NAME
40
44
  ```
41
45
 
46
+ `--no-hooks` installs without allowing them, and a later `sync` leaves them off (it is remembered
47
+ like a revoke). `revoke-hooks` turns them off and a `sync` keeps them off until `allow-hooks` or a
48
+ fresh `overlays add`. An overlay installed by 0.9.2, whose hooks were off, is allowed by its next
49
+ `overlays sync`, unless it was revoked.
50
+
51
+ Hooks run **outside the agent's sandbox**, with your access. An agent that can write the project's
52
+ `.claude/` (its own `settings.local.json`) or your user store can run code outside its sandbox
53
+ anyway, so a hook is nothing it could not already add: installing an overlay is the trust decision,
54
+ and `dotagents overlays add` and `sync` make it once. The default `dotagents mcp` server behaves as
55
+ the command line does, so an add or sync an agent asks for through it allows hooks too. Start it
56
+ **`dotagents mcp --strict`** (or put `{"strict": true}` in `overlays/.dotagents-state/mcp.json` of
57
+ your user store, which a server started by `DOTAGENTS_MCP` reads) for a stricter server: an add or sync
58
+ through it installs without allowing hooks, says the hooks are off and that `allow-hooks` at a terminal
59
+ turns them on, and records nothing (what you allowed before stays allowed at its digest). The setting
60
+ is read from the user store only; a project's files cannot make a server strict or lax.
61
+
42
62
  The allowance is kept in your user store (`overlays/.dotagents-state/hooks-allowed.json`), never
43
- in an overlay or a project, per overlay and store. There is one digest per overlay: any change to
44
- the overlay turns its hooks off until you allow the new one, and going back to an earlier digest
45
- needs a new yes. `allow-hooks` asks at the terminal itself (`/dev/tty`, `CONIN$`), refuses to run
46
- inside an agent harness's session and is not an MCP tool; `--no-prompt` is for your own setup
47
- script. The allowance is only as strong as the agent's lack of write access to your user store:
48
- an agent that can write there can write an allowance.
63
+ in an overlay or a project, per overlay and store. There is one digest per overlay: a change to the
64
+ overlay that did not come through `overlays sync` or `add` (a hand edit, an update by other means)
65
+ turns its hooks off until you allow the new digest, and going back to an earlier digest needs a new
66
+ yes. `allow-hooks` asks at the terminal itself (`/dev/tty`, `CONIN$`), refuses to run inside an
67
+ agent harness's session and is not an MCP tool; `--no-prompt` is for your own setup script. The
68
+ allowance is only as strong as the agent's lack of write access to your user store: an agent that
69
+ can write there can write an allowance, and one that can run `dotagents overlays add` through a shell
70
+ tool installs and allows as you would.
49
71
 
50
72
  An overlay with a hook that has `on_failure = "refuse"` (a guard) is **not activated** while its
51
73
  hooks are off, and its guards **fail** meanwhile: a prompt is blocked, a tool call denied, as if
@@ -85,8 +85,6 @@ them:
85
85
 
86
86
  ```bash
87
87
  dotagents overlays hooks private-sync # what runs, on which harness, and whether it is allowed
88
- dotagents overlays allow-hooks private-sync # shows the same and the digest to allow
89
- dotagents overlays allow-hooks private-sync --digest sha256:<the digest shown>
90
88
  dotagents init -g
91
89
  ```
92
90
 
@@ -53,6 +53,7 @@ and an error saying a command is required, and exits 2.
53
53
  | `overlays list [-g] [--repo SPEC]... [--json]` | Installed overlays per store (shadowed copies marked, unmet `requires` and refused overlays flagged) and what the repos offer. |
54
54
  | `overlays show NAME [-g] [--repo SPEC]... [--json]` | One overlay: the copy a session would use, else the source's; its declarations, a refused manifest's problems, tree digest, pin, what this machine lacks for it, its weakenings, skills, member count, recorded source and commit. No name: exit 2. |
55
55
  | `overlays hooks [NAME] [-g]` | An installed overlay's hooks (every overlay with some, without a name): per event the script and its settings, whether the hooks are allowed at the overlay's current digest, and per harness whether the event fires natively, is fired by dotagents itself (`pre-env`) or cannot fire. |
56
+ | `overlays add` / `overlays sync` `[--no-hooks]` | Besides installing, allow the overlay's hooks at the digest installed and log which are on (event, script); `--no-hooks` installs without allowing and remembers it. A sync that changes the digest allows the new one; hooks `revoke-hooks` turned off stay off across syncs until `allow-hooks` or a fresh `add`; an install made through the MCP tool allows nothing and says so. |
56
57
  | `overlays allow-hooks NAME [--digest D] [--no-prompt] [-g]` | Show the same, then allow the overlay's hooks at digest `D`, which must be the current one: without `--digest` it prints the digest to pass and allows nothing (exit 0); a wrong digest exits 2. Then it asks at the terminal itself (`/dev/tty`, `CONIN$`): `allow NAME` to confirm; `--no-prompt` is for the user's own setup script. It refuses (exit 2) inside an agent harness's session and is not an MCP tool. The allowance is kept in the user store (`overlays/.dotagents-state/hooks-allowed.json`), one digest per overlay and store, and is only as strong as an agent's lack of write access to the user store. |
57
58
  | `overlays revoke-hooks NAME [-g]` | Take back the allowance. |
58
59
  | `hook EVENT --harness claude-code\|codex [--level user\|project] [--guard]` | Not for typing: what a harness's own hook entry runs (through `overlay_hook.py`). Reads the harness's payload on standard input, fires the overlays' hooks for the event (`user`: the user and system stores' overlays, `project`: the project store's), and answers in the harness's words; a hook's output never reaches the harness and the answer is never an approval. `--guard`: a failure of this program answers with a refusal where the event can be refused. Always exits 0 (2 for a bad argument). Not an MCP tool. |
@@ -63,7 +64,7 @@ and an error saying a command is required, and exits 2.
63
64
  | `findings add\|list\|show\|done\|reopen\|remove\|index\|path [-g] [--dir D]` | The findings queue at `<store>/findings/` (`--dir` overrides): one markdown file per finding, `done` moves it to `processed/` with a required resolution, `INDEX.md` regenerated on every change. The frontmatter is valid YAML (a value is double-quoted when it must be); `index --requote` fixes values written bare in existing files, touching nothing else. |
64
65
  | `about [--json]` | `dotagents-cli <version>`, then `<distribution> <version>` per bundled (`.pyz`) or installed package. |
65
66
  | `build-pyz [--out P] [--python SHEBANG] [--duho-version V] [--pathapi-version V] [--extras a,b] [--with-pip]` | Build a zipapp from a source checkout; errors elsewhere. `--with-pip` vendors the pinned pip wheel (`_pipwheel`) as `dotagents/_vendor/`. |
66
- | `mcp [--prefix TEMPLATE]` | Serve command and active overlay MCP tools over stdio; `{name}` in the template is the skill or server name. |
67
+ | `mcp [--prefix TEMPLATE] [--strict]` | Serve command and active overlay MCP tools over stdio (`--strict`, or the user store's `mcp.json` `{"strict": true}`: an `overlays add`/`sync` through it installs without allowing hooks); `{name}` in the template is the skill or server name. |
67
68
 
68
69
  **Every command but `env` and `mcp` is also an MCP tool.** `dotagents mcp` (or
69
70
  `DOTAGENTS_MCP=stdio dotagents`, the same server) serves the command set — built-in,
@@ -419,7 +420,7 @@ returns the exit code.
419
420
  `bundle_manifest() -> Optional[dict]`, `installed_packages() -> dict[str, str]`,
420
421
  `packages() -> tuple[dict[str, str], Optional[dict]]` (the bundle's packages, else
421
422
  the installed ones; the CLI itself in neither).
422
- - `mcp.Mcp` — `prefix: Optional[str]`; exits 2 when the template uses a placeholder
423
+ - `mcp.Mcp` — `prefix: Optional[str]`, `strict: bool` (installs through it allow no hooks); exits 2 when the template uses a placeholder
423
424
  other than `{name}` or does not format to a tool-name segment.
424
425
  - `build_pyz`: `EXTRA_REQUIREMENTS` — `{extra: (requirement, ...)}`, what each
425
426
  dotagents-cli extra installs (`uri`, `http`, `mcp-http`, `sftp`, `s3`, `repo`),
@@ -976,8 +977,14 @@ its directory, bounded by `LIMITS`.
976
977
  - Allowances, in the user store only, keyed by the overlay's store and name:
977
978
  `allowances_path(user_store)`, `read_allowances`, `allowed_digest(user_store, identity,
978
979
  store=None)`, `allow(user_store, identity, digest, store=None)`, `revoke(...) -> bool`.
979
- - `install_notice(overlay, user_store) -> Optional[str]` — the line `overlays add/sync` logs
980
- when an overlay has hooks that are off;
980
+ - Installing allows: `record_install(user_store, overlay, *, fresh, no_hooks, dry_run=False) ->
981
+ (outcome, entries)` is what `overlays add` (`fresh=True`) and `overlays sync` do to the allowance
982
+ of the installed copy: `on` (allowed at the digest installed; a fresh add also forgets a revoke),
983
+ `declined` (`--no-hooks`, remembered), `strict` (`strict_mcp(user_store)`: a server started `dotagents mcp --strict`, or by `DOTAGENTS_MCP`
984
+ with the user's `mcp.json` `{"strict": true}`, installs without allowing and writes nothing; the
985
+ default server behaves as the command line; `mcp_settings_path`, `mcp_strict_setting` read the user
986
+ store only; `_MCP_STRICT` is the process flag), `revoked` (kept off across syncs), `none`; `read_revoked`,
987
+ `is_revoked`; `revoke` also records the revoke (the allowance file holds `allowed` and `revoked`).
981
988
  - `read_tree(overlay, keep=False) -> (digest, files)` (the bytes the digest covers when `keep`),
982
989
  `snapshot(user_store, overlay, digest, files) -> Path` — the copy a hook runs from, one directory
983
990
  per digest under `<user store>/.cache/hook-snapshots/` made from exactly the bytes hashed (what
@@ -1,3 +1,3 @@
1
1
  """dotagents: installable CLI for the dotagents agent-config payload."""
2
2
 
3
- __version__ = "0.9.2"
3
+ __version__ = "0.9.3"
@@ -195,12 +195,32 @@ def allowed_digest(user_store: "str | os.PathLike[str]", identity: str,
195
195
  return read_allowances(user_store).get(_key(user_store, identity, store))
196
196
 
197
197
 
198
- def _write_allowances(user_store: "str | os.PathLike[str]", allowed: "dict[str, str]") -> None:
198
+ def read_revoked(user_store: "str | os.PathLike[str]") -> "set[str]":
199
+ """The overlays (by store and name) whose hooks the user turned off or declined at install: a
200
+ later sync does not allow them again; `allow-hooks` or a fresh `overlays add` does."""
201
+ try:
202
+ data = json.loads(allowances_path(user_store).read_text(encoding="utf-8"))
203
+ except (OSError, ValueError):
204
+ return set()
205
+ revoked = data.get("revoked") if isinstance(data, dict) else None
206
+ return {str(item) for item in revoked} if isinstance(revoked, list) else set()
207
+
208
+
209
+ def _write_allowances(user_store: "str | os.PathLike[str]", allowed: "dict[str, str]",
210
+ revoked: "Optional[set[str]]" = None) -> None:
199
211
  from dotagents._fs import write_text_lf
200
212
 
201
213
  path = allowances_path(user_store)
202
214
  path.parent.mkdir(parents=True, exist_ok=True)
203
- write_text_lf(path, json.dumps({"allowed": dict(sorted(allowed.items()))}, indent=2) + "\n", atomic=True)
215
+ if revoked is None:
216
+ revoked = read_revoked(user_store)
217
+ write_text_lf(path, json.dumps({"allowed": dict(sorted(allowed.items())), "revoked": sorted(revoked)},
218
+ indent=2) + "\n", atomic=True)
219
+
220
+
221
+ def is_revoked(user_store: "str | os.PathLike[str]", identity: str,
222
+ store: "Optional[str | os.PathLike[str]]" = None) -> bool:
223
+ return _key(user_store, identity, store) in read_revoked(user_store)
204
224
 
205
225
 
206
226
  def allow(user_store: "str | os.PathLike[str]", identity: str, digest: str,
@@ -208,19 +228,80 @@ def allow(user_store: "str | os.PathLike[str]", identity: str, digest: str,
208
228
  """Allow ``identity``'s hooks (of the overlay installed in ``store``, default the user store) at
209
229
  ``digest``, replacing the digest allowed before: there is one."""
210
230
  allowed = read_allowances(user_store)
211
- allowed[_key(user_store, identity, store)] = digest.lower()
212
- _write_allowances(user_store, allowed)
231
+ key = _key(user_store, identity, store)
232
+ allowed[key] = digest.lower()
233
+ _write_allowances(user_store, allowed, read_revoked(user_store) - {key})
213
234
 
214
235
 
215
236
  def revoke(user_store: "str | os.PathLike[str]", identity: str,
216
237
  store: "Optional[str | os.PathLike[str]]" = None) -> bool:
238
+ """Turn the overlay's hooks off and keep them off across syncs. True when that changed anything."""
217
239
  allowed = read_allowances(user_store)
218
- if allowed.pop(_key(user_store, identity, store), None) is None:
240
+ key = _key(user_store, identity, store)
241
+ revoked = read_revoked(user_store)
242
+ if allowed.pop(key, None) is None and key in revoked:
219
243
  return False
220
- _write_allowances(user_store, allowed)
244
+ _write_allowances(user_store, allowed, revoked | {key})
221
245
  return True
222
246
 
223
247
 
248
+ def mcp_settings_path(user_store: "str | os.PathLike[str]") -> Path:
249
+ return Path(user_store) / "overlays" / ".dotagents-state" / "mcp.json"
250
+
251
+
252
+ def mcp_strict_setting(user_store: "str | os.PathLike[str]") -> bool:
253
+ """The user's own setting ``{"strict": true}`` for the MCP server, read from the user store only
254
+ (never from a project, whose MCP configuration an agent can write)."""
255
+ try:
256
+ data = json.loads(mcp_settings_path(user_store).read_text(encoding="utf-8"))
257
+ except (OSError, ValueError):
258
+ return False
259
+ return isinstance(data, dict) and data.get("strict") is True
260
+
261
+
262
+ #: Set by `dotagents mcp --strict` (or the user's setting): installs through this server allow no hooks.
263
+ _MCP_STRICT: "list[bool]" = []
264
+
265
+
266
+ def strict_mcp(user_store: "str | os.PathLike[str]") -> bool:
267
+ """Whether this process serves MCP tools in strict mode, chosen when the server was started: an
268
+ add or sync through it installs without allowing hooks, as ``--no-hooks`` does. The default server
269
+ behaves as the command line does."""
270
+ return bool(_MCP_STRICT) or (bool(os.environ.get("DOTAGENTS_MCP")) and mcp_strict_setting(user_store))
271
+
272
+
273
+ def record_install(user_store: "str | os.PathLike[str]", overlay: Any, *, fresh: bool, no_hooks: bool,
274
+ dry_run: bool = False) -> "tuple[str, list[dict[str, Any]]]":
275
+ """What installing or syncing ``overlay`` (the installed copy) does to its hooks' allowance.
276
+ Returns ``(outcome, entries)``: ``none`` (it has no hooks), or ``on`` (allowed at the digest it
277
+ holds, which is the digest installed), ``declined`` (``--no-hooks``: recorded so a later sync
278
+ does not allow them), ``strict`` (installed through a strict MCP server: nothing is allowed or recorded),
279
+ ``revoked`` (turned off earlier and kept off by a sync). A fresh add allows, and forgets an
280
+ earlier revoke. With ``dry_run`` nothing is written."""
281
+ plan = overlay_plan(overlay, ANYWHERE, user_store)
282
+ entries = plan["hooks"]
283
+ if not entries:
284
+ return "none", []
285
+ key = _key(user_store, overlay.name, overlay.store)
286
+ if strict_mcp(user_store) and not no_hooks:
287
+ # Installs without allowing and writes nothing: what was allowed stays allowed at its own digest.
288
+ return "strict", entries
289
+ if no_hooks:
290
+ if not dry_run and plan["digest"] and key not in read_revoked(user_store):
291
+ allowed = read_allowances(user_store)
292
+ allowed.pop(key, None)
293
+ _write_allowances(user_store, allowed, read_revoked(user_store) | {key})
294
+ return "declined", entries
295
+ if not fresh and key in read_revoked(user_store):
296
+ return "revoked", entries
297
+ if plan["digest"] is None:
298
+ return "declined", entries
299
+ if not dry_run:
300
+ allow(user_store, overlay.name, plan["digest"], overlay.store)
301
+ return "on", entries
302
+
303
+
304
+
224
305
  # --------------------------------------------------------------------------- #
225
306
  # What an overlay's hooks are on this machine, for a harness.
226
307
  # --------------------------------------------------------------------------- #
@@ -357,20 +438,6 @@ def _token() -> str:
357
438
  return secrets.token_hex(4)
358
439
 
359
440
 
360
- def install_notice(overlay: Any, user_store: "str | os.PathLike[str]") -> "Optional[str]":
361
- """A line for an install or sync: the overlay has hooks and they are off until the user allows
362
- them. None when it has none, or they are allowed at this digest."""
363
- from dotagents import _activate
364
-
365
- entries, _ = _aos.hooks_of(overlay.document(), _activate.files(overlay), _aos.current_os(),
366
- sorted(_activate.interpreters()))
367
- if not entries or overlay_plan(overlay, ANYWHERE, user_store)["on"]:
368
- return None
369
- return ("overlay %s has hooks (%s); they are off until you allow them at its digest: "
370
- "dotagents overlays hooks %s" % (overlay.name, ", ".join(entry["event"] for entry in entries),
371
- overlay.name))
372
-
373
-
374
441
  def declares_guard(overlay: Any) -> bool:
375
442
  """Whether the overlay has a hook with ``on_failure = "refuse"``."""
376
443
  from dotagents import _activate
@@ -19,7 +19,18 @@ class Mcp(LoggingArgs, Cmd):
19
19
  "Name prefix template for forwarded tools; `{name}` is the skill or server name."
20
20
  ("--prefix",)
21
21
 
22
+ strict: bool = False
23
+ "Strict mode: an add or sync through this server installs without allowing the overlay's hooks."
24
+ ("--strict",)
25
+
22
26
  def __call__(self) -> int:
27
+ from dotagents import _overlay_hooks
28
+ from dotagents._scope import resolve_user_store
29
+
30
+ # Chosen when the server starts: by the flag, or by the user's own setting in the user store
31
+ # (never a project's: an agent can write a project's MCP configuration).
32
+ if self.strict or _overlay_hooks.mcp_strict_setting(resolve_user_store()):
33
+ _overlay_hooks._MCP_STRICT.append(True)
23
34
  if self.prefix is not None:
24
35
  try:
25
36
  sample = self.prefix.format(name="server")
@@ -241,12 +241,6 @@ def _prepare(scope, dest_dir: Path, name: str, *, change: str, no_setup=False, d
241
241
  except _aos.Refused as refused:
242
242
  logger.error("%s: %s %s (hash every requirement or none)", name, refused.code, refused.subject)
243
243
  return 1
244
- if change != "remove":
245
- from dotagents import _overlay_hooks
246
-
247
- note = _overlay_hooks.install_notice(overlay, scope.user_root)
248
- if note:
249
- logger.warning("%s", note)
250
244
  if variant is None and change != "remove":
251
245
  # A default setup.<ext> present that this machine cannot run.
252
246
  plan = _aos.plan(overlay.document(), _activate.context(overlay), pinned=True)
@@ -543,9 +537,34 @@ def _log_install(logger, verb: str, name: str, result, dry_run: bool) -> None:
543
537
  logger.info("overlay %s: backed up %s", name, ", ".join(result.backed_up))
544
538
 
545
539
 
540
+ def _record_hooks(scope, dest: Path, name: str, *, fresh: bool, no_hooks: bool, dry_run: bool, logger) -> None:
541
+ """Installing or syncing an overlay allows its hooks at the digest installed (the person chose
542
+ this source, and the overlay declares them), and says which are on. ``--no-hooks`` installs
543
+ without allowing and remembers it; an overlay whose hooks were revoked stays off across syncs;
544
+ a request through the MCP tool allows like any other, unless the server was started `--strict`
545
+ (or the user's own setting says so), when it installs without allowing."""
546
+ from dotagents import _overlay_hooks
547
+ from dotagents._overlays import Overlay
548
+
549
+ overlay = Overlay(dest if dest.is_dir() else dest, scope.agents_root)
550
+ outcome, entries = _overlay_hooks.record_install(scope.user_root, overlay, fresh=fresh, no_hooks=no_hooks,
551
+ dry_run=dry_run)
552
+ if outcome == "none":
553
+ return
554
+ listed = ", ".join("%s (%s)" % (e["event"], e["path"] or "no hook.<ext> can run here") for e in entries)
555
+ if outcome == "on":
556
+ logger.info("overlay %s: %s its hooks, now on: %s (dotagents overlays hooks %s; revoke-hooks turns them off)",
557
+ name, "would allow" if dry_run else "allowed", listed, name)
558
+ else:
559
+ why = {"declined": "--no-hooks", "revoked": "they were revoked and a sync keeps them off",
560
+ "strict": "installed through `dotagents mcp --strict`, which allows no hooks"}[outcome]
561
+ logger.warning("overlay %s: its hooks are off (%s): %s -- allow them with `dotagents overlays "
562
+ "allow-hooks %s`", name, why, listed, name)
563
+
564
+
546
565
  def _install_one(scope, name: str, src: Path, origin, *, copy: bool, no_setup: bool,
547
566
  dry_run: bool, logger, setup_timeout: "Optional[int]" = None,
548
- no_venv: bool = False, cache=None) -> "list[str]":
567
+ no_venv: bool = False, cache=None, no_hooks: bool = False) -> "list[str]":
549
568
  """Install (or re-install) overlay ``name`` from ``src`` into the scope,
550
569
  publish its skills, install its Python dependencies into the scope's venv
551
570
  and run its setup (``install`` the first time, then ``sync``). Nothing is written for a source
@@ -634,6 +653,8 @@ def _install_one(scope, name: str, src: Path, origin, *, copy: bool, no_setup: b
634
653
  "was last prepared for (fix it and run `overlays sync %s`)", what, name, name,
635
654
  )
636
655
  raise SystemExit("error: %s for overlay %r failed (exit %d)" % (what, name, rc))
656
+ _record_hooks(scope, dest if dest.is_dir() else src, name, fresh=True, no_hooks=no_hooks, dry_run=dry_run,
657
+ logger=logger)
637
658
  return skills
638
659
 
639
660
 
@@ -737,6 +758,10 @@ class OverlayAdd(_RepoArgs):
737
758
  "Skip running an overlay's declared `setup` after install."
738
759
  ("--no-setup",)
739
760
 
761
+ no_hooks: bool = False
762
+ "Do not allow the overlay's hooks (installing allows them at the digest installed; `allow-hooks` does it later)."
763
+ ("--no-hooks",)
764
+
740
765
  setup_timeout: Optional[int] = None
741
766
  "Seconds setup may run (default: the setup variant's timeout, else 300; 0 = no limit)."
742
767
  ("--setup-timeout",)
@@ -805,7 +830,7 @@ class OverlayAdd(_RepoArgs):
805
830
  scope, name, src, origin, copy=self.copy, no_setup=self.no_setup,
806
831
  dry_run=self.dry_run, logger=self._logger_,
807
832
  setup_timeout=self.setup_timeout, no_venv=self.no_venv,
808
- cache=source.cache,
833
+ cache=source.cache, no_hooks=self.no_hooks,
809
834
  )
810
835
  except (SystemExit, OSError) as exc:
811
836
  why = str(exc.code if isinstance(exc, SystemExit) else exc)
@@ -1273,6 +1298,10 @@ class OverlaySync(_RepoArgs):
1273
1298
  "Skip running each overlay's declared `setup` after sync."
1274
1299
  ("--no-setup",)
1275
1300
 
1301
+ no_hooks: bool = False
1302
+ "Do not allow the overlay's hooks (installing allows them at the digest installed; `allow-hooks` does it later)."
1303
+ ("--no-hooks",)
1304
+
1276
1305
  no_venv: bool = False
1277
1306
  "Do not create or update the scope's Python venv (overlay requirements, duho, the .pth layering)."
1278
1307
  ("--no-venv",)
@@ -1401,6 +1430,9 @@ class OverlaySync(_RepoArgs):
1401
1430
  self._logger_.error("preparing overlay %s failed (exit %d); it is activated only for the "
1402
1431
  "digest it was last prepared for", name, rc)
1403
1432
  failures.append(name)
1433
+ else:
1434
+ _record_hooks(scope, dest_dir, name, fresh=False, no_hooks=self.no_hooks,
1435
+ dry_run=self.dry_run, logger=self._logger_)
1404
1436
 
1405
1437
  # A `requires` added upstream: install what no store provides yet.
1406
1438
  have = frozenset(o.normalized_name for o in scope.overlays)
@@ -1425,7 +1457,7 @@ class OverlaySync(_RepoArgs):
1425
1457
  skills += _install_one(
1426
1458
  scope, dep, src, origin, copy=self.copy, no_setup=self.no_setup,
1427
1459
  dry_run=self.dry_run, logger=self._logger_, setup_timeout=self.setup_timeout,
1428
- no_venv=self.no_venv, cache=chain.cache,
1460
+ no_venv=self.no_venv, cache=chain.cache, no_hooks=self.no_hooks,
1429
1461
  )
1430
1462
  finally:
1431
1463
  # Recompose over ALL installed overlays in (priority, name) order