dotagents-cli 0.9.0__tar.gz → 0.9.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/CHANGELOG.md +53 -1
  2. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/PKG-INFO +1 -1
  3. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/docs/guide/commands.md +8 -4
  4. dotagents_cli-0.9.2/docs/guide/overlay-hooks.md +213 -0
  5. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/docs/guide/private-sync.md +14 -6
  6. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/mkdocs.yml +1 -0
  7. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/AGENTS.md +150 -4
  8. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/__init__.py +1 -1
  9. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/_activate.py +5 -0
  10. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/_agents.py +187 -15
  11. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/_aos.py +483 -6
  12. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/_env.py +101 -8
  13. dotagents_cli-0.9.2/src/dotagents/_hook_dispatch.py +315 -0
  14. dotagents_cli-0.9.2/src/dotagents/_hook_main.py +55 -0
  15. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/_hooks.py +7 -3
  16. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/_overlay/dotagents/cmds/launch.py +20 -2
  17. dotagents_cli-0.9.2/src/dotagents/_overlay/dotagents/hooks/overlay_hook.py +289 -0
  18. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/_overlay/dotagents/hooks/preinvocation_antigravity_context.py +3 -3
  19. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/_overlay/dotagents/hooks/pretooluse_codex_env.py +4 -5
  20. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/_overlay/dotagents/hooks/sessionstart_codex_context.py +5 -5
  21. dotagents_cli-0.9.2/src/dotagents/_overlay_hooks.py +947 -0
  22. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/_run.py +128 -7
  23. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/cli/__init__.py +2 -0
  24. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/cli/env.py +4 -1
  25. dotagents_cli-0.9.2/src/dotagents/cli/hook.py +70 -0
  26. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/cli/init.py +3 -2
  27. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/cli/overlays.py +208 -2
  28. dotagents_cli-0.9.2/tests/_hook_helpers.py +93 -0
  29. dotagents_cli-0.9.2/tests/aos_vectors/REVISION +2 -0
  30. dotagents_cli-0.9.2/tests/aos_vectors/hooks.json +6797 -0
  31. dotagents_cli-0.9.2/tests/aos_vectors/hooks_compose.json +436 -0
  32. dotagents_cli-0.9.2/tests/aos_vectors/hooks_validation.json +660 -0
  33. dotagents_cli-0.9.2/tests/test_aos_hook_vectors.py +147 -0
  34. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_aos_vectors.py +5 -1
  35. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_audit_leak.py +1 -1
  36. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_cmds_discovery.py +1 -1
  37. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_launch_cmd.py +57 -10
  38. dotagents_cli-0.9.2/tests/test_overlay_hook_forwarder.py +286 -0
  39. dotagents_cli-0.9.2/tests/test_overlay_hook_wiring.py +181 -0
  40. dotagents_cli-0.9.2/tests/test_overlay_hooks.py +458 -0
  41. dotagents_cli-0.9.2/tests/test_overlay_hooks_cli.py +136 -0
  42. dotagents_cli-0.9.2/tests/test_overlay_hooks_probes.py +188 -0
  43. dotagents_cli-0.9.2/tests/test_overlay_hooks_security.py +284 -0
  44. dotagents_cli-0.9.2/tests/test_overlay_hooks_snapshot.py +278 -0
  45. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_powershell_env_hook.py +29 -8
  46. dotagents_cli-0.9.2/tests/test_pre_env_hook.py +130 -0
  47. dotagents_cli-0.9.2/tests/test_run_hook.py +94 -0
  48. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_wire_hooks.py +56 -39
  49. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tools/cloud-setup.sh +24 -31
  50. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tools/pyz_smoke.sh +1 -1
  51. dotagents_cli-0.9.0/tests/aos_vectors/REVISION +0 -1
  52. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/.gitignore +0 -0
  53. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/LICENSE +0 -0
  54. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/README.md +0 -0
  55. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/docs/api/agents.md +0 -0
  56. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/docs/api/cli.md +0 -0
  57. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/docs/api/commands.md +0 -0
  58. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/docs/api/context.md +0 -0
  59. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/docs/api/env.md +0 -0
  60. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/docs/api/fs.md +0 -0
  61. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/docs/api/hooks.md +0 -0
  62. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/docs/api/merge.md +0 -0
  63. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/docs/api/overlays.md +0 -0
  64. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/docs/api/reference.md +0 -0
  65. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/docs/api/resources.md +0 -0
  66. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/docs/api/scope.md +0 -0
  67. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/docs/api/skills.md +0 -0
  68. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/docs/api/sources.md +0 -0
  69. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/docs/api/wrappers.md +0 -0
  70. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/docs/changelog.md +0 -0
  71. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/docs/guide/authoring.md +0 -0
  72. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/docs/guide/install.md +0 -0
  73. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/docs/guide/overlays.md +0 -0
  74. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/docs/index.md +0 -0
  75. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/install.py +0 -0
  76. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/pyproject.toml +0 -0
  77. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/__main__.py +0 -0
  78. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/_context.py +0 -0
  79. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/_deps.py +0 -0
  80. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/_fs.py +0 -0
  81. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/_launchers.py +0 -0
  82. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/_mcp_gateway.py +0 -0
  83. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/_mcp_gateway_clients.py +0 -0
  84. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/_mcp_gateway_config.py +0 -0
  85. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/_merge.py +0 -0
  86. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/_overlay/dotagents/cmds/findings.py +0 -0
  87. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/_overlay/dotagents/templates/AGENTS.md +0 -0
  88. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/_overlay/dotagents/templates/PROJECT.md +0 -0
  89. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/_overlays.py +0 -0
  90. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/_pipwheel.py +0 -0
  91. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/_policy.py +0 -0
  92. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/_pyvenv.py +0 -0
  93. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/_resources.py +0 -0
  94. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/_scope.py +0 -0
  95. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/_skills.py +0 -0
  96. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/_sources.py +0 -0
  97. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/_wrappers.py +0 -0
  98. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/cli/_common.py +0 -0
  99. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/cli/about.py +0 -0
  100. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/cli/build_pyz.py +0 -0
  101. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/cli/context.py +0 -0
  102. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/cli/mcp.py +0 -0
  103. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/cli/path.py +0 -0
  104. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/src/dotagents/py.typed +0 -0
  105. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/_helpers.py +0 -0
  106. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/_shell.py +0 -0
  107. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/aos_vectors/README.md +0 -0
  108. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/aos_vectors/compose.json +0 -0
  109. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/aos_vectors/digest.json +0 -0
  110. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/aos_vectors/effective.json +0 -0
  111. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/aos_vectors/names.json +0 -0
  112. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/aos_vectors/reading.json +0 -0
  113. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/aos_vectors/source.json +0 -0
  114. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/aos_vectors/validation.json +0 -0
  115. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/conftest.py +0 -0
  116. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_about.py +0 -0
  117. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_agents.py +0 -0
  118. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_aos_consumer.py +0 -0
  119. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_api_header.py +0 -0
  120. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_build_pyz.py +0 -0
  121. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_cli_main.py +0 -0
  122. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_cloud_setup.py +0 -0
  123. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_context.py +0 -0
  124. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_deps.py +0 -0
  125. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_env.py +0 -0
  126. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_env_cli_scope.py +0 -0
  127. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_env_format.py +0 -0
  128. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_env_into.py +0 -0
  129. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_env_regressions.py +0 -0
  130. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_env_safety.py +0 -0
  131. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_findings_cmd.py +0 -0
  132. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_fix_adapters.py +0 -0
  133. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_fix_cli.py +0 -0
  134. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_fix_env.py +0 -0
  135. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_fix_hooks.py +0 -0
  136. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_fix_init.py +0 -0
  137. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_fix_overlays.py +0 -0
  138. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_fix_packaging.py +0 -0
  139. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_fix_scope_context.py +0 -0
  140. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_fix_skills_adapters.py +0 -0
  141. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_git_ssl_revoke.py +0 -0
  142. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_hooks.py +0 -0
  143. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_init_base.py +0 -0
  144. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_last_mile.py +0 -0
  145. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_launchers.py +0 -0
  146. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_local_additions.py +0 -0
  147. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_mcp.py +0 -0
  148. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_no_internal_labels.py +0 -0
  149. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_overlay_libs.py +0 -0
  150. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_overlays_cli.py +0 -0
  151. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_overlays_command.py +0 -0
  152. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_path_timeout_frontmatter.py +0 -0
  153. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_pipwheel.py +0 -0
  154. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_pyvenv_core.py +0 -0
  155. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_quoting_injection_safety.py +0 -0
  156. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_scope.py +0 -0
  157. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_sources.py +0 -0
  158. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_sources_uri.py +0 -0
  159. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tests/test_wrappers.py +0 -0
  160. {dotagents_cli-0.9.0 → dotagents_cli-0.9.2}/tools/audit.py +0 -0
