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