diff --git a/src/negative_test/claude-code-settings/additional-properties-hook.json b/src/negative_test/claude-code-settings/additional-properties-hook.json new file mode 100644 index 00000000000..c38f3f836d1 --- /dev/null +++ b/src/negative_test/claude-code-settings/additional-properties-hook.json @@ -0,0 +1,17 @@ +{ + "hooks": { + "PreToolUse": [ + { + "extraField": "should not be allowed", + "hooks": [ + { + "command": "echo 'test'", + "type": "command", + "unknownProperty": "not allowed" + } + ], + "matcher": "Write" + } + ] + } +} diff --git a/src/negative_test/claude-code-settings/invalid-enum-values.json b/src/negative_test/claude-code-settings/invalid-enum-values.json new file mode 100644 index 00000000000..250d2b2b679 --- /dev/null +++ b/src/negative_test/claude-code-settings/invalid-enum-values.json @@ -0,0 +1,6 @@ +{ + "forceLoginMethod": "github", + "permissions": { + "defaultMode": "invalid-mode" + } +} diff --git a/src/negative_test/claude-code-settings/invalid-env-variable-name.json b/src/negative_test/claude-code-settings/invalid-env-variable-name.json new file mode 100644 index 00000000000..3f2c7183803 --- /dev/null +++ b/src/negative_test/claude-code-settings/invalid-env-variable-name.json @@ -0,0 +1,9 @@ +{ + "env": { + "HAS SPACES": "should fail", + "has-dashes": "should fail", + "lowercase": "should fail", + "valid_VAR": "ok", + "123STARTS_WITH_NUMBER": "should fail" + } +} diff --git a/src/negative_test/claude-code-settings/invalid-hook-type.json b/src/negative_test/claude-code-settings/invalid-hook-type.json new file mode 100644 index 00000000000..ccc08d2afa1 --- /dev/null +++ b/src/negative_test/claude-code-settings/invalid-hook-type.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "PreToolUse": [ + { + "hooks": [ + { + "command": "echo 'Invalid type - should be command'", + "type": "script" + } + ], + "matcher": "Write" + } + ] + } +} diff --git a/src/negative_test/claude-code-settings/invalid-permission-rule.json b/src/negative_test/claude-code-settings/invalid-permission-rule.json new file mode 100644 index 00000000000..676bc5a8fca --- /dev/null +++ b/src/negative_test/claude-code-settings/invalid-permission-rule.json @@ -0,0 +1,10 @@ +{ + "permissions": { + "allow": [ + "InvalidTool", + "Bash without parentheses", + "Read[wrong-brackets]", + "WebFetch(invalid:syntax" + ] + } +} diff --git a/src/negative_test/claude-code-settings/invalid-timeout-value.json b/src/negative_test/claude-code-settings/invalid-timeout-value.json new file mode 100644 index 00000000000..a0ed3c0fe26 --- /dev/null +++ b/src/negative_test/claude-code-settings/invalid-timeout-value.json @@ -0,0 +1,15 @@ +{ + "hooks": { + "PreToolUse": [ + { + "hooks": [ + { + "command": "echo 'test'", + "timeout": 0, + "type": "command" + } + ] + } + ] + } +} diff --git a/src/negative_test/claude-code-settings/missing-required-hook-fields.json b/src/negative_test/claude-code-settings/missing-required-hook-fields.json new file mode 100644 index 00000000000..b3adc9d2644 --- /dev/null +++ b/src/negative_test/claude-code-settings/missing-required-hook-fields.json @@ -0,0 +1,13 @@ +{ + "hooks": { + "PostToolUse": [ + { + "hooks": [ + { + "type": "command" + } + ] + } + ] + } +} diff --git a/src/negative_test/claude-code-settings/wrong-property-types.json b/src/negative_test/claude-code-settings/wrong-property-types.json new file mode 100644 index 00000000000..5f90e1bac85 --- /dev/null +++ b/src/negative_test/claude-code-settings/wrong-property-types.json @@ -0,0 +1,10 @@ +{ + "cleanupPeriodDays": "thirty", + "enableAllProjectMcpServers": 1, + "includeCoAuthoredBy": "yes", + "learnMode": "true", + "permissions": { + "additionalDirectories": "should be array", + "allow": "should be array" + } +} diff --git a/src/schemas/json/claude-code-settings.json b/src/schemas/json/claude-code-settings.json index d2b48200ef4..b1415b69e13 100644 --- a/src/schemas/json/claude-code-settings.json +++ b/src/schemas/json/claude-code-settings.json @@ -2,7 +2,48 @@ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://json.schemastore.org/claude-code-settings.json", "$defs": { - "rule": { + "hookCommand": { + "type": "object", + "description": "Hook command configuration", + "additionalProperties": false, + "required": ["type", "command"], + "properties": { + "type": { + "type": "string", + "description": "Type of hook implementation", + "const": "command" + }, + "command": { + "type": "string", + "description": "Shell command to execute" + }, + "timeout": { + "type": "number", + "description": "Optional timeout in seconds for this specific command", + "exclusiveMinimum": 0 + } + } + }, + "hookMatcher": { + "type": "object", + "description": "Hook matcher configuration with multiple hooks", + "additionalProperties": false, + "required": ["hooks"], + "properties": { + "matcher": { + "type": "string", + "description": "String (e.g. Write) to match values related to the hook event, e.g. tool names" + }, + "hooks": { + "type": "array", + "description": "Array of hooks to execute", + "items": { + "$ref": "#/$defs/hookCommand" + } + } + } + }, + "permissionRule": { "type": "string", "description": "Tool permission rule (e.g., 'Bash(ls:*)', 'Read(~/.zshrc)', 'WebFetch(domain:github.com)')", "pattern": "^(Agent|Bash|Edit|Glob|Grep|LS|MultiEdit|NotebookEdit|NotebookRead|Read|TodoRead|TodoWrite|WebFetch|WebSearch|Write)\\(?|^mcp__" @@ -21,14 +62,8 @@ "type": "string", "description": "Custom script path to generate an auth value" }, - "autoUpdaterStatus": { - "type": "string", - "description": "Enable or disable the auto-updater", - "enum": ["enabled", "disabled"], - "default": "enabled" - }, "cleanupPeriodDays": { - "type": "number", + "type": "integer", "description": "How long to locally retain chat transcripts (in days)", "default": 30, "minimum": 0 @@ -52,8 +87,7 @@ }, "model": { "type": "string", - "description": "Claude model to use", - "enum": ["opus", "sonnet"] + "description": "Deprecated: use env.ANTHROPIC_MODEL and env.ANTHROPIC_SMALL_FAST_MODEL instead. Either \"sonnet\", \"opus\", or a specific Claude model ID (see https://docs.anthropic.com/en/docs/about-claude/models/overview)" }, "permissions": { "type": "object", @@ -62,42 +96,104 @@ "properties": { "allow": { "type": "array", - "description": "List of allowed tool rules", - "items": { "$ref": "#/$defs/rule" }, + "description": "List of allowed tool permission rules", + "items": { "$ref": "#/$defs/permissionRule" }, "uniqueItems": true }, "deny": { "type": "array", - "description": "List of denied tool rules", - "items": { "$ref": "#/$defs/rule" }, + "description": "List of denied tool permission rules", + "items": { "$ref": "#/$defs/permissionRule" }, + "uniqueItems": true + }, + "defaultMode": { + "type": "string", + "description": "Default permission mode for tool execution", + "enum": ["acceptEdits", "bypassPermissions", "default", "plan"] + }, + "disableBypassPermissionsMode": { + "type": "string", + "description": "Disable bypass permissions mode", + "const": "disable" + }, + "additionalDirectories": { + "type": "array", + "description": "Paths to additional directories Claude can access beyond the working directory", + "items": { + "type": "string" + }, "uniqueItems": true } - }, - "default": { - "allow": [], - "deny": [] } }, - "preferredNotifChannel": { - "type": "string", - "description": "Preferred notification channel", - "enum": [ - "iterm2", - "iterm2_with_bell", - "terminal_bell", - "notifications_disabled" - ], - "default": "iterm2" + "enableAllProjectMcpServers": { + "type": "boolean", + "description": "Whether to automatically approve all MCP servers in the project" }, - "theme": { - "type": "string", - "description": "UI theme configuration", - "enum": ["dark", "light", "light-daltonized", "dark-daltonized"] + "enabledMcpjsonServers": { + "type": "array", + "description": "List of allowed MCP servers from .mcp.json", + "items": { + "type": "string" + } + }, + "disabledMcpjsonServers": { + "type": "array", + "description": "List of denied MCP servers from .mcp.json", + "items": { + "type": "string" + } + }, + "hooks": { + "type": "object", + "description": "Hooks configuration for executing commands at specific points in Claude Code's lifecycle", + "additionalProperties": false, + "properties": { + "PreToolUse": { + "type": "array", + "description": "Hooks that run before tool calls", + "items": { + "$ref": "#/$defs/hookMatcher" + } + }, + "PostToolUse": { + "type": "array", + "description": "Hooks that run after tool completion", + "items": { + "$ref": "#/$defs/hookMatcher" + } + }, + "Notification": { + "type": "array", + "description": "Hooks that trigger on notifications", + "items": { + "$ref": "#/$defs/hookMatcher" + } + }, + "Stop": { + "type": "array", + "description": "Hooks that run when agents finish responding", + "items": { + "$ref": "#/$defs/hookMatcher" + } + }, + "SubagentStop": { + "type": "array", + "description": "Hooks that run when subagents finish responding", + "items": { + "$ref": "#/$defs/hookMatcher" + } + } + } }, - "verbose": { + "learnMode": { "type": "boolean", - "description": "Show full bash and command outputs", - "default": false + "description": "Request the model focuses more on educating and co-creating with the user, rather than just completing tasks" + }, + "forceLoginMethod": { + "type": "string", + "description": "Force a specific login method, and skip the login method selection screen", + "enum": ["claudeai", "console"] } } } diff --git a/src/test/claude-code-settings/hooks-complete.json b/src/test/claude-code-settings/hooks-complete.json new file mode 100644 index 00000000000..c81092942a9 --- /dev/null +++ b/src/test/claude-code-settings/hooks-complete.json @@ -0,0 +1,68 @@ +{ + "hooks": { + "Notification": [ + { + "hooks": [ + { + "command": "osascript -e 'display notification \"Claude task complete\" with title \"Claude Code\"'", + "type": "command" + } + ] + } + ], + "PostToolUse": [ + { + "hooks": [ + { + "command": "git diff", + "type": "command" + } + ], + "matcher": "Edit" + } + ], + "PreToolUse": [ + { + "hooks": [ + { + "command": "echo 'About to write file' >> /tmp/claude-log.txt", + "type": "command" + } + ], + "matcher": "Write" + }, + { + "hooks": [ + { + "command": "echo 'Running bash command' >> /tmp/claude-log.txt", + "timeout": 5, + "type": "command" + } + ], + "matcher": "Bash" + } + ], + "Stop": [ + { + "hooks": [ + { + "command": "echo 'Claude finished' | tee -a /tmp/claude-session.log", + "type": "command" + } + ] + } + ], + "SubagentStop": [ + { + "hooks": [ + { + "command": "echo 'Subagent completed' >> /tmp/claude-subagent.log", + "timeout": 2, + "type": "command" + } + ], + "matcher": "Agent" + } + ] + } +} diff --git a/src/test/claude-code-settings/mcp-servers.json b/src/test/claude-code-settings/mcp-servers.json new file mode 100644 index 00000000000..cabd490d41b --- /dev/null +++ b/src/test/claude-code-settings/mcp-servers.json @@ -0,0 +1,5 @@ +{ + "disabledMcpjsonServers": ["experimental-server", "legacy-api"], + "enableAllProjectMcpServers": false, + "enabledMcpjsonServers": ["filesystem", "github", "database-reader"] +} diff --git a/src/test/claude-code-settings/modern-complete-config.json b/src/test/claude-code-settings/modern-complete-config.json new file mode 100644 index 00000000000..841578041d0 --- /dev/null +++ b/src/test/claude-code-settings/modern-complete-config.json @@ -0,0 +1,67 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-settings.json", + "apiKeyHelper": "/usr/local/bin/claude-auth-helper", + "cleanupPeriodDays": 60, + "disabledMcpjsonServers": ["untrusted-server"], + "enableAllProjectMcpServers": true, + "env": { + "ANTHROPIC_MODEL": "claude-3-5-sonnet-20241022", + "ANTHROPIC_SMALL_FAST_MODEL": "claude-3-5-haiku-20241022", + "CLAUDE_LOG_LEVEL": "debug", + "PROJECT_ROOT": "/home/user/projects" + }, + "forceLoginMethod": "claudeai", + "hooks": { + "PreToolUse": [ + { + "hooks": [ + { + "command": "echo 'File write detected' >> ~/.claude-code/activity.log", + "type": "command" + } + ], + "matcher": "Write" + } + ], + "Stop": [ + { + "hooks": [ + { + "command": "osascript -e 'display notification \"Task completed\" with title \"Claude Code\"'", + "timeout": 3, + "type": "command" + } + ] + } + ] + }, + "includeCoAuthoredBy": true, + "learnMode": true, + "permissions": { + "additionalDirectories": ["~/Documents/reference", "~/.config/claude-code"], + "allow": [ + "Agent", + "Bash(git:*)", + "Bash(npm:*)", + "Edit", + "Glob", + "Grep", + "LS", + "MultiEdit", + "NotebookEdit", + "Read", + "TodoWrite", + "WebFetch(domain:*.anthropic.com)", + "WebSearch", + "Write", + "mcp__*" + ], + "defaultMode": "plan", + "deny": [ + "Bash(sudo:*)", + "Bash(rm -rf:*)", + "Write(/etc/**)", + "Write(/System/**)" + ] + } +} diff --git a/src/test/claude-code-settings/permissions-advanced.json b/src/test/claude-code-settings/permissions-advanced.json new file mode 100644 index 00000000000..72637fa9586 --- /dev/null +++ b/src/test/claude-code-settings/permissions-advanced.json @@ -0,0 +1,26 @@ +{ + "permissions": { + "additionalDirectories": [ + "~/Documents/shared-projects", + "/opt/company/resources" + ], + "allow": [ + "Agent", + "Glob", + "Grep", + "LS", + "Read(~/projects/**)", + "Edit(~/projects/**)", + "MultiEdit", + "NotebookEdit", + "TodoWrite", + "WebFetch(domain:github.com)", + "WebSearch", + "mcp__ide__getDiagnostics", + "mcp__ide__executeCode" + ], + "defaultMode": "acceptEdits", + "deny": ["Bash(rm:*)", "Write(/etc/**)", "WebFetch(domain:malicious.com)"], + "disableBypassPermissionsMode": "disable" + } +}