feat(safety): make safe shell commands user-configurable

- Replace hardcoded safeCommands map with sync.OnceValue loading from ~/.config/grokkit/safe_shell_commands.yaml
- Provide fallback built-in safe list if config load fails
- Add safe_shell_commands.yaml.example for user reference
- Update safety check to use loaded map and prefix matching
This commit is contained in:
Greg Gauthier 2026-03-07 18:22:39 +00:00
parent 7e4bdbc21c
commit f9d99527e0
2 changed files with 65 additions and 35 deletions

View File

@ -4,44 +4,48 @@ import (
"bytes" "bytes"
"fmt" "fmt"
"os" "os"
"path/filepath"
"regexp" "regexp"
"strings" "strings"
"sync"
"text/template" "text/template"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
// Global safe read-only whitelist // Global safe read-only whitelist
var safeCommands = map[string]bool{ // LoadSafeCommands reads the user's safe shell commands config (with fallback)
//GNU Utilities var safeCommands = sync.OnceValue(func() map[string]bool {
"ls": true, cfgPath := filepath.Join(os.Getenv("HOME"), ".config", "grokkit", "safe_shell_commands.yaml")
"pwd": true,
"cat": true, data, err := os.ReadFile(cfgPath)
"tree": true, if err != nil {
"find": true, // Fallback to a built-in safe list
"grep": true, return map[string]bool{
"which": true, "ls": true, "pwd": true, "cat": true, "tree": true,
//Git and Gitea "find": true, "grep": true, "rg": true,
"git status": true, "git status": true, "git log": true, "git diff": true, "git branch": true,
"git log": true, "go test": true, "go vet": true, "go fmt": true, "go mod tidy": true,
"git diff": true,
"git branch": true,
"tea repos list -o csv -lm 100": true,
"tea repos search -o csv": true,
//Miscellaneous Utilities
"cndump -s": true,
// Safe test runners
"go test": true,
"make test": true, "make test": true,
"make lint": true, "pytest": true, "poetry run pytest": true, "ctest": true,
"pytest": true, "python -m pytest": true, "python": true, "poetry": true,
"poetry run pytest": true,
"ctest": true,
"python -m pytest": true,
"go vet": true,
"go fmt": true,
"go mod tidy": true,
} }
}
var cfg struct {
SafeCommands []string `yaml:"safe_commands"`
}
err = yaml.Unmarshal(data, &cfg)
if err != nil {
return nil
}
m := make(map[string]bool)
for _, c := range cfg.SafeCommands {
m[strings.ToLower(strings.TrimSpace(c))] = true
}
return m
})
var ( var (
// stepRe still finds the headings (this one is solid) // stepRe still finds the headings (this one is solid)
@ -64,13 +68,12 @@ func Load(path string, userParams map[string]any) (*Recipe, error) {
return nil, fmt.Errorf("yaml parse: %w", err) return nil, fmt.Errorf("yaml parse: %w", err)
} }
// === SAFETY CHECK: reject truly dangerous commands === // === SAFETY CHECK using user-configurable whitelist ===
safeMap := safeCommands()
for _, cmd := range r.AllowedShellCommands { for _, cmd := range r.AllowedShellCommands {
trimmed := strings.TrimSpace(strings.ToLower(cmd)) trimmed := strings.ToLower(strings.TrimSpace(cmd))
// Allow exact matches or common prefixed commands
allowed := false allowed := false
for safe := range safeCommands { for safe := range safeMap {
if strings.HasPrefix(trimmed, safe) { if strings.HasPrefix(trimmed, safe) {
allowed = true allowed = true
break break

View File

@ -0,0 +1,27 @@
# Grokkit safe shell commands whitelist
# Only commands listed here (or prefixed by these) are allowed in recipes.
# This is a safety boundary — never add rm, mv, cd, sudo, etc.
# This file should be placed in ~/.config/grokkit/safe_shell_commands.yaml
# customize it as you see fit.
safe_commands:
- ls
- pwd
- cat
- tree
- find
- grep
- rg # ripgrep
- git status
- git log
- git diff
- git branch
- go test
- go vet
- go fmt
- go mod tidy
- make test
- pytest
- poetry run pytest
- ctest
- python -m pytest