-
Notifications
You must be signed in to change notification settings - Fork 160
fix: preserve unknown Claude Code hook types in entire enable #310
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,88 +1,16 @@ | ||
| { | ||
| "hooks": { | ||
| "SessionStart": [ | ||
| "PreCompact": [ | ||
| { | ||
| "matcher": "", | ||
| "matcher": "*", | ||
| "hooks": [ | ||
| { | ||
| "type": "command", | ||
| "command": "bash ${CLAUDE_PROJECT_DIR}/.claude/scripts/remote-setup.sh" | ||
| }, | ||
| { | ||
| "type": "command", | ||
| "command": "go run ${CLAUDE_PROJECT_DIR}/cmd/entire/main.go hooks claude-code session-start" | ||
| } | ||
| ] | ||
| } | ||
| ], | ||
| "SessionEnd": [ | ||
| { | ||
| "matcher": "", | ||
| "hooks": [ | ||
| { | ||
| "type": "command", | ||
| "command": "go run ${CLAUDE_PROJECT_DIR}/cmd/entire/main.go hooks claude-code session-end" | ||
| } | ||
| ] | ||
| } | ||
| ], | ||
| "UserPromptSubmit": [ | ||
| { | ||
| "matcher": "", | ||
| "hooks": [ | ||
| { | ||
| "type": "command", | ||
| "command": "go run ${CLAUDE_PROJECT_DIR}/cmd/entire/main.go hooks claude-code user-prompt-submit" | ||
| "command": "echo 'compacting session'" | ||
| } | ||
| ] | ||
| } | ||
| ], | ||
| "Stop": [ | ||
| { | ||
| "matcher": "", | ||
| "hooks": [ | ||
| { | ||
| "type": "command", | ||
| "command": "go run ${CLAUDE_PROJECT_DIR}/cmd/entire/main.go hooks claude-code stop" | ||
| } | ||
| ] | ||
| } | ||
| ], | ||
| "PreToolUse": [ | ||
| { | ||
| "matcher": "Task", | ||
| "hooks": [ | ||
| { | ||
| "type": "command", | ||
| "command": "go run ${CLAUDE_PROJECT_DIR}/cmd/entire/main.go hooks claude-code pre-task" | ||
| } | ||
| ] | ||
| } | ||
| ], | ||
| "PostToolUse": [ | ||
| { | ||
| "matcher": "Task", | ||
| "hooks": [ | ||
| { | ||
| "type": "command", | ||
| "command": "go run ${CLAUDE_PROJECT_DIR}/cmd/entire/main.go hooks claude-code post-task" | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "matcher": "TodoWrite", | ||
| "hooks": [ | ||
| { | ||
| "type": "command", | ||
| "command": "go run ${CLAUDE_PROJECT_DIR}/cmd/entire/main.go hooks claude-code post-todo" | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| }, | ||
| "permissions": { | ||
| "deny": [ | ||
| "Read(./.entire/metadata/**)" | ||
| ] | ||
| "SessionStart": [] | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -87,9 +87,17 @@ func (c *ClaudeCodeAgent) InstallHooks(localDev bool, force bool) (int, error) { | |||||||||||||||||||||||||||||||||||||||
| return 0, fmt.Errorf("failed to parse existing settings.json: %w", err) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| if hooksRaw, ok := rawSettings["hooks"]; ok { | ||||||||||||||||||||||||||||||||||||||||
| // 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 | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| if permRaw, ok := rawSettings["permissions"]; ok { | ||||||||||||||||||||||||||||||||||||||||
| if err := json.Unmarshal(permRaw, &rawPermissions); err != nil { | ||||||||||||||||||||||||||||||||||||||||
|
|
@@ -188,11 +196,41 @@ func (c *ClaudeCodeAgent) InstallHooks(localDev bool, force bool) (int, error) { | |||||||||||||||||||||||||||||||||||||||
| return 0, nil // All hooks and permissions already installed | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // Marshal hooks and update raw settings | ||||||||||||||||||||||||||||||||||||||||
| hooksJSON, err := json.Marshal(settings.Hooks) | ||||||||||||||||||||||||||||||||||||||||
| // Marshal hooks preserving unknown fields | ||||||||||||||||||||||||||||||||||||||||
| // First get the raw hooks map | ||||||||||||||||||||||||||||||||||||||||
| var rawHooks rawClaudeHooks | ||||||||||||||||||||||||||||||||||||||||
| if rawHooksRaw, ok := rawSettings["hooks_raw"]; ok { | ||||||||||||||||||||||||||||||||||||||||
| if err := json.Unmarshal(rawHooksRaw, &rawHooks); err != nil { | ||||||||||||||||||||||||||||||||||||||||
| return 0, fmt.Errorf("failed to unmarshal raw hooks: %w", err) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||
| rawHooks = make(rawClaudeHooks) | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| // Update known hook types in rawHooks with modified values | ||||||||||||||||||||||||||||||||||||||||
| 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 0, fmt.Errorf("failed to marshal %s hooks: %w", hookName, 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) | |
| } | |
| 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
AI
Feb 12, 2026
There was a problem hiding this comment.
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
AI
Feb 12, 2026
There was a problem hiding this comment.
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.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,71 @@ | ||
| #!/bin/bash | ||
| # Test script for Kilo CLI fix | ||
|
|
||
| cd ~/Dev/cli | ||
|
|
||
| echo "=== Step 1: Check current changes ===" | ||
| git diff --stat | ||
|
|
||
| echo "" | ||
| echo "=== Step 2: Build the project ===" | ||
| mise run build 2>&1 | tail -20 | ||
|
|
||
| echo "" | ||
| echo "=== Step 3: Test the fix ===" | ||
| echo "Creating test settings with PreCompact hook..." | ||
| mkdir -p .claude | ||
| cat > .claude/settings.json << 'EOF' | ||
| { | ||
| "hooks": { | ||
| "PreCompact": [ | ||
| { | ||
| "matcher": "*", | ||
| "hooks": [ | ||
| { | ||
| "type": "command", | ||
| "command": "echo 'compacting session'" | ||
| } | ||
| ] | ||
| } | ||
| ], | ||
| "SessionStart": [] | ||
| } | ||
| } | ||
| EOF | ||
|
|
||
| echo "Running: ./bin/entire enable" | ||
| ./bin/entire enable | ||
|
|
||
| echo "" | ||
| echo "=== Step 4: Verify PreCompact is preserved ===" | ||
| if grep -q "PreCompact" .claude/settings.json; then | ||
| echo "✅ SUCCESS: PreCompact hook is preserved!" | ||
| cat .claude/settings.json | grep -A 10 "PreCompact" | ||
| else | ||
| echo "❌ FAIL: PreCompact hook was deleted" | ||
| fi | ||
|
|
||
| echo "" | ||
| echo "=== Step 5: Run tests ===" | ||
| mise run test 2>&1 | tail -30 | ||
|
|
||
| echo "" | ||
| echo "=== Step 6: Create branch for PR ===" | ||
| git checkout -b fix-308-preserve-hooks | ||
| git add . | ||
| git commit -m "fix: preserve unknown Claude Code hook types in entire enable | ||
|
|
||
| The InstallHooks and UninstallHooks functions were silently dropping | ||
| any Claude Code hook types not defined in Entire's 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 #308" | ||
|
|
||
| echo "" | ||
| echo "=== Ready to push! ===" | ||
| echo "Run: git push origin fix-308-preserve-hooks" |
There was a problem hiding this comment.
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.