Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion cmd/entire/cli/agent/codex/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
Expand All @@ -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

Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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
}
Expand All @@ -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)
Expand Down Expand Up @@ -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 ---
Expand Down
8 changes: 4 additions & 4 deletions cmd/entire/cli/agent/codex/hooks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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) {
Expand Down
6 changes: 4 additions & 2 deletions cmd/entire/cli/agent/codex/lifecycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -44,6 +45,7 @@ func (c *CodexAgent) HookNames() []string {
HookNameUserPromptSubmit,
HookNameStop,
HookNamePreToolUse,
HookNamePostToolUse,
}
}

Expand All @@ -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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's where the real PR meat starts. Happy for you to take over and add an implementation that I can review later on.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Okay, let's kick off.

return nil, nil //nolint:nilnil // nil event = no lifecycle action
default:
return nil, nil //nolint:nilnil // Unknown hooks have no lifecycle action
Expand Down
21 changes: 17 additions & 4 deletions cmd/entire/cli/agent/codex/lifecycle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
1 change: 1 addition & 0 deletions cmd/entire/cli/agent/codex/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down