Skip to content

fix: preserve unknown Claude Code hook types in entire enable#310

Open
Ashwinhegde19 wants to merge 1 commit intoentireio:mainfrom
Ashwinhegde19:fix-308-preserve-hooks
Open

fix: preserve unknown Claude Code hook types in entire enable#310
Ashwinhegde19 wants to merge 1 commit intoentireio:mainfrom
Ashwinhegde19:fix-308-preserve-hooks

Conversation

@Ashwinhegde19
Copy link

Summary

Fixes #308 - entire enable was silently dropping unknown Claude Code hook types (PreCompact, Notification, SubagentStart, etc.)

Problem

The ClaudeHooks struct only defines 6 hook types, but Claude Code supports 14+. When json.Unmarshal runs, Go silently discards unknown fields, causing permanent data loss.

Solution

Use rawClaudeHooks (map[string]json.RawMessage) to preserve all hook types while modifying only the 6 known hooks. Same pattern already used for top-level settings.

Changes

  • types.go: Added rawClaudeHooks type
  • hooks.go: Updated InstallHooks() and UninstallHooks() to preserve unknown hooks

Testing

  • Created test settings with PreCompact hook
  • Ran entire enable
  • ✅ PreCompact preserved (not deleted)

Checklist

Fixes #308

@Ashwinhegde19 Ashwinhegde19 requested a review from a team as a code owner February 12, 2026 19:20
Copilot AI review requested due to automatic review settings February 12, 2026 19:20
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes a critical data loss bug where entire enable was silently dropping unknown Claude Code hook types (like PreCompact, Notification, SubagentStart, etc.). The fix uses a map[string]json.RawMessage pattern to preserve unknown hook types while only modifying the 6 known hooks that Entire supports.

Changes:

  • Added rawClaudeHooks type for preserving unknown hook fields during JSON marshaling
  • Modified InstallHooks() to preserve unknown hooks using the raw map pattern
  • Modified UninstallHooks() to preserve unknown hooks using the same pattern

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
cmd/entire/cli/agent/claudecode/types.go Adds rawClaudeHooks type definition for preserving unknown JSON fields
cmd/entire/cli/agent/claudecode/hooks.go Updates InstallHooks and UninstallHooks to use rawClaudeHooks pattern to preserve unknown hook types
Comments suppressed due to low confidence (2)

cmd/entire/cli/agent/claudecode/hooks.go:290

  • The rawHooks variable is declared but might remain nil if there's no "hooks" key in rawSettings. This will cause a panic at line 359 when trying to assign to rawHooks[hookName]. Initialize rawHooks to an empty map if it's not present, similar to the pattern in InstallHooks at lines 206-208.
	var rawHooks rawClaudeHooks
	
	if hooksRaw, ok := rawSettings["hooks"]; ok {
		// Unmarshal into rawClaudeHooks to preserve unknown fields
		if err := json.Unmarshal(hooksRaw, &rawHooks); err != nil {
			return fmt.Errorf("failed to parse hooks: %w", err)
		}
		// Also unmarshal into settings.Hooks for the known fields
		if err := json.Unmarshal(hooksRaw, &settings.Hooks); err != nil {
			return fmt.Errorf("failed to parse hooks: %w", err)
		}
	}

cmd/entire/cli/agent/claudecode/hooks.go:290

  • Add a test case to verify that UninstallHooks also preserves unknown hook types. The test should create settings with an unknown hook type (e.g., PreCompact) and an Entire hook, run UninstallHooks, and verify that the unknown hook is still present while the Entire hook is removed.
	if hooksRaw, ok := rawSettings["hooks"]; ok {
		// Unmarshal into rawClaudeHooks to preserve unknown fields
		if err := json.Unmarshal(hooksRaw, &rawHooks); err != nil {
			return fmt.Errorf("failed to parse hooks: %w", err)
		}
		// Also unmarshal into settings.Hooks for the known fields
		if err := json.Unmarshal(hooksRaw, &settings.Hooks); err != nil {
			return fmt.Errorf("failed to parse hooks: %w", err)
		}
	}

Comment on lines +219 to +224

