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
80 changes: 4 additions & 76 deletions .claude/settings.json
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": []
}
}
77 changes: 73 additions & 4 deletions cmd/entire/cli/agent/claudecode/hooks.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment on lines +90 to +100
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.
}
if permRaw, ok := rawSettings["permissions"]; ok {
if err := json.Unmarshal(permRaw, &rawPermissions); err != nil {
Expand Down Expand Up @@ -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
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.
rawHooks[hookName] = hooksJSON
}

// Marshal the complete rawHooks back
hooksJSON, err := json.Marshal(rawHooks)
if err != nil {
return 0, fmt.Errorf("failed to marshal hooks: %w", err)
}
delete(rawSettings, "hooks_raw") // Remove temporary storage
rawSettings["hooks"] = hooksJSON

// Marshal permissions and update raw settings
Expand Down Expand Up @@ -238,11 +276,23 @@ func (c *ClaudeCodeAgent) UninstallHooks() error {
}

var settings ClaudeSettings
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)
}
}

// Initialize rawHooks if it wasn't populated (no hooks key or empty)
if rawHooks == nil {
rawHooks = make(rawClaudeHooks)
}

// Remove Entire hooks from all hook types
settings.Hooks.SessionStart = removeEntireHooks(settings.Hooks.SessionStart)
Expand Down Expand Up @@ -295,8 +345,27 @@ func (c *ClaudeCodeAgent) UninstallHooks() error {
}
}

// Marshal hooks back
hooksJSON, err := json.Marshal(settings.Hooks)
// Marshal hooks back preserving unknown fields
// 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 fmt.Errorf("failed to marshal %s hooks: %w", hookName, err)
}
rawHooks[hookName] = hooksJSON
}
Comment on lines +359 to +365
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 +350 to +365
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.

// Marshal the complete rawHooks back
hooksJSON, err := json.Marshal(rawHooks)
if err != nil {
return fmt.Errorf("failed to marshal hooks: %w", err)
}
Expand Down
4 changes: 4 additions & 0 deletions cmd/entire/cli/agent/claudecode/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ type ClaudeSettings struct {
Hooks ClaudeHooks `json:"hooks"`
}

// rawClaudeHooks preserves unknown hook types using RawMessage
// This prevents silently dropping hooks not defined in ClaudeHooks struct
type rawClaudeHooks map[string]json.RawMessage

// ClaudeHooks contains the hook configurations
type ClaudeHooks struct {
SessionStart []ClaudeHookMatcher `json:"SessionStart,omitempty"`
Expand Down
71 changes: 71 additions & 0 deletions test-fix.sh
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"