[pull] main from gi-dellav:main#96
Merged
Merged
Conversation
Introduce a default-off `hooks` feature under src/extras/hooks/: a rig-free HookDispatcher (config schema, matcher index, stdin envelope, exit-code and stdout-JSON channels, deterministic merge/dedup, subprocess execution with timeout and process-group cleanup), the single rig-typed adapter (HookedTool plus wrap_all), and the per-hash trust model. Add the permission-seam methods the adapter needs (force_ask_once, allow_once, record_blocked reusing track_doom_loop), gated behind the feature so the default build is unchanged.
Cover the dispatcher engine, the rig adapter, the trust model, lifecycle dispatch, and the permission seam, plus the data-driven contract corpus. All hook tests live under src/tests/hooks/ to keep the flat test directory readable.
Weave the tool-event decorator into the agent and subagent builders through a single wrap_from_global point. Dispatch the lifecycle events from zerostack's own runner: UserPromptSubmit, Stop (including the headless -p/--loop path), SessionStart/SessionEnd, and SubagentStart/SubagentStop. Add the --no-hooks kill switch and the hooks-test dry-run entry point.
Add a Windows managed-settings path and a powershell -Command fallback for build_shell_invocation, following the sh/powershell convention already used in main.rs and event_handler.rs. Document the Windows settings paths (experimental) and platform-aware command wording in CONFIG.md, and make the subprocess tests cfg-aware so they stay meaningful on the Unix CI runner.
hash_hook_binding omitted the project path, so trusting a binding like
{"command": "./guard.sh"} in one project silently trusted the
identical binding in any other project, executing that project's file
since hook subprocesses inherit zerostack's CWD. Thread project_root
through hash_hook_binding, filter_trusted_project_hooks, and
build_dispatcher_from_paths as an explicit parameter (keeping the
dispatcher builder fully unit-testable without touching the CWD), and
have load_dispatcher pass the canonicalized current directory, falling
back gracefully and never panicking if canonicalization fails.
Add a test proving two different project roots produce different
hashes for the same event/matcher/handler, and a test proving trust
does not carry over across build_dispatcher_from_paths calls with
different roots.
runner::run_print took _max_turns but never used it: rig 0.39 seeds
each request's turn bound from agent.default_max_turns, set once at
construction (agent::builder::build_agent_inner) from the same
cli.resolve_max_agent_turns(cfg) value, so behavior is unchanged
without the parameter. Remove it from run_print, its AnyAgent wrapper,
and both call sites; drop the now-unnecessary
#[allow(clippy::too_many_arguments)].
Also replace the Option<(u32, bool)> loop_info tuple with a documented
LoopInfo { iteration, active } struct across spawn_agent, run_print,
and their AnyAgent wrappers, so the Stop hook envelope fields it feeds
are self-explanatory at the call site instead of positional.
…ny rule
Add a test tool that mirrors bash.rs's real check_perm flow (parses
args as {"command": "..."} and checks the parsed command, not the raw
JSON), then configure a PreToolUse hook that rewrites an innocuous
command into one matched by a permission deny rule. Asserts the call
still errors with the permission denial, proving decorator.rs applies
updated_input before the inner call and the inner tool's own
permission check sees the rewritten command rather than the original.
…ments CONFIG.md claimed copying a CC hooks setup works "with no changes to the hook scripts themselves". That's true of the settings.json schema, but not of scripts that read $CLAUDE_PROJECT_DIR: zerostack sets $ZEROSTACK_PROJECT_DIR instead (a deliberate product-decoupling decision, not a gap to fix), so such scripts do need a one-line change. Soften both places that made the stronger claim. Also add a short comment on decorator.rs's WasmBoxedFuture explaining it's rig's ToolDyn return type (Send-bound only relaxed on wasm32), and a doc comment on AnyAgent::spawn_runner explaining why it's async: the UserPromptSubmit gate must resolve before spawning.
feat(hooks): external-command hooks at agent lifecycle points
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
See Commits and Changes for more details.
Created by
pull[bot] (v2.0.0-alpha.4)
Can you help keep this open source service alive? 💖 Please sponsor : )
Note
Add a hooks system that gates and rewrites tool calls, prompts, and session lifecycle events
hooksCargo feature (off by default) that wraps every registered tool withHookedTool, dispatchingPreToolUse/PostToolUse/PostToolUseFailureshell hooks that can deny, rewrite, or allow calls via exit codes and JSON on stdout/stderr.HookDispatcherloaded from global, project, and managedsettings.jsonfiles; project-level hooks require interactive trust confirmation before activation, stored intrusted-hooks.json.LoopInfocontext allows Stop hooks to force agent continuation up to 8 consecutive times with a hook-provided instruction.--no-hooks,--hooks-test <tool>, and--hooks-test-inputCLI flags; a/hooksslash command reports configured event/handler counts; session start/end and subagent lifecycle events are dispatched around key state transitions.PermissionCheckerwithforce_ask_onceandallow_onceone-shot methods consumed on the next permission check, feeding hook-denied calls into doom-loop detection viarecord_blocked.AnyAgent::spawn_runneris nowasyncandrun_printdrops itsmax_turnsparameter, which are breaking API changes for any callers outside this PR.Macroscope summarized bf6bcb6.