Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
47 changes: 26 additions & 21 deletions cmd/entire/cli/agent/claudecode/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,13 +123,13 @@ func (c *ClaudeCodeAgent) InstallHooks(ctx context.Context, localDev bool, force
postTaskCmd = "go run ${CLAUDE_PROJECT_DIR}/cmd/entire/main.go hooks claude-code post-task"
postTodoCmd = "go run ${CLAUDE_PROJECT_DIR}/cmd/entire/main.go hooks claude-code post-todo"
} else {
sessionStartCmd = "entire hooks claude-code session-start"
sessionEndCmd = "entire hooks claude-code session-end"
stopCmd = "entire hooks claude-code stop"
userPromptSubmitCmd = "entire hooks claude-code user-prompt-submit"
preTaskCmd = "entire hooks claude-code pre-task"
postTaskCmd = "entire hooks claude-code post-task"
postTodoCmd = "entire hooks claude-code post-todo"
sessionStartCmd = agent.WrapProductionJSONSessionStartHookCommand("entire hooks claude-code session-start", agent.WarningFormatMultiLine)
sessionEndCmd = agent.WrapProductionSilentHookCommand("entire hooks claude-code session-end")
stopCmd = agent.WrapProductionSilentHookCommand("entire hooks claude-code stop")
userPromptSubmitCmd = agent.WrapProductionSilentHookCommand("entire hooks claude-code user-prompt-submit")
preTaskCmd = agent.WrapProductionSilentHookCommand("entire hooks claude-code pre-task")
postTaskCmd = agent.WrapProductionSilentHookCommand("entire hooks claude-code post-task")
postTodoCmd = agent.WrapProductionSilentHookCommand("entire hooks claude-code post-todo")
}

