mirror of
https://github.com/imjasonh/cnotes
synced 2026-07-09 09:29:24 +00:00
- 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>
190 lines
4.1 KiB
Go
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)
|
|
}
|