Detailed changes
@@ -161,6 +161,31 @@ Add to your `crush.json` config file:
}
```
+### Configurable Default Permissions
+
+Crush includes a permission system to control which tools can be executed without prompting. You can configure allowed tools in your `crush.json` config file:
+
+```json
+{
+ "permissions": {
+ "allowed_tools": [
+ "view",
+ "ls",
+ "grep",
+ "edit:write",
+ "mcp_context7_get-library-doc"
+ ]
+ }
+}
+```
+
+The `allowed_tools` array accepts:
+
+- Tool names (e.g., `"view"`) - allows all actions for that tool
+- Tool:action combinations (e.g., `"edit:write"`) - allows only specific actions
+
+You can also skip all permission prompts entirely by running Crush with the `--yolo` flag.
+
### OpenAI-Compatible APIs
Crush supports all OpenAI-compatible APIs. Here's an example configuration for Deepseek, which uses an OpenAI-compatible API. Don't forget to set `DEEPSEEK_API_KEY` in your environment.
@@ -59,13 +59,17 @@ func New(ctx context.Context, conn *sql.DB, cfg *config.Config) (*App, error) {
sessions := session.NewService(q)
messages := message.NewService(q)
files := history.NewService(q, conn)
- skipPermissionsRequests := cfg.Options != nil && cfg.Options.SkipPermissionsRequests
+ skipPermissionsRequests := cfg.Permissions != nil && cfg.Permissions.SkipRequests
+ allowedTools := []string{}
+ if cfg.Permissions != nil && cfg.Permissions.AllowedTools != nil {
+ allowedTools = cfg.Permissions.AllowedTools
+ }
app := &App{
Sessions: sessions,
Messages: messages,
History: files,
- Permissions: permission.NewPermissionService(cfg.WorkingDir(), skipPermissionsRequests),
+ Permissions: permission.NewPermissionService(cfg.WorkingDir(), skipPermissionsRequests, allowedTools),
LSPClients: make(map[string]*lsp.Client),
globalCtx: ctx,
@@ -73,7 +73,10 @@ to assist developers in writing, debugging, and understanding code directly from
if err != nil {
return err
}
- cfg.Options.SkipPermissionsRequests = yolo
+ if cfg.Permissions == nil {
+ cfg.Permissions = &config.Permissions{}
+ }
+ cfg.Permissions.SkipRequests = yolo
ctx := cmd.Context()
@@ -120,14 +120,18 @@ type TUIOptions struct {
// Here we can add themes later or any TUI related options
}
+type Permissions struct {
+ AllowedTools []string `json:"allowed_tools,omitempty"` // Tools that don't require permission prompts
+ SkipRequests bool `json:"-"` // Automatically accept all permissions (YOLO mode)
+}
+
type Options struct {
- ContextPaths []string `json:"context_paths,omitempty"`
- TUI *TUIOptions `json:"tui,omitempty"`
- Debug bool `json:"debug,omitempty"`
- DebugLSP bool `json:"debug_lsp,omitempty"`
- DisableAutoSummarize bool `json:"disable_auto_summarize,omitempty"`
- DataDirectory string `json:"data_directory,omitempty"` // Relative to the cwd
- SkipPermissionsRequests bool `json:"-"` // Automatically accept all permissions (YOLO mode)
+ ContextPaths []string `json:"context_paths,omitempty"`
+ TUI *TUIOptions `json:"tui,omitempty"`
+ Debug bool `json:"debug,omitempty"`
+ DebugLSP bool `json:"debug_lsp,omitempty"`
+ DisableAutoSummarize bool `json:"disable_auto_summarize,omitempty"`
+ DataDirectory string `json:"data_directory,omitempty"` // Relative to the cwd
}
type MCPs map[string]MCPConfig
@@ -244,6 +248,8 @@ type Config struct {
Options *Options `json:"options,omitempty"`
+ Permissions *Permissions `json:"permissions,omitempty"`
+
// Internal
workingDir string `json:"-"`
// TODO: most likely remove this concept when I come back to it
@@ -50,6 +50,7 @@ type permissionService struct {
autoApproveSessions []string
autoApproveSessionsMu sync.RWMutex
skip bool
+ allowedTools []string
}
func (s *permissionService) GrantPersistent(permission PermissionRequest) {
@@ -82,6 +83,12 @@ func (s *permissionService) Request(opts CreatePermissionRequest) bool {
return true
}
+ // Check if the tool/action combination is in the allowlist
+ commandKey := opts.ToolName + ":" + opts.Action
+ if slices.Contains(s.allowedTools, commandKey) || slices.Contains(s.allowedTools, opts.ToolName) {
+ return true
+ }
+
s.autoApproveSessionsMu.RLock()
autoApprove := slices.Contains(s.autoApproveSessions, opts.SessionID)
s.autoApproveSessionsMu.RUnlock()
@@ -130,11 +137,12 @@ func (s *permissionService) AutoApproveSession(sessionID string) {
s.autoApproveSessionsMu.Unlock()
}
-func NewPermissionService(workingDir string, skip bool) Service {
+func NewPermissionService(workingDir string, skip bool, allowedTools []string) Service {
return &permissionService{
Broker: pubsub.NewBroker[PermissionRequest](),
workingDir: workingDir,
sessionPermissions: make([]PermissionRequest, 0),
skip: skip,
+ allowedTools: allowedTools,
}
}
@@ -0,0 +1,92 @@
+package permission
+
+import (
+ "testing"
+)
+
+func TestPermissionService_AllowedCommands(t *testing.T) {
+ tests := []struct {
+ name string
+ allowedTools []string
+ toolName string
+ action string
+ expected bool
+ }{
+ {
+ name: "tool in allowlist",
+ allowedTools: []string{"bash", "view"},
+ toolName: "bash",
+ action: "execute",
+ expected: true,
+ },
+ {
+ name: "tool:action in allowlist",
+ allowedTools: []string{"bash:execute", "edit:create"},
+ toolName: "bash",
+ action: "execute",
+ expected: true,
+ },
+ {
+ name: "tool not in allowlist",
+ allowedTools: []string{"view", "ls"},
+ toolName: "bash",
+ action: "execute",
+ expected: false,
+ },
+ {
+ name: "tool:action not in allowlist",
+ allowedTools: []string{"bash:read", "edit:create"},
+ toolName: "bash",
+ action: "execute",
+ expected: false,
+ },
+ {
+ name: "empty allowlist",
+ allowedTools: []string{},
+ toolName: "bash",
+ action: "execute",
+ expected: false,
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ service := NewPermissionService("/tmp", false, tt.allowedTools)
+
+ // Create a channel to capture the permission request
+ // Since we're testing the allowlist logic, we need to simulate the request
+ ps := service.(*permissionService)
+
+ // Test the allowlist logic directly
+ commandKey := tt.toolName + ":" + tt.action
+ allowed := false
+ for _, cmd := range ps.allowedTools {
+ if cmd == commandKey || cmd == tt.toolName {
+ allowed = true
+ break
+ }
+ }
+
+ if allowed != tt.expected {
+ t.Errorf("expected %v, got %v for tool %s action %s with allowlist %v",
+ tt.expected, allowed, tt.toolName, tt.action, tt.allowedTools)
+ }
+ })
+ }
+}
+
+func TestPermissionService_SkipMode(t *testing.T) {
+ service := NewPermissionService("/tmp", true, []string{})
+
+ result := service.Request(CreatePermissionRequest{
+ SessionID: "test-session",
+ ToolName: "bash",
+ Action: "execute",
+ Description: "test command",
+ Path: "/tmp",
+ })
+
+ if !result {
+ t.Error("expected permission to be granted in skip mode")
+ }
+}