diff --git a/cmd/entire/cli/agent/codex/hooks.go b/cmd/entire/cli/agent/codex/hooks.go index 0976adf96..df94575cf 100644 --- a/cmd/entire/cli/agent/codex/hooks.go +++ b/cmd/entire/cli/agent/codex/hooks.go @@ -54,6 +54,7 @@ func (c *CodexAgent) InstallHooks(ctx context.Context, localDev bool, force bool // Parse event types we manage var sessionStart, userPromptSubmit, stop []MatcherGroup + var preTool, postTool []MatcherGroup if err := parseHookType(rawHooks, "SessionStart", &sessionStart); err != nil { return 0, err } @@ -63,11 +64,19 @@ func (c *CodexAgent) InstallHooks(ctx context.Context, localDev bool, force bool if err := parseHookType(rawHooks, "Stop", &stop); err != nil { return 0, err } + if err := parseHookType(rawHooks, "PreToolUse", &preTool); err != nil { + return 0, err + } + if err := parseHookType(rawHooks, "PostToolUse", &postTool); err != nil { + return 0, err + } if force { sessionStart = removeEntireHooks(sessionStart) userPromptSubmit = removeEntireHooks(userPromptSubmit) stop = removeEntireHooks(stop) + preTool = removeEntireHooks(preTool) + postTool = removeEntireHooks(postTool) } // Build hook commands @@ -80,6 +89,8 @@ func (c *CodexAgent) InstallHooks(ctx context.Context, localDev bool, force bool sessionStartCmd := cmdPrefix + "session-start" userPromptSubmitCmd := cmdPrefix + "user-prompt-submit" stopCmd := cmdPrefix + "stop" + preToolCmd := cmdPrefix + "pre-tool-use" + postToolCmd := cmdPrefix + "post-tool-use" count := 0 @@ -95,6 +106,14 @@ func (c *CodexAgent) InstallHooks(ctx context.Context, localDev bool, force bool stop = addHook(stop, stopCmd) count++ } + if !hookCommandExists(preTool, preToolCmd) { + preTool = addHook(preTool, preToolCmd) + count++ + } + if !hookCommandExists(postTool, postToolCmd) { + postTool = addHook(postTool, postToolCmd) + count++ + } if count == 0 { // Still ensure the feature flag is configured even if hooks @@ -109,6 +128,8 @@ func (c *CodexAgent) InstallHooks(ctx context.Context, localDev bool, force bool marshalHookType(rawHooks, "SessionStart", sessionStart) marshalHookType(rawHooks, "UserPromptSubmit", userPromptSubmit) marshalHookType(rawHooks, "Stop", stop) + marshalHookType(rawHooks, "PreToolUse", preTool) + marshalHookType(rawHooks, "PostToolUse", postTool) // Preserve existing top-level keys (e.g., $schema) by reusing the parsed file topLevel := make(map[string]json.RawMessage) @@ -174,6 +195,7 @@ func (c *CodexAgent) UninstallHooks(ctx context.Context) error { } var sessionStart, userPromptSubmit, stop []MatcherGroup + var preTool, postTool []MatcherGroup if err := parseHookType(rawHooks, "SessionStart", &sessionStart); err != nil { return err } @@ -183,14 +205,24 @@ func (c *CodexAgent) UninstallHooks(ctx context.Context) error { if err := parseHookType(rawHooks, "Stop", &stop); err != nil { return err } + if err := parseHookType(rawHooks, "PreToolUse", &preTool); err != nil { + return err + } + if err := parseHookType(rawHooks, "PostToolUse", &postTool); err != nil { + return err + } sessionStart = removeEntireHooks(sessionStart) userPromptSubmit = removeEntireHooks(userPromptSubmit) stop = removeEntireHooks(stop) + preTool = removeEntireHooks(preTool) + postTool = removeEntireHooks(postTool) marshalHookType(rawHooks, "SessionStart", sessionStart) marshalHookType(rawHooks, "UserPromptSubmit", userPromptSubmit) marshalHookType(rawHooks, "Stop", stop) + marshalHookType(rawHooks, "PreToolUse", preTool) + marshalHookType(rawHooks, "PostToolUse", postTool) if len(rawHooks) > 0 { hooksJSON, err := json.Marshal(rawHooks) @@ -232,7 +264,9 @@ func (c *CodexAgent) AreHooksInstalled(ctx context.Context) bool { return hasEntireHook(hooksFile.Hooks.SessionStart) && hasEntireHook(hooksFile.Hooks.UserPromptSubmit) && - hasEntireHook(hooksFile.Hooks.Stop) + hasEntireHook(hooksFile.Hooks.Stop) && + hasEntireHook(hooksFile.Hooks.PreToolUse) && + hasEntireHook(hooksFile.Hooks.PostToolUse) } // --- Helpers --- diff --git a/cmd/entire/cli/agent/codex/hooks_test.go b/cmd/entire/cli/agent/codex/hooks_test.go index d8d1ac797..c092a9f49 100644 --- a/cmd/entire/cli/agent/codex/hooks_test.go +++ b/cmd/entire/cli/agent/codex/hooks_test.go @@ -25,7 +25,7 @@ func TestInstallHooks_CreatesConfig(t *testing.T) { ag := &CodexAgent{} count, err := ag.InstallHooks(context.Background(), false, false) require.NoError(t, err) - require.Equal(t, 3, count) // SessionStart, UserPromptSubmit, Stop + require.Equal(t, 5, count) // SessionStart, UserPromptSubmit, Stop, PreToolUse, PostToolUse // Verify hooks.json was created in the repo hooksPath := filepath.Join(tempDir, ".codex", HooksFileName) @@ -50,7 +50,7 @@ func TestInstallHooks_Idempotent(t *testing.T) { count1, err := ag.InstallHooks(context.Background(), false, false) require.NoError(t, err) - require.Equal(t, 3, count1) + require.Equal(t, 5, count1) count2, err := ag.InstallHooks(context.Background(), false, false) require.NoError(t, err) @@ -63,7 +63,7 @@ func TestInstallHooks_LocalDev(t *testing.T) { ag := &CodexAgent{} count, err := ag.InstallHooks(context.Background(), true, false) require.NoError(t, err) - require.Equal(t, 3, count) + require.Equal(t, 5, count) hooksPath := filepath.Join(tempDir, ".codex", HooksFileName) data, err := os.ReadFile(hooksPath) @@ -81,7 +81,7 @@ func TestInstallHooks_Force(t *testing.T) { count, err := ag.InstallHooks(context.Background(), false, true) require.NoError(t, err) - require.Equal(t, 3, count) + require.Equal(t, 5, count) } func TestUninstallHooks(t *testing.T) { diff --git a/cmd/entire/cli/agent/codex/lifecycle.go b/cmd/entire/cli/agent/codex/lifecycle.go index 4d7b1d3a5..a1c345b84 100644 --- a/cmd/entire/cli/agent/codex/lifecycle.go +++ b/cmd/entire/cli/agent/codex/lifecycle.go @@ -35,6 +35,7 @@ const ( HookNameUserPromptSubmit = "user-prompt-submit" HookNameStop = "stop" HookNamePreToolUse = "pre-tool-use" + HookNamePostToolUse = "post-tool-use" ) // HookNames returns the hook verbs Codex supports. @@ -44,6 +45,7 @@ func (c *CodexAgent) HookNames() []string { HookNameUserPromptSubmit, HookNameStop, HookNamePreToolUse, + HookNamePostToolUse, } } @@ -57,8 +59,8 @@ func (c *CodexAgent) ParseHookEvent(_ context.Context, hookName string, stdin io return c.parseTurnStart(stdin) case HookNameStop: return c.parseTurnEnd(stdin) - case HookNamePreToolUse: - // PreToolUse has no lifecycle significance — pass through + case HookNamePreToolUse, HookNamePostToolUse: + // Acknowledged hooks with no lifecycle action return nil, nil //nolint:nilnil // nil event = no lifecycle action default: return nil, nil //nolint:nilnil // Unknown hooks have no lifecycle action diff --git a/cmd/entire/cli/agent/codex/lifecycle_test.go b/cmd/entire/cli/agent/codex/lifecycle_test.go index cf95f18b9..f7464a950 100644 --- a/cmd/entire/cli/agent/codex/lifecycle_test.go +++ b/cmd/entire/cli/agent/codex/lifecycle_test.go @@ -98,13 +98,26 @@ func TestParseHookEvent_Stop(t *testing.T) { require.Equal(t, "/tmp/rollout.jsonl", event.SessionRef) } -func TestParseHookEvent_PreToolUse_ReturnsNil(t *testing.T) { +func TestParseHookEvent_PassThroughHooks_ReturnsNil(t *testing.T) { t.Parallel() + + passThroughHooks := []string{ + HookNamePreToolUse, + HookNamePostToolUse, + } + ag := &CodexAgent{} // PreToolUse is a pass-through — should return nil event - event, err := ag.ParseHookEvent(context.Background(), HookNamePreToolUse, strings.NewReader("{}")) - require.NoError(t, err) - require.Nil(t, event) + + for _, hookName := range passThroughHooks { + t.Run(hookName, func(t *testing.T) { + t.Parallel() + + event, err := ag.ParseHookEvent(context.Background(), hookName, strings.NewReader("{}")) + require.NoError(t, err) + require.Nil(t, event) + }) + } } func TestParseHookEvent_UnknownHook_ReturnsNil(t *testing.T) { diff --git a/cmd/entire/cli/agent/codex/types.go b/cmd/entire/cli/agent/codex/types.go index af124b85d..6356f6454 100644 --- a/cmd/entire/cli/agent/codex/types.go +++ b/cmd/entire/cli/agent/codex/types.go @@ -11,6 +11,7 @@ type HookEvents struct { UserPromptSubmit []MatcherGroup `json:"UserPromptSubmit,omitempty"` Stop []MatcherGroup `json:"Stop,omitempty"` PreToolUse []MatcherGroup `json:"PreToolUse,omitempty"` + PostToolUse []MatcherGroup `json:"PostToolUse,omitempty"` } // MatcherGroup groups hooks under an optional matcher pattern.