count := 0
Expand Down Expand Up @@ -195,14 +195,14 @@ func (c *ClaudeCodeAgent) InstallHooks(ctx context.Context, localDev bool, force
marshalHookType(rawHooks, "PostToolUse", postToolUse)

// Marshal hooks and update raw settings
hooksJSON, err := json.Marshal(rawHooks)
hooksJSON, err := jsonutil.MarshalWithNoHTMLEscape(rawHooks)
if err != nil {
return 0, fmt.Errorf("failed to marshal hooks: %w", err)
}
rawSettings["hooks"] = hooksJSON

// Marshal permissions and update raw settings
permJSON, err := json.Marshal(rawPermissions)
permJSON, err := jsonutil.MarshalWithNoHTMLEscape(rawPermissions)
if err != nil {
return 0, fmt.Errorf("failed to marshal permissions: %w", err)
}
Expand Down Expand Up @@ -241,7 +241,7 @@ func marshalHookType(rawHooks map[string]json.RawMessage, hookType string, match
delete(rawHooks, hookType)
return
}
data, err := json.Marshal(matchers)
data, err := jsonutil.MarshalWithNoHTMLEscape(matchers)
if err != nil {
return // Silently ignore marshal errors (shouldn't happen)
}
Expand Down Expand Up @@ -336,7 +336,7 @@ func (c *ClaudeCodeAgent) UninstallHooks(ctx context.Context) error {

// If permissions is empty, remove it entirely
if len(rawPermissions) > 0 {
permJSON, err := json.Marshal(rawPermissions)
permJSON, err := jsonutil.MarshalWithNoHTMLEscape(rawPermissions)
if err == nil {
rawSettings["permissions"] = permJSON
}
Expand All @@ -347,7 +347,7 @@ func (c *ClaudeCodeAgent) UninstallHooks(ctx context.Context) error {

// Marshal hooks back (preserving unknown hook types)
if len(rawHooks) > 0 {
hooksJSON, err := json.Marshal(rawHooks)
hooksJSON, err := jsonutil.MarshalWithNoHTMLEscape(rawHooks)
if err != nil {
return fmt.Errorf("failed to marshal hooks: %w", err)
}
Expand Down Expand Up @@ -385,14 +385,8 @@ func (c *ClaudeCodeAgent) AreHooksInstalled(ctx context.Context) bool {
return false
}

// Check for at least one of our hooks (new or old format)
return hookCommandExists(settings.Hooks.Stop, "entire hooks claude-code stop") ||
hookCommandExists(settings.Hooks.Stop, "go run ${CLAUDE_PROJECT_DIR}/cmd/entire/main.go hooks claude-code stop") ||
// Backwards compatibility: check for old hook formats
hookCommandExists(settings.Hooks.Stop, "entire hooks claudecode stop") ||
hookCommandExists(settings.Hooks.Stop, "go run ${CLAUDE_PROJECT_DIR}/cmd/entire/main.go hooks claudecode stop") ||
hookCommandExists(settings.Hooks.Stop, "entire rewind claude-hook --stop") ||
hookCommandExists(settings.Hooks.Stop, "go run ${CLAUDE_PROJECT_DIR}/cmd/entire/main.go rewind claude-hook --stop")
// Check for at least one of our hooks (new, wrapped, or legacy format)
return hasEntireHook(settings.Hooks.Stop)
}

// Helper functions for hook management
Expand All @@ -408,6 +402,17 @@ func hookCommandExists(matchers []ClaudeHookMatcher, command string) bool {
return false
}

func hasEntireHook(matchers []ClaudeHookMatcher) bool {
for _, matcher := range matchers {
for _, hook := range matcher.Hooks {
if isEntireHook(hook.Command) {
return true
}
}
}
return false
}

func hookCommandExistsWithMatcher(matchers []ClaudeHookMatcher, matcherName, command string) bool {
for _, matcher := range matchers {
if matcher.Matcher == matcherName {
Expand Down Expand Up @@ -458,7 +463,7 @@ func addHookToMatcher(matchers []ClaudeHookMatcher, matcherName, command string)
// isEntireHook checks if a command is an Entire hook (old or new format)
func isEntireHook(command string) bool {
for _, prefix := range entireHookPrefixes {
if strings.HasPrefix(command, prefix) {
if strings.Contains(command, prefix) {
return true
}
}
Expand Down
9 changes: 5 additions & 4 deletions cmd/entire/cli/agent/claudecode/hooks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"slices"
"testing"

agentpkg "github.com/entireio/cli/cmd/entire/cli/agent"
"github.com/entireio/cli/cmd/entire/cli/agent/testutil"
)

Expand Down Expand Up @@ -478,7 +479,7 @@ func TestInstallHooks_PreservesUserHooksOnSameType(t *testing.T) {
t.Fatalf("failed to parse Stop hooks: %v", err)
}
assertHookExists(t, matchers, "", "echo user stop hook", "user Stop hook")
assertHookExists(t, matchers, "", "entire hooks claude-code stop", "Entire Stop hook")
assertHookExists(t, matchers, "", agentpkg.WrapProductionSilentHookCommand("entire hooks claude-code stop"), "Entire Stop hook")
})

t.Run("SessionStart", func(t *testing.T) {
Expand All @@ -488,7 +489,7 @@ func TestInstallHooks_PreservesUserHooksOnSameType(t *testing.T) {
t.Fatalf("failed to parse SessionStart hooks: %v", err)
}
assertHookExists(t, matchers, "", "echo user session start", "user SessionStart hook")
assertHookExists(t, matchers, "", "entire hooks claude-code session-start", "Entire SessionStart hook")
assertHookExists(t, matchers, "", agentpkg.WrapProductionJSONSessionStartHookCommand("entire hooks claude-code session-start", agentpkg.WarningFormatMultiLine), "Entire SessionStart hook")
})

t.Run("PostToolUse", func(t *testing.T) {
Expand All @@ -498,8 +499,8 @@ func TestInstallHooks_PreservesUserHooksOnSameType(t *testing.T) {
t.Fatalf("failed to parse PostToolUse hooks: %v", err)
}
assertHookExists(t, matchers, "Write", "echo user wrote file", "user Write hook")
assertHookExists(t, matchers, "Task", "entire hooks claude-code post-task", "Entire Task hook")
assertHookExists(t, matchers, "TodoWrite", "entire hooks claude-code post-todo", "Entire TodoWrite hook")
assertHookExists(t, matchers, "Task", agentpkg.WrapProductionSilentHookCommand("entire hooks claude-code post-task"), "Entire Task hook")
assertHookExists(t, matchers, "TodoWrite", agentpkg.WrapProductionSilentHookCommand("entire hooks claude-code post-todo"), "Entire TodoWrite hook")
})
}

Expand Down
16 changes: 12 additions & 4 deletions cmd/entire/cli/agent/codex/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"path/filepath"
"strings"

"github.com/entireio/cli/cmd/entire/cli/agent"
"github.com/entireio/cli/cmd/entire/cli/jsonutil"
"github.com/entireio/cli/cmd/entire/cli/paths"
)
Expand Down Expand Up @@ -78,8 +79,15 @@ func (c *CodexAgent) InstallHooks(ctx context.Context, localDev bool, force bool
cmdPrefix = "entire hooks codex "
}
sessionStartCmd := cmdPrefix + "session-start"
if !localDev {
sessionStartCmd = agent.WrapProductionJSONSessionStartHookCommand(sessionStartCmd, agent.WarningFormatSingleLine)
}
userPromptSubmitCmd := cmdPrefix + "user-prompt-submit"
stopCmd := cmdPrefix + "stop"
if !localDev {
userPromptSubmitCmd = agent.WrapProductionSilentHookCommand(userPromptSubmitCmd)
stopCmd = agent.WrapProductionSilentHookCommand(stopCmd)
}

count := 0

Expand Down Expand Up @@ -116,7 +124,7 @@ func (c *CodexAgent) InstallHooks(ctx context.Context, localDev bool, force bool
// Re-parse the original file to preserve all top-level keys
_ = json.Unmarshal(existingData, &topLevel) //nolint:errcheck // best-effort preservation
}
hooksJSON, err := json.Marshal(rawHooks)
hooksJSON, err := jsonutil.MarshalWithNoHTMLEscape(rawHooks)
if err != nil {
return 0, fmt.Errorf("failed to marshal hooks: %w", err)
}
Expand Down Expand Up @@ -193,7 +201,7 @@ func (c *CodexAgent) UninstallHooks(ctx context.Context) error {
marshalHookType(rawHooks, "Stop", stop)

if len(rawHooks) > 0 {
hooksJSON, err := json.Marshal(rawHooks)
hooksJSON, err := jsonutil.MarshalWithNoHTMLEscape(rawHooks)
if err != nil {
return fmt.Errorf("failed to marshal hooks: %w", err)
}
Expand Down Expand Up @@ -251,7 +259,7 @@ func marshalHookType(rawHooks map[string]json.RawMessage, hookType string, group
delete(rawHooks, hookType)
return
}
data, err := json.Marshal(groups)
data, err := jsonutil.MarshalWithNoHTMLEscape(groups)
if err != nil {
return
}
Expand Down Expand Up @@ -291,7 +299,7 @@ func addHook(groups []MatcherGroup, command string) []MatcherGroup {

func isEntireHook(command string) bool {
for _, prefix := range entireHookPrefixes {
if strings.HasPrefix(command, prefix) {
if strings.Contains(command, prefix) {
return true
}
}
Expand Down
2 changes: 2 additions & 0 deletions cmd/entire/cli/agent/codex/hooks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ func TestInstallHooks_CreatesConfig(t *testing.T) {
hooksPath := filepath.Join(tempDir, ".codex", HooksFileName)
data, err := os.ReadFile(hooksPath)
require.NoError(t, err)
require.Contains(t, string(data), "Powered by Entire: Tracking is enabled")
require.Contains(t, string(data), "installation-methods")
require.Contains(t, string(data), "entire hooks codex session-start")
require.Contains(t, string(data), "entire hooks codex user-prompt-submit")
require.Contains(t, string(data), "entire hooks codex stop")
Expand Down
13 changes: 9 additions & 4 deletions cmd/entire/cli/agent/copilotcli/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,11 @@ func (c *CopilotCLIAgent) InstallHooks(ctx context.Context, localDev bool, force
// Add hooks that don't already exist
for _, hookName := range c.HookNames() {
cmd := cmdPrefix + hookName
if !localDev && hookName == HookNameSessionStart {
cmd = agent.WrapProductionSessionStartHookCommand(cmd, agent.WarningFormatSingleLine)
} else if !localDev {
cmd = agent.WrapProductionSilentHookCommand(cmd)
}
entries := hookEntries[hookName]
if !hookBashExists(entries, cmd) {
entries = append(entries, CopilotHookEntry{
Expand All @@ -137,7 +142,7 @@ func (c *CopilotCLIAgent) InstallHooks(ctx context.Context, localDev bool, force
}

// Marshal hooks and update raw file
hooksJSON, err := json.Marshal(rawHooks)
hooksJSON, err := jsonutil.MarshalWithNoHTMLEscape(rawHooks)
if err != nil {
return 0, fmt.Errorf("failed to marshal hooks: %w", err)
}
Expand Down Expand Up @@ -206,7 +211,7 @@ func (c *CopilotCLIAgent) UninstallHooks(ctx context.Context) error {

// Marshal hooks back (preserving unknown hook types)
if len(rawHooks) > 0 {
hooksJSON, err := json.Marshal(rawHooks)
hooksJSON, err := jsonutil.MarshalWithNoHTMLEscape(rawHooks)
if err != nil {
return fmt.Errorf("failed to marshal hooks: %w", err)
}
Expand Down Expand Up @@ -292,7 +297,7 @@ func marshalCopilotHookType(rawHooks map[string]json.RawMessage, hookType string
delete(rawHooks, hookType)
return nil
}
data, err := json.Marshal(entries)
data, err := jsonutil.MarshalWithNoHTMLEscape(entries)
if err != nil {
return fmt.Errorf("failed to marshal hook type %s: %w", hookType, err)
}
Expand All @@ -313,7 +318,7 @@ func hookBashExists(entries []CopilotHookEntry, bash string) bool {
// isEntireHook checks if a hook entry's bash command belongs to Entire.
func isEntireHook(bash string) bool {
for _, prefix := range entireHookPrefixes {
if strings.HasPrefix(bash, prefix) {
if strings.Contains(bash, prefix) {
return true
}
}
Expand Down
21 changes: 11 additions & 10 deletions cmd/entire/cli/agent/copilotcli/hooks_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"strings"
"testing"

"github.com/entireio/cli/cmd/entire/cli/agent"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -59,14 +60,14 @@ func TestInstallHooks_FreshInstall(t *testing.T) {
}

// Verify commands use bash field and type is "command"
assertEntryBash(t, hooksFile.Hooks.UserPromptSubmitted, "entire hooks copilot-cli user-prompt-submitted")
assertEntryBash(t, hooksFile.Hooks.SessionStart, "entire hooks copilot-cli session-start")
assertEntryBash(t, hooksFile.Hooks.AgentStop, "entire hooks copilot-cli agent-stop")
assertEntryBash(t, hooksFile.Hooks.SessionEnd, "entire hooks copilot-cli session-end")
assertEntryBash(t, hooksFile.Hooks.SubagentStop, "entire hooks copilot-cli subagent-stop")
assertEntryBash(t, hooksFile.Hooks.PreToolUse, "entire hooks copilot-cli pre-tool-use")
assertEntryBash(t, hooksFile.Hooks.PostToolUse, "entire hooks copilot-cli post-tool-use")
assertEntryBash(t, hooksFile.Hooks.ErrorOccurred, "entire hooks copilot-cli error-occurred")
assertEntryBash(t, hooksFile.Hooks.UserPromptSubmitted, agent.WrapProductionSilentHookCommand("entire hooks copilot-cli user-prompt-submitted"))
assertEntryBash(t, hooksFile.Hooks.SessionStart, agent.WrapProductionSessionStartHookCommand("entire hooks copilot-cli session-start", agent.WarningFormatSingleLine))
assertEntryBash(t, hooksFile.Hooks.AgentStop, agent.WrapProductionSilentHookCommand("entire hooks copilot-cli agent-stop"))
assertEntryBash(t, hooksFile.Hooks.SessionEnd, agent.WrapProductionSilentHookCommand("entire hooks copilot-cli session-end"))
assertEntryBash(t, hooksFile.Hooks.SubagentStop, agent.WrapProductionSilentHookCommand("entire hooks copilot-cli subagent-stop"))
assertEntryBash(t, hooksFile.Hooks.PreToolUse, agent.WrapProductionSilentHookCommand("entire hooks copilot-cli pre-tool-use"))
assertEntryBash(t, hooksFile.Hooks.PostToolUse, agent.WrapProductionSilentHookCommand("entire hooks copilot-cli post-tool-use"))
assertEntryBash(t, hooksFile.Hooks.ErrorOccurred, agent.WrapProductionSilentHookCommand("entire hooks copilot-cli error-occurred"))

// Verify type field is "command"
assertEntryType(t, hooksFile.Hooks.AgentStop, "command")
Expand Down Expand Up @@ -226,7 +227,7 @@ func TestInstallHooks_PreservesExistingHooks(t *testing.T) {
t.Errorf("AgentStop hooks = %d, want 2 (user + entire)", len(hooksFile.Hooks.AgentStop))
}
assertEntryBash(t, hooksFile.Hooks.AgentStop, "echo user hook")
assertEntryBash(t, hooksFile.Hooks.AgentStop, "entire hooks copilot-cli agent-stop")
assertEntryBash(t, hooksFile.Hooks.AgentStop, agent.WrapProductionSilentHookCommand("entire hooks copilot-cli agent-stop"))
}

func TestInstallHooks_LocalDev(t *testing.T) {
Expand Down Expand Up @@ -312,7 +313,7 @@ func TestInstallHooks_PreservesUnknownFields(t *testing.T) {
t.Errorf("agentStop hooks = %d, want 2 (user + entire)", len(agentStopHooks))
}
assertEntryBash(t, agentStopHooks, "echo user stop")
assertEntryBash(t, agentStopHooks, "entire hooks copilot-cli agent-stop")
assertEntryBash(t, agentStopHooks, agent.WrapProductionSilentHookCommand("entire hooks copilot-cli agent-stop"))
}

func TestUninstallHooks_PreservesUnknownFields(t *testing.T) {
Expand Down
19 changes: 15 additions & 4 deletions cmd/entire/cli/agent/cursor/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,23 @@ func (c *CursorAgent) InstallHooks(ctx context.Context, localDev bool, force boo
}

sessionStartCmd := cmdPrefix + HookNameSessionStart
if !localDev {
sessionStartCmd = agent.WrapProductionSessionStartHookCommand(sessionStartCmd, agent.WarningFormatSingleLine)
}
sessionEndCmd := cmdPrefix + HookNameSessionEnd
beforeSubmitPromptCmd := cmdPrefix + HookNameBeforeSubmitPrompt
stopCmd := cmdPrefix + HookNameStop
preCompactCmd := cmdPrefix + HookNamePreCompact
subagentStartCmd := cmdPrefix + HookNameSubagentStart
subagentEndCmd := cmdPrefix + HookNameSubagentStop
if !localDev {
sessionEndCmd = agent.WrapProductionSilentHookCommand(sessionEndCmd)
beforeSubmitPromptCmd = agent.WrapProductionSilentHookCommand(beforeSubmitPromptCmd)
stopCmd = agent.WrapProductionSilentHookCommand(stopCmd)
preCompactCmd = agent.WrapProductionSilentHookCommand(preCompactCmd)
subagentStartCmd = agent.WrapProductionSilentHookCommand(subagentStartCmd)
subagentEndCmd = agent.WrapProductionSilentHookCommand(subagentEndCmd)
}

count := 0

Expand Down Expand Up @@ -174,7 +185,7 @@ func (c *CursorAgent) InstallHooks(ctx context.Context, localDev bool, force boo
marshalCursorHookType(rawHooks, "subagentStop", subagentStop)

// Marshal hooks and update raw file
hooksJSON, err := json.Marshal(rawHooks)
hooksJSON, err := jsonutil.MarshalWithNoHTMLEscape(rawHooks)
if err != nil {
return 0, fmt.Errorf("failed to marshal hooks: %w", err)
}
Expand Down Expand Up @@ -255,7 +266,7 @@ func (c *CursorAgent) UninstallHooks(ctx context.Context) error {

// Marshal hooks back (preserving unknown hook types)
if len(rawHooks) > 0 {
hooksJSON, err := json.Marshal(rawHooks)
hooksJSON, err := jsonutil.MarshalWithNoHTMLEscape(rawHooks)
if err != nil {
return fmt.Errorf("failed to marshal hooks: %w", err)
}
Expand Down Expand Up @@ -331,7 +342,7 @@ func marshalCursorHookType(rawHooks map[string]json.RawMessage, hookType string,
delete(rawHooks, hookType)
return
}
data, err := json.Marshal(entries)
data, err := jsonutil.MarshalWithNoHTMLEscape(entries)
if err != nil {
return // Silently ignore marshal errors (shouldn't happen)
}
Expand All @@ -351,7 +362,7 @@ func hookCommandExists(entries []CursorHookEntry, command string) bool {

func isEntireHook(command string) bool {
for _, prefix := range entireHookPrefixes {
if strings.HasPrefix(command, prefix) {
if strings.Contains(command, prefix) {
return true
}
}
Expand Down
Loading
Loading