@@ -43,7 +43,7 @@ rm -rf ./crush
Then, run Crush by typing `crush`.
-***
+---
</details>
@@ -108,7 +108,7 @@ Crush supports Model Context Protocol (MCP) servers through three transport type
"mcp": {
"filesystem": {
"type": "stdio",
- "command": "node",
+ "command": "node",
"args": ["/path/to/mcp-server.js"],
"env": {
"NODE_ENV": "production"
@@ -143,7 +143,7 @@ crush -d
# View last 1000 lines
crush logs
-# Follow logs in real-time
+# Follow logs in real-time
crush logs -f
# Show last 500 lines
@@ -161,6 +161,25 @@ 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", "bash:read"]
+ }
+}
+```
+
+The `allowed_tools` array accepts:
+
+- Tool names (e.g., `"view"`) - allows all actions for that tool
+- Tool:action combinations (e.g., `"bash:read"`) - 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.
@@ -60,16 +60,16 @@ func New(ctx context.Context, conn *sql.DB, cfg *config.Config) (*App, error) {
messages := message.NewService(q)
files := history.NewService(q, conn)
skipPermissionsRequests := cfg.Permissions != nil && cfg.Permissions.SkipRequests
- allowedCommands := []string{}
+ allowedTools := []string{}
if cfg.Permissions != nil && cfg.Permissions.AllowedTools != nil {
- allowedCommands = cfg.Permissions.AllowedTools
+ allowedTools = cfg.Permissions.AllowedTools
}
app := &App{
Sessions: sessions,
Messages: messages,
History: files,
- Permissions: permission.NewPermissionService(cfg.WorkingDir(), skipPermissionsRequests, allowedCommands),
+ Permissions: permission.NewPermissionService(cfg.WorkingDir(), skipPermissionsRequests, allowedTools),
LSPClients: make(map[string]*lsp.Client),
globalCtx: ctx,
@@ -50,7 +50,7 @@ type permissionService struct {
autoApproveSessions []string
autoApproveSessionsMu sync.RWMutex
skip bool
- allowedCommands []string
+ allowedTools []string
}
func (s *permissionService) GrantPersistent(permission PermissionRequest) {
@@ -85,7 +85,7 @@ func (s *permissionService) Request(opts CreatePermissionRequest) bool {
// Check if the tool/action combination is in the allowlist
commandKey := opts.ToolName + ":" + opts.Action
- if slices.Contains(s.allowedCommands, commandKey) || slices.Contains(s.allowedCommands, opts.ToolName) {
+ if slices.Contains(s.allowedTools, commandKey) || slices.Contains(s.allowedTools, opts.ToolName) {
return true
}
@@ -137,12 +137,12 @@ func (s *permissionService) AutoApproveSession(sessionID string) {
s.autoApproveSessionsMu.Unlock()
}
-func NewPermissionService(workingDir string, skip bool, allowedCommands []string) Service {
+func NewPermissionService(workingDir string, skip bool, allowedTools []string) Service {
return &permissionService{
Broker: pubsub.NewBroker[PermissionRequest](),
workingDir: workingDir,
sessionPermissions: make([]PermissionRequest, 0),
skip: skip,
- allowedCommands: allowedCommands,
+ allowedTools: allowedTools,
}
}
@@ -6,52 +6,52 @@ import (
func TestPermissionService_AllowedCommands(t *testing.T) {
tests := []struct {
- name string
- allowedCommands []string
- toolName string
- action string
- expected bool
+ name string
+ allowedTools []string
+ toolName string
+ action string
+ expected bool
}{
{
- name: "tool in allowlist",
- allowedCommands: []string{"bash", "view"},
- toolName: "bash",
- action: "execute",
- expected: true,
+ name: "tool in allowlist",
+ allowedTools: []string{"bash", "view"},
+ toolName: "bash",
+ action: "execute",
+ expected: true,
},
{
- name: "tool:action in allowlist",
- allowedCommands: []string{"bash:execute", "edit:create"},
- 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",
- allowedCommands: []string{"view", "ls"},
- toolName: "bash",
- action: "execute",
- expected: false,
+ name: "tool not in allowlist",
+ allowedTools: []string{"view", "ls"},
+ toolName: "bash",
+ action: "execute",
+ expected: false,
},
{
- name: "tool:action not in allowlist",
- allowedCommands: []string{"bash:read", "edit:create"},
- 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",
- allowedCommands: []string{},
- 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.allowedCommands)
+ 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
@@ -60,7 +60,7 @@ func TestPermissionService_AllowedCommands(t *testing.T) {
// Test the allowlist logic directly
commandKey := tt.toolName + ":" + tt.action
allowed := false
- for _, cmd := range ps.allowedCommands {
+ for _, cmd := range ps.allowedTools {
if cmd == commandKey || cmd == tt.toolName {
allowed = true
break
@@ -69,7 +69,7 @@ func TestPermissionService_AllowedCommands(t *testing.T) {
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.allowedCommands)
+ tt.expected, allowed, tt.toolName, tt.action, tt.allowedTools)
}
})
}