@@ -7,6 +7,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.9.2] - 2026-11-11
11
+
12
+ ### Added
13
+
14
+ - Overlay hooks, the AOS "Hooks" extension: an overlay's `hook.<ext>` and `[hooks]` table
15
+ run at a session's events (`pre-env`, `session-start`, `session-end`, `prompt`,
16
+ `turn-end`, `cd`, `pre-compact`, `pre-tool`, `post-tool`). dotagents fires `pre-env`
17
+ itself as it builds the environment, on every harness; `init` and
18
+ `overlays add/sync/remove` wire Claude Code's and Codex's own hooks to a new
19
+ `dotagents hook` command for the other events an installed overlay has a hook for (nothing
20
+ is wired, so no process starts, for a store without them). A hook is off until the user
21
+ allows it at the overlay's tree digest (`overlays hooks`, `overlays allow-hooks NAME
22
+ --digest D`, `overlays revoke-hooks`; kept in the user store, one digest per overlay), is
23
+ bounded (60 s a run, 120 s an event, 64 KiB of output, 1 MiB of payload, arguments as a
24
+ list, its own process group), and can only tighten: a `decision` other than
25
+ `block`, `deny` or `ask` is a refused attempt to grant, and the answer to the harness is
26
+ built by dotagents, never the hook's output. An overlay with a hook that refuses on failure
27
+ is not activated while its hooks are off. What a harness cannot fire or apply is reported
28
+ (`hook-unsupported`, `hook-partial`, `hook-not-applied`, `hook-ask-denied`,
29
+ `hook-not-allowed`, `hook-failed`), never dropped. Passes the specification's hook
30
+ vectors (the `OVERLAY.md` cases excepted: that form is not read yet). See
31
+ `docs/guide/overlay-hooks.md`.
32
+ - The sensitive-variable list holds `CLAUDE_CONFIG_DIR` and `CODEX_HOME`, which point a
33
+ harness at another configuration, with its own permissions and hooks.
34
+
35
+ ### Fixed
36
+
37
+ - The session-start, PowerShell, Codex and Antigravity hook entries run the `dotagents` of the
38
+ store (`$AGENTS_HOME/bin`) or on PATH, never one in the project's own `.agents/bin`: the agent
39
+ can write there, and what it planted ran with the user's access.
40
+
41
+ ### Changed
42
+
43
+ - `private-sync`'s session start and sync-back are now overlay hooks (`session-start` and
44
+ `turn-end`) instead of scripts you register in `~/.claude/settings.json`; it no longer ships
45
+ `hooks/settings.snippet.json`. Allow them once (`dotagents overlays allow-hooks private-sync`)
46
+ and run `dotagents init -g`; remove the old entries from `settings.json` or they run twice.
47
+ `tools/cloud-setup.sh` does both steps itself.
48
+
49
+ ## [0.9.1] - 2026-11-06
50
+
51
+ ### Fixed
52
+
53
+ - The Claude Code PowerShell-tool env loader (`init --powershell-env-hook`) no longer
54
+ answers `permissionDecision: "allow"`, which ran calls that would have asked and, under
55
+ `--permission-mode dontAsk` or with no way to ask, calls that would have been refused.
56
+ It now only rewrites the call and the permission system decides. A PowerShell call
57
+ therefore asks unless `PowerShell` itself is allowed, since rules judge the rewritten
58
+ command. An `init --powershell-env-hook` replaces a loader already wired.
59
+
10
60
  ## [0.9.0] - 2026-11-04
11
61
 
12
62
  dotagents implements the [Agent Overlay Specification](https://boriken-dev.github.io/agent-overlay-spec/)
@@ -1743,7 +1793,9 @@ Patch release: the PATH/POSIX-conversion fix above (the only change since 0.3.0)
1743
1793
  affected module sources (`dotagents.cli`, `duho.presets`) to extracted temp files
1744
1794
  before dispatch; a no-op for a plain install.
1745
1795
 
1746
- [Unreleased]: https://github.com/jose-pr/dotagents/compare/v0.9.0...HEAD
1796
+ [Unreleased]: https://github.com/jose-pr/dotagents/compare/v0.9.2...HEAD
1797
+ [0.9.2]: https://github.com/jose-pr/dotagents/compare/v0.9.1...v0.9.2
1798
+ [0.9.1]: https://github.com/jose-pr/dotagents/compare/v0.9.0...v0.9.1
1747
1799
  [0.9.0]: https://github.com/jose-pr/dotagents/compare/v0.8.3...v0.9.0
1748
1800
  [0.8.3]: https://github.com/jose-pr/dotagents/compare/v0.8.2...v0.8.3
1749
1801
  [0.8.2]: https://github.com/jose-pr/dotagents/compare/v0.8.1...v0.8.2
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: dotagents-cli
3
- Version: 0.9.0
3
+ Version: 0.9.2
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
@@ -144,10 +144,12 @@ Claude Code's PowerShell tool never reads `$CLAUDE_ENV_FILE`, so its commands do
144
144
  see the env layers. On Windows, `init --powershell-env-hook` adds a `PreToolUse` hook
145
145
  (matched on the `PowerShell` tool) that prepends the `dotagents env` change set to each
146
146
  PowerShell call (`env --diff --cache`, so an unchanged environment is not reassembled
147
- on every call). **It auto-approves every PowerShell tool call**: rewriting a command
148
- without a prompt on each one requires `permissionDecision: "allow"`, which skips the
149
- approval prompt (your deny and ask rules still apply). It is off by default, and a
150
- plain `init` removes one an earlier release wired.
147
+ on every call). It states no permission decision, so a call asks, is denied or runs as
148
+ your rules say. **Your rules judge the rewritten command**: a narrow allow rule such as
149
+ `PowerShell(Get-Location:*)`, or a command that would pass as read-only, no longer
150
+ matches once the loader leads it, so each PowerShell call asks unless `PowerShell`
151
+ itself is in your allow rules. It is off by default, and a plain `init` removes one an
152
+ earlier release wired.
151
153
 
152
154
  The env half never truncates the file and is guarded by `[ -n "$CLAUDE_ENV_FILE" ]`
153
155
  (see the [hooks docs](https://code.claude.com/docs/en/hooks#persist-environment-variables)):
@@ -215,6 +217,8 @@ dotagents overlays list # installed (discovered) + available
215
217
  dotagents overlays sync 'py*' # refresh installed overlays matching a glob
216
218
  dotagents overlays remove python # delete the overlay dir, unpublish its skills, un-merge its rules
217
219
  dotagents overlays show python # describe one: declarations, digest, pin, weakenings, source (--json)
220
+ dotagents overlays hooks private-sync # its hooks: what runs where, and whether they are allowed
221
+ dotagents overlays allow-hooks private-sync --digest sha256:... # allow them at that digest (see overlay-hooks)
218
222
  ```
