fix: preserve unknown Claude Code hook types in entire enable#310
fix: preserve unknown Claude Code hook types in entire enable#310Ashwinhegde19 wants to merge 1 commit intoentireio:mainfrom
Conversation
There was a problem hiding this comment.
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
rawClaudeHookstype 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)
}
}
|
|
||
| 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) | ||
| } |
There was a problem hiding this comment.
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.
| 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 | |
| } | |
| } |
| 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 | ||
| } |
There was a problem hiding this comment.
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.
| 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 | ||
| } |
There was a problem hiding this comment.
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.
| // 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 |
There was a problem hiding this comment.
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.
b46062b to
432d538
Compare
|
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
432d538 to
f36982d
Compare
|
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 !! |
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
Testing
Checklist
Fixes #308