From 3f4d5626cd5506af9a7eb666eab92ad42aaf47da Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 21:11:29 +0000 Subject: [PATCH 1/5] feat(security): Add excluded paths configuration for enhanced security MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add exclude_paths field to ToolsConfig with default exclusions for .infer directory - Implement path exclusion validation in LocalFileService - Add CLI commands for managing excluded paths: - `infer config tools exclude list` - List excluded paths - `infer config tools exclude add ` - Add path exclusion - `infer config tools exclude remove ` - Remove path exclusion - Update tools list command to show excluded paths - Default exclusions prevent access to infer's own configuration files - Support for directory patterns with wildcards (e.g., .infer/*) - Comprehensive pattern matching for exact paths, directories, and prefixes - Update documentation with new commands and configuration options Addresses all acceptance criteria: - Infer CLI cannot modify its own config by default - Additional excluded paths list under tools configuration - Full documentation of the feature 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Eden Reich --- CLAUDE.md | 12 ++++ cmd/config.go | 116 +++++++++++++++++++++++++++++++- config/config.go | 11 ++- internal/container/container.go | 2 +- internal/services/file.go | 57 +++++++++++++++- 5 files changed, 190 insertions(+), 8 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 6d5577c8..bc886040 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -152,6 +152,9 @@ tools: - "^kubectl get pods$" safety: require_approval: true # Prompt user before executing any command + exclude_paths: # Paths excluded from tool access for security + - ".infer/" # Protect infer's own configuration directory + - ".infer/*" # Protect all files in infer's configuration directory compact: output_dir: ".infer" # Directory for compact command exports (default: project root/.infer) chat: @@ -177,6 +180,10 @@ chat: - `enable`: Enable safety approval prompts - `disable`: Disable safety approval prompts - `status`: Show current safety approval status + - `exclude`: Manage excluded paths for security + - `list`: List all excluded paths + - `add `: Add a path to the exclusion list + - `remove `: Remove a path from the exclusion list - `version`: Version information ## Dependencies @@ -245,6 +252,11 @@ infer config tools exec "git status" infer config tools safety enable infer config tools safety disable infer config tools safety status + +# Manage excluded paths for security +infer config tools exclude list +infer config tools exclude add ".github/" +infer config tools exclude remove "test.txt" ``` ## Code Style Guidelines diff --git a/cmd/config.go b/cmd/config.go index aa814b92..8084fbaa 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -130,6 +130,35 @@ var configToolsSafetyStatusCmd = &cobra.Command{ RunE: safetyStatus, } +var configToolsExcludeCmd = &cobra.Command{ + Use: "exclude", + Short: "Manage excluded paths", + Long: `Manage paths that are excluded from tool access for security purposes.`, +} + +var configToolsExcludeListCmd = &cobra.Command{ + Use: "list", + Short: "List excluded paths", + Long: `Display all paths that are excluded from tool access.`, + RunE: listExcludedPaths, +} + +var configToolsExcludeAddCmd = &cobra.Command{ + Use: "add ", + Short: "Add a path to the exclusion list", + Long: `Add a path to the exclusion list to prevent tools from accessing it.`, + Args: cobra.ExactArgs(1), + RunE: addExcludedPath, +} + +var configToolsExcludeRemoveCmd = &cobra.Command{ + Use: "remove ", + Short: "Remove a path from the exclusion list", + Long: `Remove a path from the exclusion list to allow tools to access it again.`, + Args: cobra.ExactArgs(1), + RunE: removeExcludedPath, +} + func setDefaultModel(modelName string) error { cfg, err := config.LoadConfig("") if err != nil { @@ -158,11 +187,16 @@ func init() { configToolsCmd.AddCommand(configToolsValidateCmd) configToolsCmd.AddCommand(configToolsExecCmd) configToolsCmd.AddCommand(configToolsSafetyCmd) + configToolsCmd.AddCommand(configToolsExcludeCmd) configToolsSafetyCmd.AddCommand(configToolsSafetyEnableCmd) configToolsSafetyCmd.AddCommand(configToolsSafetyDisableCmd) configToolsSafetyCmd.AddCommand(configToolsSafetyStatusCmd) + configToolsExcludeCmd.AddCommand(configToolsExcludeListCmd) + configToolsExcludeCmd.AddCommand(configToolsExcludeAddCmd) + configToolsExcludeCmd.AddCommand(configToolsExcludeRemoveCmd) + configInitCmd.Flags().Bool("overwrite", false, "Overwrite existing configuration file") configToolsListCmd.Flags().StringP("format", "f", "text", "Output format (text, json)") configToolsExecCmd.Flags().StringP("format", "f", "text", "Output format (text, json)") @@ -208,9 +242,10 @@ func listTools(cmd *cobra.Command, args []string) error { if format == "json" { data := map[string]interface{}{ - "enabled": cfg.Tools.Enabled, - "commands": cfg.Tools.Whitelist.Commands, - "patterns": cfg.Tools.Whitelist.Patterns, + "enabled": cfg.Tools.Enabled, + "commands": cfg.Tools.Whitelist.Commands, + "patterns": cfg.Tools.Whitelist.Patterns, + "exclude_paths": cfg.Tools.ExcludePaths, "safety": map[string]bool{ "require_approval": cfg.Tools.Safety.RequireApproval, }, @@ -240,6 +275,15 @@ func listTools(cmd *cobra.Command, args []string) error { fmt.Printf(" • %s\n", pattern) } + fmt.Printf("\nExcluded Paths (%d):\n", len(cfg.Tools.ExcludePaths)) + if len(cfg.Tools.ExcludePaths) == 0 { + fmt.Printf(" • None\n") + } else { + for _, path := range cfg.Tools.ExcludePaths { + fmt.Printf(" • %s\n", path) + } + } + fmt.Printf("\nSafety Settings:\n") if cfg.Tools.Safety.RequireApproval { fmt.Printf(" • Approval required: %s\n", ui.FormatSuccess("Enabled")) @@ -381,3 +425,69 @@ func getConfigPath() string { } return configPath } + +func listExcludedPaths(cmd *cobra.Command, args []string) error { + cfg, err := config.LoadConfig("") + if err != nil { + return fmt.Errorf("failed to load config: %w", err) + } + + if len(cfg.Tools.ExcludePaths) == 0 { + fmt.Println("No paths are currently excluded.") + return nil + } + + fmt.Printf("Excluded Paths (%d):\n", len(cfg.Tools.ExcludePaths)) + for _, path := range cfg.Tools.ExcludePaths { + fmt.Printf(" • %s\n", path) + } + + return nil +} + +func addExcludedPath(cmd *cobra.Command, args []string) error { + pathToAdd := args[0] + + _, err := loadAndUpdateConfig(func(c *config.Config) { + for _, existingPath := range c.Tools.ExcludePaths { + if existingPath == pathToAdd { + return + } + } + c.Tools.ExcludePaths = append(c.Tools.ExcludePaths, pathToAdd) + }) + if err != nil { + return err + } + + fmt.Printf("%s\n", ui.FormatSuccess(fmt.Sprintf("Added '%s' to excluded paths", pathToAdd))) + fmt.Printf("Tools will no longer be able to access this path\n") + return nil +} + +func removeExcludedPath(cmd *cobra.Command, args []string) error { + pathToRemove := args[0] + var found bool + + _, err := loadAndUpdateConfig(func(c *config.Config) { + for i, existingPath := range c.Tools.ExcludePaths { + if existingPath == pathToRemove { + c.Tools.ExcludePaths = append(c.Tools.ExcludePaths[:i], c.Tools.ExcludePaths[i+1:]...) + found = true + return + } + } + }) + if err != nil { + return err + } + + if !found { + fmt.Printf("%s\n", ui.FormatWarning(fmt.Sprintf("Path '%s' was not in the excluded paths list", pathToRemove))) + return nil + } + + fmt.Printf("%s\n", ui.FormatSuccess(fmt.Sprintf("Removed '%s' from excluded paths", pathToRemove))) + fmt.Printf("Tools can now access this path again\n") + return nil +} diff --git a/config/config.go b/config/config.go index 7bf5db19..f1241977 100644 --- a/config/config.go +++ b/config/config.go @@ -34,9 +34,10 @@ type OutputConfig struct { // ToolsConfig contains tool execution settings type ToolsConfig struct { - Enabled bool `yaml:"enabled"` - Whitelist ToolWhitelistConfig `yaml:"whitelist"` - Safety SafetyConfig `yaml:"safety"` + Enabled bool `yaml:"enabled"` + Whitelist ToolWhitelistConfig `yaml:"whitelist"` + Safety SafetyConfig `yaml:"safety"` + ExcludePaths []string `yaml:"exclude_paths"` } // ToolWhitelistConfig contains whitelisted commands and patterns @@ -89,6 +90,10 @@ func DefaultConfig() *Config { Safety: SafetyConfig{ RequireApproval: true, }, + ExcludePaths: []string{ + ".infer/", + ".infer/*", + }, }, Compact: CompactConfig{ OutputDir: ".infer", diff --git a/internal/container/container.go b/internal/container/container.go index 371d8c15..0aabd112 100644 --- a/internal/container/container.go +++ b/internal/container/container.go @@ -55,7 +55,7 @@ func (c *ServiceContainer) initializeDomainServices() { c.config.Gateway.APIKey, ) - c.fileService = services.NewLocalFileService() + c.fileService = services.NewLocalFileService(c.config) if c.config.Tools.Enabled { c.toolService = services.NewLLMToolService(c.config, c.fileService) diff --git a/internal/services/file.go b/internal/services/file.go index 67d0843b..0ae90e89 100644 --- a/internal/services/file.go +++ b/internal/services/file.go @@ -9,6 +9,7 @@ import ( "sort" "strings" + "github.com/inference-gateway/cli/config" "github.com/inference-gateway/cli/internal/domain" ) @@ -18,11 +19,13 @@ type LocalFileService struct { excludeExts map[string]bool maxFileSize int64 maxDepth int + config *config.Config } // NewLocalFileService creates a new local file service -func NewLocalFileService() *LocalFileService { +func NewLocalFileService(cfg *config.Config) *LocalFileService { return &LocalFileService{ + config: cfg, excludeDirs: map[string]bool{ ".git": true, ".github": true, @@ -207,6 +210,11 @@ func (s *LocalFileService) ValidateFile(path string) error { return fmt.Errorf("file path cannot be empty") } + // Check if path is explicitly excluded + if s.isPathExcluded(path) { + return fmt.Errorf("file path is excluded for security: %s", path) + } + absPath, err := filepath.Abs(path) if err != nil { return fmt.Errorf("failed to resolve file path: %w", err) @@ -237,6 +245,53 @@ func (s *LocalFileService) ValidateFile(path string) error { return nil } +// isPathExcluded checks if a file path should be excluded based on configuration +func (s *LocalFileService) isPathExcluded(path string) bool { + if s.config == nil { + return false + } + + // Clean the path for consistent matching + cleanPath := filepath.Clean(path) + + // Convert to forward slashes for consistent pattern matching + normalizedPath := filepath.ToSlash(cleanPath) + + for _, excludePattern := range s.config.Tools.ExcludePaths { + // Clean the pattern as well + cleanPattern := filepath.Clean(excludePattern) + normalizedPattern := filepath.ToSlash(cleanPattern) + + // Check for exact match + if normalizedPath == normalizedPattern { + return true + } + + // Check if pattern ends with /* for directory wildcard matching + if strings.HasSuffix(normalizedPattern, "/*") { + dirPattern := strings.TrimSuffix(normalizedPattern, "/*") + if strings.HasPrefix(normalizedPath, dirPattern+"/") || normalizedPath == dirPattern { + return true + } + } + + // Check if pattern ends with / for directory matching + if strings.HasSuffix(normalizedPattern, "/") { + dirPattern := strings.TrimSuffix(normalizedPattern, "/") + if strings.HasPrefix(normalizedPath, dirPattern+"/") || normalizedPath == dirPattern { + return true + } + } + + // Check for simple prefix matching + if strings.HasPrefix(normalizedPath, normalizedPattern) { + return true + } + } + + return false +} + func (s *LocalFileService) GetFileInfo(path string) (domain.FileInfo, error) { absPath, err := filepath.Abs(path) if err != nil { From 2176197cc62d00911e81606d394cc9fc6d3f9a51 Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Tue, 12 Aug 2025 23:21:08 +0200 Subject: [PATCH 2/5] refactor: Remove redundant comments Signed-off-by: Eden Reich --- internal/services/file.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/internal/services/file.go b/internal/services/file.go index 0ae90e89..99e264ac 100644 --- a/internal/services/file.go +++ b/internal/services/file.go @@ -210,7 +210,6 @@ func (s *LocalFileService) ValidateFile(path string) error { return fmt.Errorf("file path cannot be empty") } - // Check if path is explicitly excluded if s.isPathExcluded(path) { return fmt.Errorf("file path is excluded for security: %s", path) } @@ -251,23 +250,18 @@ func (s *LocalFileService) isPathExcluded(path string) bool { return false } - // Clean the path for consistent matching cleanPath := filepath.Clean(path) - // Convert to forward slashes for consistent pattern matching normalizedPath := filepath.ToSlash(cleanPath) for _, excludePattern := range s.config.Tools.ExcludePaths { - // Clean the pattern as well cleanPattern := filepath.Clean(excludePattern) normalizedPattern := filepath.ToSlash(cleanPattern) - // Check for exact match if normalizedPath == normalizedPattern { return true } - // Check if pattern ends with /* for directory wildcard matching if strings.HasSuffix(normalizedPattern, "/*") { dirPattern := strings.TrimSuffix(normalizedPattern, "/*") if strings.HasPrefix(normalizedPath, dirPattern+"/") || normalizedPath == dirPattern { @@ -275,7 +269,6 @@ func (s *LocalFileService) isPathExcluded(path string) bool { } } - // Check if pattern ends with / for directory matching if strings.HasSuffix(normalizedPattern, "/") { dirPattern := strings.TrimSuffix(normalizedPattern, "/") if strings.HasPrefix(normalizedPath, dirPattern+"/") || normalizedPath == dirPattern { @@ -283,7 +276,6 @@ func (s *LocalFileService) isPathExcluded(path string) bool { } } - // Check for simple prefix matching if strings.HasPrefix(normalizedPath, normalizedPattern) { return true } From 66ae051ecec8bbfe7b3fcd5ec1dea198c3629482 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Tue, 12 Aug 2025 22:14:07 +0000 Subject: [PATCH 3/5] refactor: Change exclude command to exclude-path for clarity - Update CLI command structure from `infer config tools exclude` to `infer config tools exclude-path` - Rename all related command variables and registrations - Update CLAUDE.md documentation to reflect new command structure - Maintain all existing functionality with more explicit command naming Co-authored-by: Eden Reich --- CLAUDE.md | 8 ++++---- cmd/config.go | 18 +++++++++--------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index bc886040..f5358ae4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -180,7 +180,7 @@ chat: - `enable`: Enable safety approval prompts - `disable`: Disable safety approval prompts - `status`: Show current safety approval status - - `exclude`: Manage excluded paths for security + - `exclude-path`: Manage excluded paths for security - `list`: List all excluded paths - `add `: Add a path to the exclusion list - `remove `: Remove a path from the exclusion list @@ -254,9 +254,9 @@ infer config tools safety disable infer config tools safety status # Manage excluded paths for security -infer config tools exclude list -infer config tools exclude add ".github/" -infer config tools exclude remove "test.txt" +infer config tools exclude-path list +infer config tools exclude-path add ".github/" +infer config tools exclude-path remove "test.txt" ``` ## Code Style Guidelines diff --git a/cmd/config.go b/cmd/config.go index 8084fbaa..075d3a18 100644 --- a/cmd/config.go +++ b/cmd/config.go @@ -130,20 +130,20 @@ var configToolsSafetyStatusCmd = &cobra.Command{ RunE: safetyStatus, } -var configToolsExcludeCmd = &cobra.Command{ - Use: "exclude", +var configToolsExcludePathCmd = &cobra.Command{ + Use: "exclude-path", Short: "Manage excluded paths", Long: `Manage paths that are excluded from tool access for security purposes.`, } -var configToolsExcludeListCmd = &cobra.Command{ +var configToolsExcludePathListCmd = &cobra.Command{ Use: "list", Short: "List excluded paths", Long: `Display all paths that are excluded from tool access.`, RunE: listExcludedPaths, } -var configToolsExcludeAddCmd = &cobra.Command{ +var configToolsExcludePathAddCmd = &cobra.Command{ Use: "add ", Short: "Add a path to the exclusion list", Long: `Add a path to the exclusion list to prevent tools from accessing it.`, @@ -151,7 +151,7 @@ var configToolsExcludeAddCmd = &cobra.Command{ RunE: addExcludedPath, } -var configToolsExcludeRemoveCmd = &cobra.Command{ +var configToolsExcludePathRemoveCmd = &cobra.Command{ Use: "remove ", Short: "Remove a path from the exclusion list", Long: `Remove a path from the exclusion list to allow tools to access it again.`, @@ -187,15 +187,15 @@ func init() { configToolsCmd.AddCommand(configToolsValidateCmd) configToolsCmd.AddCommand(configToolsExecCmd) configToolsCmd.AddCommand(configToolsSafetyCmd) - configToolsCmd.AddCommand(configToolsExcludeCmd) + configToolsCmd.AddCommand(configToolsExcludePathCmd) configToolsSafetyCmd.AddCommand(configToolsSafetyEnableCmd) configToolsSafetyCmd.AddCommand(configToolsSafetyDisableCmd) configToolsSafetyCmd.AddCommand(configToolsSafetyStatusCmd) - configToolsExcludeCmd.AddCommand(configToolsExcludeListCmd) - configToolsExcludeCmd.AddCommand(configToolsExcludeAddCmd) - configToolsExcludeCmd.AddCommand(configToolsExcludeRemoveCmd) + configToolsExcludePathCmd.AddCommand(configToolsExcludePathListCmd) + configToolsExcludePathCmd.AddCommand(configToolsExcludePathAddCmd) + configToolsExcludePathCmd.AddCommand(configToolsExcludePathRemoveCmd) configInitCmd.Flags().Bool("overwrite", false, "Overwrite existing configuration file") configToolsListCmd.Flags().StringP("format", "f", "text", "Output format (text, json)") From 03bbcf26f94a4f29f036e6b76ba9d1fac9da4a4e Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Wed, 13 Aug 2025 00:21:46 +0200 Subject: [PATCH 4/5] feat(config): Add exclude_paths to tools for improved path management Signed-off-by: Eden Reich --- .infer/config.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.infer/config.yaml b/.infer/config.yaml index e57651b8..78e47ebb 100644 --- a/.infer/config.yaml +++ b/.infer/config.yaml @@ -27,7 +27,10 @@ tools: - ^kubectl get pods$ safety: require_approval: true + exclude_paths: + - .infer/ + - .infer/* compact: output_dir: .infer chat: - default_model: deepseek/deepseek-chat + default_model: "" From 2ec7bfeab0eba89911f8dbe28b73289830344719 Mon Sep 17 00:00:00 2001 From: Eden Reich Date: Wed, 13 Aug 2025 00:48:34 +0200 Subject: [PATCH 5/5] refactor: Remove unused commands from tool whitelist and simplify error messages The cat, head and tail are optional, removing those from default whitelisting and the Read command does exactly what those commands are doing, easier to maintain security and useability. Let the user decide if they need cat command. Signed-off-by: Eden Reich --- .infer/config.yaml | 3 --- config/config.go | 2 +- internal/services/file.go | 2 +- internal/services/tool.go | 5 ++--- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/.infer/config.yaml b/.infer/config.yaml index 78e47ebb..7e91c474 100644 --- a/.infer/config.yaml +++ b/.infer/config.yaml @@ -12,9 +12,6 @@ tools: - ls - pwd - echo - - cat - - head - - tail - grep - find - wc diff --git a/config/config.go b/config/config.go index f1241977..accf28f3 100644 --- a/config/config.go +++ b/config/config.go @@ -77,7 +77,7 @@ func DefaultConfig() *Config { Enabled: true, Whitelist: ToolWhitelistConfig{ Commands: []string{ - "ls", "pwd", "echo", "cat", "head", "tail", + "ls", "pwd", "echo", "grep", "find", "wc", "sort", "uniq", }, Patterns: []string{ diff --git a/internal/services/file.go b/internal/services/file.go index 99e264ac..c223095d 100644 --- a/internal/services/file.go +++ b/internal/services/file.go @@ -211,7 +211,7 @@ func (s *LocalFileService) ValidateFile(path string) error { } if s.isPathExcluded(path) { - return fmt.Errorf("file path is excluded for security: %s", path) + return fmt.Errorf("file is excluded: %s", path) } absPath, err := filepath.Abs(path) diff --git a/internal/services/tool.go b/internal/services/tool.go index 29ae753f..4bf651ff 100644 --- a/internal/services/tool.go +++ b/internal/services/tool.go @@ -204,8 +204,7 @@ func (s *LLMToolService) executeRead(filePath string, startLine, endLine int) (* } if err != nil { - result.Error = err.Error() - return result, nil + return nil, err } result.Content = content @@ -308,7 +307,7 @@ func (s *LLMToolService) executeReadTool(args map[string]interface{}) (string, e result, err := s.executeRead(filePath, startLine, endLine) if err != nil { - return "", fmt.Errorf("file read failed: %w", err) + return "", err } if format == "json" {