219
223
 
220
224
  - **`add`** takes names or globs: `'*'` is every overlay the repos offer, so with
@@ -0,0 +1,213 @@
1
+ # Overlay hooks
2
+
3
+ An overlay can run a program at a named moment of a session: before the environment is built,
4
+ when a session starts or ends, when you send a prompt, before a tool runs. This is the
5
+ [Agent Overlay Specification's hooks](https://boriken-dev.github.io/agent-overlay-spec/spec/v0/hooks/)
6
+ (an optional extension); this page is what dotagents does with them.
7
+
8
+ ```toml
9
+ # overlay.cfg
10
+ [hooks]
11
+ session-start = "hooks/start.py"
12
+ pre-tool = { run = "hooks/guard.sh", match = ["execute"], timeout = 5, on_failure = "refuse" }
13
+ turn-end = false
14
+ ```
15
+
16
+ A `hook.<ext>` at the overlay root (chosen by runtime, as `env.<ext>` is) receives every event
17
+ that carries no content by default. `prompt`, `pre-tool` and `post-tool` carry what you typed or
18
+ what a tool was given, so they fire only for an event `[hooks]` names. The hook is told its event
19
+ as its first argument and in `AOS_HOOK_EVENT`, and its payload as one JSON object on standard
20
+ input; it answers with nothing or one JSON object (`context`, `decision`, `reason`, `env`).
21
+
22
+ ## A hook tightens and never grants
23
+
24
+ Nothing a hook prints reaches the harness. dotagents reads the output, accepts only what the
25
+ event takes, and answers the harness itself, in the harness's words. A `decision` other than
26
+ `block`, `deny` or `ask` (`allow`, `approve`, anything) is a refused attempt to grant: the run
27
+ fails (`grant`) and nothing of it applies. dotagents never states an approval and never rewrites
28
+ a tool's input.
29
+
30
+ ## Off until you allow them
31
+
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:
34
+
35
+ ```
36
+ dotagents overlays hooks NAME # what runs, where, and whether it is allowed
37
+ dotagents overlays allow-hooks NAME # shows the same and the digest
38
+ dotagents overlays allow-hooks NAME --digest sha256:...
39
+ dotagents overlays revoke-hooks NAME
40
+ ```
41
+
42
+ 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.
49
+
50
+ An overlay with a hook that has `on_failure = "refuse"` (a guard) is **not activated** while its
51
+ hooks are off, and its guards **fail** meanwhile: a prompt is blocked, a tool call denied, as if
52
+ the hook had run and failed (`hook-not-allowed` at the next session start). A guard that is
53
+ missing must not let everything through.
54
+
55
+ ## Which events fire where
56
+
57
+ | Event | Claude Code | Codex | Any other harness |
58
+ | --- | --- | --- | --- |
59
+ | `pre-env` | dotagents fires it as it builds the environment | same | same |
60
+ | `session-start` | `SessionStart`: context, and variables for Bash through the env file | `SessionStart`: context | cannot |
61
+ | `session-end` | `SessionEnd` | `SessionEnd` | cannot |
62
+ | `prompt` | `UserPromptSubmit`: context, block | same | cannot |
63
+ | `turn-end` | `Stop` (notification only) | `Stop` | cannot |
64
+ | `cd` | `CwdChanged`: variables | no such event: cannot | cannot |
65
+ | `pre-compact` | `PreCompact`: context, block | `PreCompact`: block (no context) | cannot |
66
+ | `pre-tool` | `PreToolUse`: deny, ask, context | `PreToolUse`: deny, context (`ask` is applied as `deny`) | cannot |
67
+ | `post-tool` | `PostToolUse`: context | `PostToolUse`: context | cannot |
68
+
69
+ What cannot fire is reported (`hook-unsupported`), as is an event that fires for only some
70
+ occurrences (`hook-partial`: Claude Code's `PostToolUse` is for calls that ended well; Codex's
71
+ tool hooks were measured on its shell tool), an output the harness cannot take
72
+ (`hook-not-applied`: variables from `prompt` or `pre-tool`, context from Codex's `pre-compact`,
73
+ refusing a session start), an `ask` turned into a `deny` (`hook-ask-denied`), a hook that is off
74
+ (`hook-not-allowed`), and a failed run (`hook-failed`, with its cause, never its output). The
75
+ reports reach you in the session-start message of the harness and in the log of `dotagents env`.
76
+ `dotagents overlays hooks` lists the table above per hook.
77
+
78
+ `init` and `overlays add/sync/remove` wire the harness's own hook for an event only when an
79
+ installed overlay has a hook for it (`dotagents hook` is the program they run, through a small
80
+ forwarder script `overlay_hook.py` deployed in your user config directory), so a store with no
81
+ hooks starts no extra process. A project's entries (`settings.local.json`) run the same forwarder
82
+ from your user config directory. The text of an entry holds no guard flag and no overlay name, so
83
+ it does not change when overlays do. Codex asks you to trust a hook by its hash; dotagents never
84
+ writes that trust.
85
+
86
+ ## Limits and failures
87
+
88
+ Every hook runs with the overlay root as its working directory, outside the sandbox, in its own
89
+ process group (a job object on Windows) that ends with it, with arguments as a list (no shell
90
+ reads the overlay's strings) and the environment of that overlay's own processes. Defaults:
91
+ 60 s a run (and the entry's `timeout`, default 30, if shorter), 120 s an event, 64 KiB of
92
+ output (more stops the run), 64 KiB of standard error kept for the log, 1 MiB of payload. A
93
+ failed hook applies nothing of its output; with `on_failure = "report"` the event goes on, with
94
+ `"refuse"` it is refused (a prompt blocked, a call denied, a session start reported as not
95
+ refusable). `pre-env` stops the session only under `dotagents launch`, where the harness does not
96
+ exist yet; under `dotagents env` (a harness already running, a setup) it is reported as
97
+ `hook-not-applied` and nothing of the hook applies. An oversize or unreadable payload is a failure
98
+ of the event, refused when it has a guard.
99
+
100
+ The limits are settings, read from your user store only
101
+ (`overlays/.dotagents-state/hooks-settings.json`; a project's file is never read), each in
102
+ bounds: `timeout`, `event_timeout`, `deadline`, `max_output`, `max_payload`. The deadline
103
+ (default 150 s, at most 580) covers everything a harness's hook entry does from the process's
104
+ start: building the scope, hashing, env scripts and hooks; at it the whole process tree ends. The
105
+ harness's own timeout for the entry is derived from it and written at wiring time, so run
106
+ `dotagents init -g` after changing it.
107
+
108
+ Context a hook adds is wrapped in a marker holding a random nonce chosen after the hooks ran, so
109
+ the text cannot imitate it. Variables for Bash are written to Claude Code's env file as
110
+ single-quoted values of names the hook declared in `sets`; a value holding a line separator of
111
+ any kind (CR, LF, VT, FF, U+0085, U+2028, U+2029, NUL) is not written (`hook-not-applied`), and a
112
+ sensitive name (`PATH`, `BASH_ENV`, `LD_*`, ...) that is set is reported (`env-sensitive`). What a
113
+ hook writes to standard error goes to `<user store>/.cache/hooks.log` (bounded), never to the
114
+ harness.
115
+
116
+ ## What a hook cannot reach
117
+
118
+ This is about the **overlay-hook path**: a hook of an installed overlay, fired through the
119
+ forwarder (or by `pre-env`). It is the hardened path. It is not the whole of dotagents: see
120
+ "Where project code does run" below.
121
+
122
+ - **Bytes that were not hashed.** At each event the overlay's tree is read once, hashed, compared
123
+ with the allowed digest, and written to a snapshot under `<user store>/.cache/hook-snapshots/`
124
+ from those same bytes. The manifest the event is planned from, the env scripts that build the
125
+ hook's environment, the `lib/` and `bin/` paths they declare, the hook, and its working
126
+ directory are all the snapshot's; Python hooks run under `-I` with the snapshot's libraries on
127
+ the path explicitly. A file swapped after hashing, or a `node_modules`, `__pycache__` or
128
+ `.venv` (not part of a digest) planted beside the overlay, do not run. An existing snapshot is
129
+ hashed again before it is used, and copies of other digests are removed only when unused for a
130
+ week (a session started before an update still runs from its own). The overlay's state
131
+ directory is the live one, so a hook keeps what it wrote.
132
+ - **Anything the project holds.** The hook entries run an absolute interpreter, isolated (`-I`),
133
+ a forwarder script in your user config directory, and a fixed entry of the `dotagents` that
134
+ `init` recorded there (`dotagents._hook_main`). That entry is not the `dotagents` command line:
135
+ it discovers no command and adds no library, so `<project>/.agents/dotagents/cmds/`, a
136
+ project overlay's `cmds/` and `lib/` and the project's own modules never load in it. For a
137
+ user-level hook its scope has no project store at all, so a project folder cannot shadow a hook
138
+ of yours (a same-named one is reported, `hook-shadowed`, and your hook still runs), cannot add
139
+ anything to it, and cannot choose its Python.
140
+ - **A stand-in for dotagents.** The forwarder gives the entry a fresh nonce and accepts only an
141
+ answer that carries it back. A program that exits 0 with no output, or answers without the
142
+ nonce, is a failed guard (a refusal) and nothing at all for an event with no guard; an
143
+ "everything is fine" is never inferred from silence. When the forwarder's record lists a guard
144
+ for an event and the entry finds none, that is a refusal too. The forwarder passes the harness
145
+ only a refusal, context and a message, in the harness's own shape.
146
+ - **A project's interpreters.** `pwsh`, `node`, `bash` and the like, for a hook and for the env
147
+ scripts that build its environment, are found by dotagents itself (never the current
148
+ directory, which Windows searches first) on the `PATH` the session was started from, less every
149
+ relative entry, store or overlay `bin`, venv, and anything below the project or the user store
150
+ (`.venv/Scripts`, `node_modules/.bin`, `.agents/bin`). A Windows bash is looked for only beside
151
+ a `git` on that PATH and in the standard install directories.
152
+ - **A project's Python.** A hook, and the env scripts that build its environment, run with the
153
+ user store's Python chosen before any declaration runs, never a project's venv, and never the
154
+ session's `AGENTS_PYTHON`.
155
+ - **Other overlays' secrets.** `dotagents launch` records what it changed in
156
+ `AGENTS_SESSION_BASE`; a hook starts from the environment the session was started from, so a
157
+ secret another overlay was granted does not reach it. The harness's own variables
158
+ (`CLAUDE_ENV_FILE`, `CLAUDE_CODE_*`, `CODEX_*`) are dropped too. The project's own name for
159
+ itself (`AGENTS_PROJECT_ROOT`, `CLAUDE_PROJECT_DIR`) is kept.
160
+ - **The harness itself.** `dotagents launch` looks for a bare program name (`claude`) on the PATH
161
+ the shell had, less the same places, not on the session's PATH that starts with the project's
162
+ `.agents/bin`.
163
+
164
+ ## Where project code does run
165
+
166
+ Not everything dotagents does is hardened this way, by design (the owner's decision, recorded in
167
+ dotagents' decisions):
168
+
169
+ - The session-start, Codex and Antigravity entries that `init` wrote before overlay hooks existed
170
+ run `dotagents env` and `dotagents context` **in the project**. Those are the full command line:
171
+ they load `<project>/.agents/dotagents/cmds/`, and `env` runs the env scripts of the overlays
172
+ active there, outside the harness's sandbox. They run the store's `dotagents` (never one found in
173
+ the project or on PATH), but what that program loads from the project, it runs.
174
+ - A project overlay's own `env`/env scripts run whenever the environment is built for a session
175
+ there (`env`, `launch`, `setup`), as any active overlay's do.
176
+ - **Project-level guards are advisory.** A project-level hook entry lives in
177
+ `<project>/.claude/settings.local.json`, which the agent can write: it can remove the entry. The
178
+ forwarder and the user-level entries in your own config are not affected, but a guard that only
179
+ a project overlay declares is only as strong as that file. Put a guard you rely on in an overlay
180
+ of your user store.
181
+
182
+ An agent that can write the project's `.agents`, or your user store, can run code through those
183
+ paths; the overlay-hook path is the one that does not depend on either being safe from the agent.
184
+
185
+ ## The limit, generally
186
+
187
+ All of this assumes the places dotagents runs code *from* are not the agent's to write. A
188
+ deployment gets no barrier where:
189
+
190
+ - the recorded `code_root` is inside a checkout the agent works in (a `dist/dotagents.pyz` of a
191
+ repository, a source tree on `PYTHONPATH`), or a venv's `.pth` points dotagents into such a
192
+ place: what the agent writes there runs with your access. Install dotagents where the agent
193
+ cannot write, then run `dotagents init -g`;
194
+ - the agent can write your user store (`~/.agents`): it holds the allowances, the snapshots, the
195
+ settings and the wrappers, so it can allow and run anything.
196
+
197
+ `--no-prompt` and the harness-session check of `allow-hooks` stop accidents, not an agent: an
198
+ agent can clear its environment (`env -i`) and run the command. What keeps an agent from allowing
199
+ its own hooks is that `allow-hooks` is not an MCP tool and that the allowance is in a store it
200
+ cannot write.
201
+
202
+ Smaller limits: a hook that detaches into a new session (`setsid`) can outlive the tree kill on
203
+ POSIX; an overlay whose manifest is refused cannot show its guards; a hook can still read what
204
+ its overlay `requires` puts on its paths, live, and the packages an overlay's setup installed; an
205
+ entry whose interpreter has been uninstalled cannot run to say so (`dotagents overlays hooks` and
206
+ every `init` check the recorded one); and Codex asks you to trust an entry by its hash, which
207
+ dotagents cannot read: `overlays hooks` says the event fires natively once you have trusted it,
208
+ and a change of the entry text is warned about when it is wired.
209
+
210
+ ## Not done yet
211
+
212
+ Hooks declared by your own configuration or a project's (outside overlays), a `pre-tool`
213
+ variable loader, `OVERLAY.md` front matter, and harnesses other than Claude Code and Codex.
@@ -77,12 +77,20 @@ leaves the machine is a perfectly valid setup.
77
77
 
78
78
  ## Cloud sessions
79
79
 
80
- The `private-sync` overlay ships SessionStart / Stop hook scripts that clone or pull
81
- the private repo and link/sync the project each session. `overlays add` does not
82
- register them: add them to your runner's settings yourself (for Claude Code,
83
- `~/.claude/settings.json`), using the snippet the overlay ships in
84
- `$PRIVATE_SYNC_OVERLAY_ROOT/hooks/settings.snippet.json`. The bootstrap below does that
85
- merge for you.
80
+ The `private-sync` overlay declares two [overlay hooks](overlay-hooks.md) that clone or pull
81
+ the private repo and link/sync the project each session: `session-start` pulls and links,
82
+ `turn-end` pushes the session's changes back. They are off until you allow them at the
83
+ overlay's digest, and `dotagents init -g` wires Claude Code's and Codex's own hooks to run
84
+ them:
85
+
86
+ ```bash
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
+ dotagents init -g
91
+ ```
92
+
93
+ The bootstrap below does all of that for you.
86
94
 
87
95
  For a **fresh container** with no config yet, point the web environment's setup-script
88
96
  field at the self-contained bootstrap shipped in this repo — it fetches the latest each
@@ -32,6 +32,7 @@ nav:
32
32
  - Install: guide/install.md
33
33
  - Commands: guide/commands.md
34
34
  - Overlays: guide/overlays.md
35
+ - Overlay hooks: guide/overlay-hooks.md
35
36
  - Private sync: guide/private-sync.md
36
37
  - Authoring an overlay: guide/authoring.md
37
38
  - API Reference:
@@ -52,6 +52,10 @@ and an error saying a command is required, and exits 2.
52
52
  | `overlays sync [GLOB] [-g] [--repo SPEC]... [--copy] [--no-setup] [--setup-timeout S] [--no-venv] [--dry-run]` | Refresh installed overlays from the repo recorded at install (`--repo` replaces it), checked as `add` checks a source: the install is made to hold exactly the overlay's members (new and changed files land, everything else goes, caches and setup output left inside it included); a file edited here is backed up first under `<store>/install_backup/<timestamp>/overlays/<name>/`. Installs new `requires`, then prepares each synced overlay in its `sync` phase (`install` when no preparation succeeded here yet), whether its digest changed or not: its environments rebuilt, then its setup. Exits 1 when an overlay's repo cannot be loaded, its source lies inside `<store>/overlays/` or is refused, or its dependencies or setup fail. |
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
+ | `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 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
+ | `overlays revoke-hooks NAME [-g]` | Take back the allowance. |
58
+ | `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. |
55
59
  | `context [OUT] [-g] [--agents a,b] [--format markdown\|system-reminder\|json] [--write-agent] [--inline]` | Print (or write to `OUT`) the assembled context. `--write-agent` merges it into each agent's `context_target` under the project root (not with `-g`). Exits 2 when `--agents` names no known agent. |
56
60
  | `path [-g] [--lib] [--format list\|native\|posix\|json]` | Print the dirs `env` adds to `PATH` -- each store's `bin` and each active overlay's `PATH` additions (`--lib`: the existing agent library dirs, `PYTHONPATH`) -- highest precedence first; runs no env file. `posix` is `:`-joined, MSYS2 form on Windows. |
57
61
  | `env [-g] [--format F] [--diff] [--cache] [--into FILE]` | Print the assembled environment (`--diff`: only what differs from the caller's; `--cache`: replay the previous output while `env_cache_key` holds, up to `ENV_CACHE_TTL`; `--into FILE`: write it into FILE as the one block this command owns there) in format `F`: `auto` (default: the calling shell), `export`/`posix`/`sh`/`bash`, `dotenv`/`env`, `powershell`/`pwsh`/`ps`, `cmd`/`bat`/`batch`, `fish`, `json`, `ini`, `yaml`. |
@@ -269,6 +273,24 @@ conformance vectors (`tests/aos_vectors/`, revision in `REVISION`).
269
273
  `activates`; a failed sync keeps only the recorded digest active).
270
274
  - Resolution: `class ResolveError(Exception)` (`chain`), `resolve(requires_of,
271
275
  requested) -> (visiting order, missing)`, `order_key(priority, name, identity)`.
276
+ - Hooks (the hook vectors in `tests/aos_vectors/hooks*.json` pass): `HOOK_EVENTS`
277
+ (event → class, runs `hook.<ext>` by default, outputs, refusal, what `match` selects),
278
+ `SESSION_REASONS`, `TOOL_KINDS`; `[hooks]` and `[defaults] hooks` validate in
279
+ `validate`. `default_hook(files, os_name, interpreters)`, `hooks_of(manifest, files,
280
+ os_name, interpreters) -> (entries, script-not-run files)`, `hook_plan(manifest, facts)
281
+ -> {hooks, on, activates, needs, reports, non_defaults}` (`on` only while the digest is
282
+ the one allowed; `activates` false for a guard that is off), `hook_matches(event, match,
283
+ value)`, `hook_payload(event, known)` with `PAYLOAD_COMMON`, `PAYLOAD_KEYS`, `TOOL_KEYS`;
284
+ `read_hook_output(event, data, sets)` / `hook_result(event, run, limits)` raise
285
+ `HookFailure` (`.cause`: `not-run`, `payload-too-large`, `timeout`, `too-large`, `exit`,
286
+ `grant`, `output`) with `HOOK_OUTPUT_KEYS`, `MAX_HOOK_REASON`; `fire_event(event, runs,
287
+ applies, limits, windows) -> dict` combines hooks to the tightest (a run holding `start`, a
288
+ function, is started only when its turn comes); `apply_pre_env(env, identities, runs,
289
+ limits, reports, non_defaults)` applies `pre-env` output (raises `Refused("hook-refused")`);
290
+ `DEFAULT_HOOK_TIMEOUT = 30`, `DEFAULT_HOOK_LIMITS` (60 s a run, 120 s an event, 64 KiB
291
+ output and error, 1 MiB payload). `tree_entries(root, *, with_data=False)` hands back each
292
+ file's bytes (read once, at most `MAX_TREE_BYTES = 256 MiB`) instead of its digest, so a digest
293
+ and a copy can be made from the same bytes.
272
294
 
273
295
  ## `dotagents._activate`
274
296
 
@@ -342,6 +364,14 @@ started suspended until it is in the job) so nothing it started outlives it.
342
364
  `run_step(argv, *, cwd, env, timeout, capture=False, max_output=1 MiB) ->
343
365
  (status, stdout)`. `pycache_prefix(user_store) -> str` — where Python run for an
344
366
  overlay keeps bytecode (`<user store>/.cache/pycache`), never inside an overlay.
367
+ `which_in(name, path) -> Optional[str]` finds a program on the absolute directories of `path`,
368
+ never the current directory (Windows searches it first with `shutil.which`); `interpreter_command(...,
369
+ trusted=False)` with `trusted` looks for a Windows bash only via `find_bash_on`.
370
+ `run_hook(argv, *, cwd, env, timeout, stdin, max_output, max_stderr) -> dict` runs a hook
371
+ bounded: `stdin` written without waiting on the child, stdout kept to `max_output` (a run
372
+ that writes more is stopped: `too_large`), stderr kept to `max_stderr` and the rest read and
373
+ dropped, ended at `timeout` (`timed_out`), every process it started ended with it; returns
374
+ `exit`, `stdout`, `stderr`, `timed_out`, `too_large`, `not_run`, `seconds`.
345
375
 
346
376
  ## `dotagents._resources`
347
377
 
@@ -364,13 +394,14 @@ The package's own data; library code, importing nothing from `dotagents.cli`.
364
394
  install. The archive also holds the vendored duho and pathapi; `env`
365
395
  exports it as `AGENTS_PYLIB`.
366
396
 
367
- ## Commands — `dotagents.cli.init`, `dotagents.cli.overlays`, `dotagents.cli.context`, `dotagents.cli.env`, `dotagents.cli.path`, `dotagents.cli.about`, `dotagents.cli.build_pyz`, `dotagents.cli.mcp`
397
+ ## Commands — `dotagents.cli.init`, `dotagents.cli.overlays`, `dotagents.cli.context`, `dotagents.cli.env`, `dotagents.cli.path`, `dotagents.cli.about`, `dotagents.cli.build_pyz`, `dotagents.cli.mcp`, `dotagents.cli.hook`
368
398
 
369
399
  One `duho` command class per command above; fields are the flags, `__call__()`
370
400
  returns the exit code.
371
401
 
372
402
  - `init.Init`; `overlays.Overlays` (umbrella) with `OverlayAdd`, `OverlayRemove`,
373
- `OverlayList`, `OverlaySync`, `OverlayShow`; `context.Context` (`FORMATS =
403
+ `OverlayList`, `OverlaySync`, `OverlayShow`, `OverlayHooks`, `OverlayAllowHooks`,
404
+ `OverlayRevokeHooks`; `hook.Hook`; `context.Context` (`FORMATS =
374
405
  ("markdown", "system-reminder", "json")`); `env.Env`; `path.PathCmd`
375
406
  (`PATH_FORMATS = ("list", "native", "posix", "json")`); `build_pyz.BuildPyz`;
376
407
  `about.About`.
@@ -724,6 +755,9 @@ API:
724
755
  included, from before/after snapshots taken in one bash process (MSYS2 `*PATH`
725
756
  values converted back to Windows form). No bash, a failed `source` or an `exit`
726
757
  in the file → empty, with a warning.
758
+ - `find_bash_on(path) -> Optional[str]` — a Windows bash for a hook, looked for only beside a `git` on `path`,
759
+ in the standard install dirs and as a `bash` on `path` (never `os.environ["PATH"]`, never the
760
+ working directory), proved MSYS2/Cygwin, not cached.
727
761
  - `find_bash(logger=None) -> Optional[str]` — the bash for plain env files, resolved
728
762
  once per process: `bash` on `PATH` on POSIX; on Windows Git's bash (beside `git`
729
763
  on `PATH`, then the standard install dirs, then `PATH`), accepted only if it is
@@ -879,6 +913,15 @@ API:
879
913
  derivable, for the runtime agent only (`{}` in a plain shell); never replaces a
880
914
  value in `environ` unless `explicit` names a known agent. `root` is unused.
881
915
 
916
+ - `Agent.wire_overlay_hooks(dest, *, dry_run, logger, config_root=None)` — wire the
917
+ harness's own hooks to `dotagents hook` for the events the overlays installed in the
918
+ store `dest` have a hook for, and retract an entry no overlay uses any more; a no-op on
919
+ the base class. `ClaudeAgent` writes `settings.json` (user) or `settings.local.json`
920
+ (project) with a bash entry per event and, on Windows, a PowerShell one that selects itself
921
+ when there is no Git Bash; `CodexAgent` writes the user `hooks.json` (with
922
+ `commandWindows`). `init -g` and `overlays add/sync/remove` call it; the entries are
923
+ labelled `dotagents: Overlay hooks: <native event>` and run `overlay_hook.py`.
924
+
882
925
  ## `dotagents._merge`
883
926
 
884
927
  Marker lines (a marker counts only on a line of its own, outside fenced code):
@@ -911,6 +954,109 @@ Marker lines (a marker counts only on a line of its own, outside fenced code):
911
954
  - `timestamped_backup_root(dest) -> Path` — `<dest>/install_backup/<timestamp>`, one
912
955
  per store per process.
913
956
 
957
+ ## `dotagents._overlay_hooks`
958
+
959
+ Firing overlay hooks (the model is `dotagents._aos`; the user-facing page is
960
+ `docs/guide/overlay-hooks.md`). A hook is the overlay's code: it runs only while its
961
+ overlay's tree digest is the one the user allowed, as an argv list with the overlay root as
962
+ its directory, bounded by `LIMITS`.
963
+
964
+ - `LIMITS` (`DEFAULT_HOOK_LIMITS`, a copy tests may change), `NO_HARNESS = "dotagents"`,
965
+ `class Harness` (`id`, `native`: AOS event → native name, `applies`: event → outputs
966
+ dotagents can apply, `reasons`, `fires`), `CLAUDE`, `CODEX`, `ANYWHERE` (no
967
+ harness known: nothing reported as unfireable), `HARNESSES`, `harness_of(ident) -> Harness`
968
+ (an unknown id fires only `pre-env`), `partial_events(harness)`.
969
+ - Settings, read from the user's own store only (never an overlay or a project):
970
+ `SETTING_BOUNDS` (`timeout`, `event_timeout`, `deadline`, `max_output`, `max_payload`, each
971
+ with its bounds), `settings_path(user_store)` (`overlays/.dotagents-state/hooks-settings.json`),
972
+ `read_settings`, `limits_for(user_store)` (`LIMITS` under them), `deadline_for` (default
973
+ `DEFAULT_DEADLINE = 150` s for everything a hook entry does, from the process's start),
974
+ `entry_timeout(user_store)` (the harness's own timeout for the entry: deadline + 20 s, at
975
+ most `ENTRY_TIMEOUT_CAP = 600`).
976
+ - Allowances, in the user store only, keyed by the overlay's store and name:
977
+ `allowances_path(user_store)`, `read_allowances`, `allowed_digest(user_store, identity,
978
+ 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;
981
+ - `read_tree(overlay, keep=False) -> (digest, files)` (the bytes the digest covers when `keep`),
982
+ `snapshot(user_store, overlay, digest, files) -> Path` — the copy a hook runs from, one directory
983
+ per digest under `<user store>/.cache/hook-snapshots/` made from exactly the bytes hashed (what
984
+ `AOS` excludes from a digest, `node_modules` and `__pycache__` included, is not in it) and
985
+ hashed again before an existing one is reused; others go after `SNAPSHOT_MAX_AGE` (7 days) unused;
986
+ `SnapshotOverlay(path, live)` — an overlay read from its snapshot (manifest, files, paths,
987
+ scripts), whose `state_dir`, `preparation_path` and `install_record_path` are the live overlay's;
988
+ `overlay_plan(..., keep_tree=True)` makes the snapshot and plans from its manifest
989
+ (`plan["snap"]`), so env scripts, paths and the cwd are the hashed bytes; `PYTHON_BOOT` runs a
990
+ Python hook under `-I` with the libraries explicit; `project_roots(environ, cwd)` — where the agent
991
+ works, for `clean_path(path, user_store, roots)`; `fresh_digest(overlay)` (never memoized),
992
+ `overlay_plan(overlay, harness, user_store) ->
993
+ plan` (`hook_plan` plus `digest`, `identity`), `declares_guard(overlay)`,
994
+ `refusal(overlay) -> Optional[str]` (a guard that is off keeps its overlay out of
995
+ `_activate.active`), `planned(overlays, harness, user_store)`, `has_live_pre_env(scope,
996
+ harness_id)` (`env --cache` is not replayed for it).
997
+ - `class Subject`, `execute(subject, event, payload, env, *, limits=None) -> run`,
998
+ `printable(data, bound=4096)` (stderr made fit for a log), `class Fired` (`result`, `reports`,
999
+ `errors`: what each hook wrote to standard error, for the log, `decision`), `session_id(harness, native_id)` (a hash,
1000
+ never the harness's id), `value_for_match`, `fire(event, facts, plans, harness, *,
1001
+ environment, limits=None) -> Fired`, `unsupported_reports(plans)`, `describe(report)`,
1002
+ `pre_env(plans, session, base, harness, *, cwd=None) -> (reports, non_defaults)`.
1003
+ - Wiring: `STATUS_PREFIX`, `FORWARDER = "overlay_hook.py"`, `wanted_events(store, harness) ->
1004
+ {event: guarded}`, `level_token(dest, user_store)` (`user` or `project-<hash>`: the only thing
1005
+ in an entry's text besides the harness and the event), `sh_quote` / `ps_quote` (single-quote a
1006
+ path for bash / PowerShell), `code_root()`, `forwarder_record(user_store, guards)`,
1007
+ `record_path`, `write_record`, `remove_record`, `interpreter_problem(config_dir, token)` (the
1008
+ recorded interpreter or dotagents is gone). The forwarder and its record live in the user's
1009
+ harness config directory only; the entry runs `<python> -I <forwarder> <harness> <event>
1010
+ <token>`.
1011
+ - `TOKEN_FORM` / `valid_token(token)` — a store token is `user` or `project-` and ten hex digits,
1012
+ checked before it is part of a path; `clean_path(path, user_store)` — a `PATH` without relative
1013
+ entries, any store's or overlay's `bin` or a venv, which is where hook and env-script
1014
+ interpreters (`pwsh`, `node`) are found; `shadowed_with_hooks(scope)` — the user's overlays
1015
+ with a hook that a nearer store's same-named folder hides (they still fire at `pre-env`).
1016
+ - Sessions: `KEEP_PROJECT` (the project's own names, `AGENTS_PROJECT_ROOT` and
1017
+ `CLAUDE_PROJECT_DIR`, that `session_base` leaves alone), `session_base(environ)` — the environment a hook starts from: what `dotagents
1018
+ launch` recorded in `AGENTS_SESSION_BASE` (each changed name's earlier value, null if unset)
1019
+ undone, and the harness's own variables (`CLAUDE_ENV_FILE`, `CLAUDE_CODE_*`, `CODEX_*`)
1020
+ dropped; `log_path`, `log_errors(user_store, errors)` and `LOG_LIMIT` — a hook's standard
1021
+ error goes to `<user store>/.cache/hooks.log`, never to the harness.
1022
+
1023
+ ## `dotagents._hook_main`
1024
+
1025
+ The fixed entry the hook forwarder runs, in place of the `dotagents` command line: it discovers
1026
+ no command and adds no library (discovery reads what the project's `.agents` holds). `main(argv)`
1027
+ takes `EVENT --harness H --level user|project --agents-dir DIR --nonce N`, builds the scope
1028
+ (:func:`dotagents._hook_dispatch.scope_for`) and prints one JSON object, `{"nonce": N,
1029
+ "answer": <text for the harness>}`; the forwarder accepts nothing that does not carry its nonce.
1030
+
1031
+ ## `dotagents._hook_dispatch`
1032
+
1033
+ The program a harness's own hook runs (`dotagents hook`). Nothing a hook printed reaches the
1034
+ harness: the answer is built from the output that was accepted, and can only deny, ask or
1035
+ block.
1036
+
1037
+ - `scope_for(level, user_store)` — the scope a hook of `level` is looked for in: `user` has no
1038
+ project store in it (nothing a project holds can shadow a user hook, add commands or
1039
+ libraries, or choose the Python); `dispatch(..., shadow_scope=None)` reports, at session start,
1040
+ the user hooks a project folder of the same name hides (`hook-shadowed`).
1041
+ - `MAX_NATIVE`, `tool_kind(harness, name) -> kind` (a tool not placed is `other`, which runs
1042
+ every hook of its event), `facts_of(harness, event, native) -> known`, `context_text(parts)`
1043
+ (each part inside a marker with a nonce chosen after the hooks ran),
1044
+ `write_env_block(path, label, changes)` (single-quoted values of validated names into
1045
+ Claude's env file; the file is split at line feeds only and a value holding any of
1046
+ `LINE_SEPARATORS` is refused, `env_file_safe(changes)`), `hook_environment(scope, overlay,
1047
+ environ)` (the overlay's process environment from `session_base`, with the user store's Python,
1048
+ never a project's venv), `answer(harness, event, fired, env_file) -> (stdout, stderr)`,
1049
+ `refusal_answer(harness, event, why)`, `plans_for(scope, harness, level, event=None)`,
1050
+ `dispatch(harness_id, event, stdin, scope, level, environ) ->
1051
+ (stdout, stderr)`.
1052
+
1053
+ ## `dotagents.cli.hook`
1054
+
1055
+ `Hook` — the `hook` command (see the command table). Packaged beside the harness's hook
1056
+ configuration by `init`: `_overlay/dotagents/hooks/overlay_hook.py`, the forwarder that finds
1057
+ `dotagents`, runs `dotagents hook` with the payload, and answers a refusal itself when it
1058
+ cannot (`--guard`).
1059
+
914
1060
  ## `dotagents._hooks`
915
1061
 
916
1062
  Settings schema (Claude Code and Codex): `hooks.<Event>` is a list of matcher
@@ -919,10 +1065,10 @@ objects `{"matcher"?: str, "hooks": [{"type": "command", "command": str, ...}]}`
919
1065
  - `STATUS_PREFIX = "dotagents: "` — our hooks' `statusMessage` is
920
1066
  `STATUS_PREFIX + label`; that label is the hook's identity.
921
1067
  - `build_hook_entry(command, *, matcher=None, status_message=None, shell=None,
922
- command_windows=None) -> dict` — one matcher object; `None` keys omitted;
1068
+ command_windows=None, timeout=None) -> dict` — one matcher object; `None` keys omitted;
923
1069
  `command_windows` is written as `commandWindows`.
924
1070
  - `merge_hook(existing, command, *, matcher=None, status_message=None, shell=None,
925
- command_windows=None) -> tuple[list, bool]` — `(entries, changed)`. An identical
1071
+ command_windows=None, timeout=None) -> tuple[list, bool]` — `(entries, changed)`. An identical
926
1072
  entry is kept; ours in another shape (same status label, or the bare label on a
927
1073
  command running dotagents) is refreshed in place, keeping keys the user added;
928
1074
  duplicates dropped; foreign and malformed entries kept verbatim; a foreign hook
@@ -1,3 +1,3 @@
1
1
  """dotagents: installable CLI for the dotagents agent-config payload."""
2
2
 
3
- __version__ = "0.9.0"
3
+ __version__ = "0.9.2"
@@ -111,6 +111,11 @@ def refusal(overlay: Any, *, prepared: bool = True) -> "Optional[str]":
111
111
  foreign = _aos.foreign_entries(overlay.path)
112
112
  if foreign:
113
113
  return "its copy holds %s, which is not part of the overlay" % ", ".join(foreign[:5])
114
+ from dotagents import _overlay_hooks
115
+
116
+ held = _overlay_hooks.refusal(overlay)
117
+ if held is not None:
118
+ return held
114
119
  try:
115
120
  needs = needs_preparation(overlay)
116
121
  except _aos.Refused as refused: