refactor(hooks): simplify caching, dispatch flow, and notification helpers#2531
Merged
dgageot merged 2 commits intodocker:mainfrom Apr 27, 2026
Merged
Conversation
Same features, less plumbing. - Pre-build per-agent hooks.Executor once in NewLocalRuntime (buildHooksExecutors). hooksExec is now a plain map lookup; drop the double-checked locking and the hooksExecMu RWMutex. - Drop the redundant exec.Has(event) early-check in dispatchHook; Dispatch already short-circuits on an empty event, and logs "Executing hooks" itself, so the runtime side stays focused on Warning emission and error handling. - Drop matcher.raw and rely on pattern == nil to mean "match all", collapsing matcher.matches() into a single boolean expression. - Use r.CurrentAgent() in executeOnUserInputHooks instead of re-implementing the team lookup. - Remove the unused AgentDefaults.IsZero() helper (and its test assertion). ApplyAgentDefaults already returns nil on the zero value, which is what callers actually rely on. Assisted-By: docker-agent
Express intent more directly at the runtime callsites and split the
matcher loop out of Executor.Dispatch.
- Replace the per-event executeNotificationHooks /
executeOnErrorHooks / executeOnMaxIterationsHooks trio with two
composite helpers, notifyError and notifyMaxIterations, that fire
the (notification, structured) event pair atomically. The two
events are always emitted together in practice, so collapsing them
removes 2 lines per callsite in loop.go and surfaces intent
("notify error", "notify max iterations") instead of leaving the
reader to mentally join two calls. A private notify() helper
shares the (level, message)-shaped Input building.
- Drop the now-dead "invalid notification level" runtime check: the
level is always a compile-time constant ("error" / "warning")
inside the new helpers.
- Extract Executor.hooksFor(event, toolName) from Dispatch, so
Dispatch reads top-to-bottom as "collect → serialize → run →
aggregate". Drops the redundant len(matchers)==0 early-return
(the len(hooks)==0 check now covers both cases uniformly).
Assisted-By: docker-agent
gtardif
approved these changes
Apr 27, 2026
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 join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Same features, less plumbing. Two focused commits split between
pkg/hooksandpkg/runtime.1.
refactor(hooks): simplify executor caching and dispatch flowhooks.Executoronce inNewLocalRuntime(newbuildHooksExecutors). The map is read-only afterwards, so thehooksExecMusync.RWMutexand the double-checked locking dance go away.hooksExec(a)is now a plain map lookup.matcher.raw—pattern == nilnow means "match all", collapsingmatcher.matches()to a single boolean expression.Executor.Dispatchso it sits where the work actually happens. Drops the redundantexec.Has(event)early-check indispatchHook(Dispatch already short-circuits on the empty-event case).executeOnUserInputHooksusesr.CurrentAgent()instead of re-implementing the team lookup.builtins.AgentDefaults.IsZero()(and its test assertion).ApplyAgentDefaultsalready returns nil on the zero value, which is what callers actually rely on.2.
refactor(hooks): collapse paired notification helpers; extract hooksForexecuteNotificationHooks+executeOnErrorHooks+executeOnMaxIterationsHooks→notifyError+notifyMaxIterations— two composite helpers that fire the (notification, structured) event pair atomically. The two events are always emitted together in practice, so collapsing them removes 2 lines per callsite inloop.goand surfaces intent ("notify error", "notify max iterations") instead of leaving the reader to mentally join two calls. A privatenotify()helper shares the(level, message)-shaped Input building."error"/"warning") inside the new helpers, which is stronger enforcement than the runtime branch.Executor.hooksFor(event, toolName)fromDispatch, soDispatchreads top-to-bottom as "collect → serialize → run → aggregate". Drops the redundantlen(matchers)==0early-return; thelen(hooks)==0check now covers both the no-matchers and no-tool-match cases uniformly.What stays the same
All hook events (12), all hook types (
command,builtin), all builtins (add_date,add_environment_info,add_prompt_files), the agent-flag auto-injection, the Claude-compatible Output protocol (bothDecision: "block"andHookSpecificOutput.PermissionDecision: "deny"), and the PreToolUse fail-closed security boundary are all preserved.Validation
go test ./...— all greenmise lint— 0 issuesDiff stat
Assisted-By: docker-agent