for hookName, hookValue := range knownHooks {
hooksJSON, err := json.Marshal(hookValue)
if err != nil {
return 0, fmt.Errorf("failed to marshal %s hooks: %w", hookName, err)
}
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

This code unconditionally adds all known hook types to rawHooks, even if they're empty arrays or nil. This could add hook types that weren't present in the original settings. Consider using the omitempty behavior by checking if the hook array is empty before adding it to rawHooks, or only updating hooks that already exist in rawHooks.

Suggested change
for hookName, hookValue := range knownHooks {
hooksJSON, err := json.Marshal(hookValue)
if err != nil {
return 0, fmt.Errorf("failed to marshal %s hooks: %w", hookName, err)
}
for hookName, hookValue := range knownHooks {
hooksJSON, err := json.Marshal(hookValue)
if err != nil {
return 0, fmt.Errorf("failed to marshal %s hooks: %w", hookName, err)
}
if _, exists := rawHooks[hookName]; !exists {
// Emulate omitempty behavior for new hook types: don't add empty/nil hooks
if string(hooksJSON) == "null" || string(hooksJSON) == "[]" {
continue
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +354 to +360
for hookName, hookValue := range knownHooks {
hooksJSON, err := json.Marshal(hookValue)
if err != nil {
return fmt.Errorf("failed to marshal %s hooks: %w", hookName, err)
}
rawHooks[hookName] = hooksJSON
}
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

Same issue as in InstallHooks - this unconditionally adds all known hook types to rawHooks, even if they're empty. This could introduce hook types that weren't present in the original settings.

Copilot uses AI. Check for mistakes.
Comment on lines +345 to +360
knownHooks := map[string]interface{}{
"SessionStart": settings.Hooks.SessionStart,
"SessionEnd": settings.Hooks.SessionEnd,
"UserPromptSubmit": settings.Hooks.UserPromptSubmit,
"Stop": settings.Hooks.Stop,
"PreToolUse": settings.Hooks.PreToolUse,
"PostToolUse": settings.Hooks.PostToolUse,
}

for hookName, hookValue := range knownHooks {
hooksJSON, err := json.Marshal(hookValue)
if err != nil {
return fmt.Errorf("failed to marshal %s hooks: %w", hookName, err)
}
rawHooks[hookName] = hooksJSON
}
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

This marshaling logic is duplicated between InstallHooks (lines 210-226) and UninstallHooks (lines 345-360). Consider extracting this into a helper function that takes settings.Hooks and rawHooks and returns the merged result. This would reduce code duplication and make the logic easier to maintain.

Copilot uses AI. Check for mistakes.
Comment on lines +90 to +100
// First unmarshal into rawClaudeHooks to preserve unknown fields
var rawHooks rawClaudeHooks
if err := json.Unmarshal(hooksRaw, &rawHooks); err != nil {
return 0, fmt.Errorf("failed to parse hooks in settings.json: %w", err)
}
// Then unmarshal into settings.Hooks for the known fields
if err := json.Unmarshal(hooksRaw, &settings.Hooks); err != nil {
return 0, fmt.Errorf("failed to parse hooks in settings.json: %w", err)
}
// Store raw hooks for later preservation
rawSettings["hooks_raw"] = hooksRaw
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

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

The PR description mentions testing with a PreCompact hook to verify unknown hooks are preserved, but there's no test case in the test file that verifies this behavior. Add a test similar to TestInstallHooks_PermissionsDeny_PreservesUnknownFields but for unknown hook types. This test should create settings with an unknown hook type like PreCompact, run InstallHooks, and verify the unknown hook is still present in the output.

Copilot uses AI. Check for mistakes.
@Ashwinhegde19
Copy link
Author

Fixed the nil pointer issue identified by Copilot.

Change: Added nil check for rawHooks in UninstallHooks():

This prevents a panic when rawHooks is nil and we try to assign to it.

The InstallHooks and UninstallHooks functions were silently dropping
any Claude Code hook types not defined in the ClaudeHooks struct.
This caused hooks like PreCompact, Notification, SubagentStart, etc.
to be deleted when running 'entire enable'.

Fix by using rawClaudeHooks (map[string]json.RawMessage) to preserve
all hook types, similar to how rawSettings preserves unknown top-level
fields. Only modify the 6 known hook types while keeping all others.

Fixes entireio#308
@Soph
Copy link
Collaborator

Soph commented Feb 12, 2026

hey @Ashwinhegde19 I'm sorry but I put this on my todo list after seeing the issue earlier today and have a PR open too #314 it's also covering the same issue for the gemini config. Thanks for your effort !!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

entire enable silently drops unknown Claude Code hook types (e.g. PreCompact)

2 participants