on main help sign in
feat(config): define disabled tools option which filters out agent tools access (#1016)

* test(config): ensure all tools are included if no disabled defined * test(config): verify explicitly disabled tools are gone * refactor(config): share manual slice iteration and selection logic * refactor(config): move static slice of readonly tools into helper * test(config): ensure allowed tools for task is at most in the read only

commitad50cf423769b7738d53d1cf057f6999f6f64bfb authorAdam Stringer <adamstringer@hey.com> dateFri 12 Sep 2025, 10:36 (1 year ago) parentfb5e058
3 files changed+91−12
internal/config/config.go+47−8 internal/config/load_test.go+39 internal/lsp/protocol/tsjson.go+5−4
internal/config/config.go+47−8
@@ -143,6 +143,7 @@ type Options struct {
143143 DebugLSP bool `json:"debug_lsp,omitempty" jsonschema:"description=Enable debug logging for LSP servers,default=false"`
144144 DisableAutoSummarize bool `json:"disable_auto_summarize,omitempty" jsonschema:"description=Disable automatic conversation summarization,default=false"`
145145 DataDirectory string `json:"data_directory,omitempty" jsonschema:"description=Directory for storing application data (relative to working directory),default=.crush,example=.crush"` // Relative to the cwd
146+ DisabledTools []string `json:"disabled_tools" jsonschema:"description=Tools to disable"`
146147}
147148
148149type MCPs map[string]MCPConfig
@@ -415,7 +416,51 @@ func (c *Config) SetProviderAPIKey(providerID, apiKey string) error {
415416 return nil
416417}
417418
419+func allToolNames() []string {
420+ return []string{
421+ "bash",
422+ "download",
423+ "edit",
424+ "multiedit",
425+ "fetch",
426+ "glob",
427+ "grep",
428+ "ls",
429+ "sourcegraph",
430+ "view",
431+ "write",
432+ }
433+}
434+
435+func resolveAllowedTools(allTools []string, disabledTools []string) []string {
436+ if disabledTools == nil {
437+ return allTools
438+ }
439+ // filter out disabled tools (exclude mode)
440+ return filterSlice(allTools, disabledTools, false)
441+}
442+
443+func resolveReadOnlyTools(tools []string) []string {
444+ readOnlyTools := []string{"glob", "grep", "ls", "sourcegraph", "view"}
445+ // filter to only include tools that are in allowedtools (include mode)
446+ return filterSlice(tools, readOnlyTools, true)
447+}
448+
449+func filterSlice(data []string, mask []string, include bool) []string {
450+ filtered := []string{}
451+ for _, s := range data {
452+ // if include is true, we include items that ARE in the mask
453+ // if include is false, we include items that are NOT in the mask
454+ if include == slices.Contains(mask, s) {
455+ filtered = append(filtered, s)
456+ }
457+ }
458+ return filtered
459+}
460+
418461func (c *Config) SetupAgents() {
462+ allowedTools := resolveAllowedTools(allToolNames(), c.Options.DisabledTools)
463+
419464 agents := map[string]Agent{
420465 "coder": {
421466 ID: "coder",
@@ -423,7 +468,7 @@ func (c *Config) SetupAgents() {
423468 Description: "An agent that helps with executing coding tasks.",
424469 Model: SelectedModelTypeLarge,
425470 ContextPaths: c.Options.ContextPaths,
426− // All tools allowed
471+ AllowedTools: allowedTools,
427472 },
428473 "task": {
429474 ID: "task",
@@ -431,13 +476,7 @@ func (c *Config) SetupAgents() {
431476 Description: "An agent that helps with searching for context and finding implementation details.",
432477 Model: SelectedModelTypeLarge,
433478 ContextPaths: c.Options.ContextPaths,
434− AllowedTools: []string{
435− "glob",
436− "grep",
437− "ls",
438− "sourcegraph",
439− "view",
440− },
479+ AllowedTools: resolveReadOnlyTools(allowedTools),
441480 // NO MCPs or LSPs by default
442481 AllowedMCP: map[string][]string{},
443482 AllowedLSP: []string{},
internal/config/load_test.go+39
@@ -11,6 +11,7 @@ import (
1111 "github.com/charmbracelet/catwalk/pkg/catwalk"
1212 "github.com/charmbracelet/crush/internal/csync"
1313 "github.com/charmbracelet/crush/internal/env"
14+ "github.com/stretchr/testify/assert"
1415 "github.com/stretchr/testify/require"
1516)
1617
@@ -453,6 +454,44 @@ func TestConfig_IsConfigured(t *testing.T) {
453454 })
454455}
455456
457+func TestConfig_setupAgentsWithNoDisabledTools(t *testing.T) {
458+ cfg := &Config{
459+ Options: &Options{
460+ DisabledTools: []string{},
461+ },
462+ }
463+
464+ cfg.SetupAgents()
465+ coderAgent, ok := cfg.Agents["coder"]
466+ require.True(t, ok)
467+ assert.Equal(t, allToolNames(), coderAgent.AllowedTools)
468+
469+ taskAgent, ok := cfg.Agents["task"]
470+ require.True(t, ok)
471+ assert.Equal(t, []string{"glob", "grep", "ls", "sourcegraph", "view"}, taskAgent.AllowedTools)
472+}
473+
474+func TestConfig_setupAgentsWithDisabledTools(t *testing.T) {
475+ cfg := &Config{
476+ Options: &Options{
477+ DisabledTools: []string{
478+ "edit",
479+ "download",
480+ "grep",
481+ },
482+ },
483+ }
484+
485+ cfg.SetupAgents()
486+ coderAgent, ok := cfg.Agents["coder"]
487+ require.True(t, ok)
488+ assert.Equal(t, []string{"bash", "multiedit", "fetch", "glob", "ls", "sourcegraph", "view", "write"}, coderAgent.AllowedTools)
489+
490+ taskAgent, ok := cfg.Agents["task"]
491+ require.True(t, ok)
492+ assert.Equal(t, []string{"glob", "ls", "sourcegraph", "view"}, taskAgent.AllowedTools)
493+}
494+
456495func TestConfig_configureProvidersWithDisabledProvider(t *testing.T) {
457496 knownProviders := []catwalk.Provider{
458497 {
internal/lsp/protocol/tsjson.go+5−4
@@ -10,10 +10,11 @@ package protocol
1010// https://github.com/microsoft/vscode-languageserver-node/blob/release/protocol/3.17.6-next.9/protocol/metaModel.json
1111// LSP metaData.version = 3.17.0.
1212
13−import "bytes"
14−import "encoding/json"
15−
16−import "fmt"
13+import (
14+ "bytes"
15+ "encoding/json"
16+ "fmt"
17+)
1718
1819// UnmarshalError indicates that a JSON value did not conform to
1920// one of the expected cases of an LSP union type.