1
0
Fork 0
mirror of https://github.com/imjasonh/cnotes synced 2026-07-09 09:29:24 +00:00
cnotes/internal/config/settings.go
Jason Hall c61b579243
Complete hooks implementation with comprehensive features
- Add project-level settings support (.claude/settings.json)
- Implement regex-based dangerous command detection
- Add typed tool input parsing (BashToolInput, FileToolInput)
- Replace terminal-notifier with speech-only notifications
- Add contextual spoken messages for prompts and notifications
- Implement timeout handling for hook execution
- Add comprehensive goimports automation for Go files
- Support global, local, and project hook configurations
- Fix JSON field mapping to match Claude Code expectations
- Add extensive logging and error handling

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-21 11:27:18 -04:00

190 lines
4.1 KiB
Go

package config
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
)
type Settings struct {
Hooks map[string][]HookDefinition `json:"hooks"`
}
type HookDefinition struct {
Matcher string `json:"matcher"`
Hooks []HookAction `json:"hooks"`
}
type HookAction struct {
Type string `json:"type"`
Command string `json:"command"`
Timeout int `json:"timeout,omitempty"`
}
func LoadSettings(path string) (*Settings, error) {
data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return &Settings{}, nil
}
return nil, fmt.Errorf("failed to read settings: %w", err)
}
var settings Settings
if err := json.Unmarshal(data, &settings); err != nil {
return nil, fmt.Errorf("failed to parse settings: %w", err)
}
return &settings, nil
}
func SaveSettings(path string, settings *Settings) error {
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("failed to create settings directory: %w", err)
}
data, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal settings: %w", err)
}
if err := os.WriteFile(path, data, 0644); err != nil {
return fmt.Errorf("failed to write settings: %w", err)
}
return nil
}
func GetSettingsPath() string {
return GetGlobalSettingsPath()
}
func GetGlobalSettingsPath() string {
if path := os.Getenv("CLAUDE_SETTINGS_PATH"); path != "" {
return path
}
home, err := os.UserHomeDir()
if err != nil {
return ""
}
return filepath.Join(home, ".claude", "settings.json")
}
func GetLocalSettingsPath() string {
return filepath.Join(".", ".claude", "settings.local.json")
}
func GetProjectSettingsPath() string {
return filepath.Join(".", ".claude", "settings.json")
}
func InstallHooks(binaryPath string) error {
return InstallHooksToPath(binaryPath, GetSettingsPath())
}
func InstallHooksToPath(binaryPath, settingsPath string) error {
settings, err := LoadSettings(settingsPath)
if err != nil {
return err
}
if settings.Hooks == nil {
settings.Hooks = make(map[string][]HookDefinition)
}
hookCmd := binaryPath + " run"
hookAction := HookAction{
Type: "command",
Command: hookCmd,
}
hookDef := HookDefinition{
Matcher: ".*",
Hooks: []HookAction{hookAction},
}
// Map our event names to Claude's event names
eventMap := map[string]string{
"pre_tool_use": "PreToolUse",
"post_tool_use": "PostToolUse",
"user_prompt_submit": "UserPromptSubmit",
"stop": "Stop",
"subagent_stop": "SubagentStop",
"notification": "Notification",
"pre_compact": "PreCompact",
}
for _, claudeEvent := range eventMap {
// Check if our hook is already installed
found := false
for i, def := range settings.Hooks[claudeEvent] {
for j, action := range def.Hooks {
if action.Command == hookCmd {
// Update existing hook
settings.Hooks[claudeEvent][i].Hooks[j] = hookAction
found = true
break
}
}
if found {
break
}
}
if !found {
// Add new hook
settings.Hooks[claudeEvent] = append(settings.Hooks[claudeEvent], hookDef)
}
}
return SaveSettings(settingsPath, settings)
}
func UninstallHooks(binaryPath string) error {
return UninstallHooksFromPath(binaryPath, GetSettingsPath())
}
func UninstallHooksFromPath(binaryPath, settingsPath string) error {
settings, err := LoadSettings(settingsPath)
if err != nil {
return err
}
if settings.Hooks == nil {
return nil
}
hookCmd := binaryPath + " run"
// Remove our hook from all events
for eventName, hookDefs := range settings.Hooks {
newDefs := make([]HookDefinition, 0)
for _, def := range hookDefs {
newActions := make([]HookAction, 0)
for _, action := range def.Hooks {
if action.Command != hookCmd {
newActions = append(newActions, action)
}
}
// Only keep the definition if it still has actions
if len(newActions) > 0 {
def.Hooks = newActions
newDefs = append(newDefs, def)
}
}
if len(newDefs) > 0 {
settings.Hooks[eventName] = newDefs
} else {
delete(settings.Hooks, eventName)
}
}
return SaveSettings(settingsPath, settings)
}