@@ -10,7 +10,6 @@ import (
1010
1111 "github.com/charmbracelet/crush/internal/permission"
1212 "github.com/charmbracelet/crush/internal/shell"
13− "mvdan.cc/sh/v3/syntax"
1413)
1514
1615type BashParams struct {
@@ -41,235 +40,10 @@ const (
4140 BashNoOutput = "no output"
4241)
4342
44−func containsBannedCommand(node syntax.Node) bool {
45− if node == nil {
46− return false
47− }
48−
49− switch n := node.(type) {
50− case *syntax.CallExpr:
51− if len(n.Args) > 0 {
52− cmdName := getWordValue(n.Args[0])
53− for _, banned := range bannedCommands {
54− if strings.EqualFold(cmdName, banned) {
55− return true
56− }
57− }
58− }
59− for _, arg := range n.Args {
60− if containsBannedCommand(arg) {
61− return true
62− }
63− }
64− case *syntax.Word:
65− if checkWordForBannedCommands(n) {
66− return true
67− }
68− for _, part := range n.Parts {
69− if containsBannedCommand(part) {
70− return true
71− }
72− }
73− case *syntax.CmdSubst:
74− for _, stmt := range n.Stmts {
75− if containsBannedCommand(stmt) {
76− return true
77− }
78− }
79− case *syntax.Subshell:
80− for _, stmt := range n.Stmts {
81− if containsBannedCommand(stmt) {
82− return true
83− }
84− }
85− case *syntax.Stmt:
86− if containsBannedCommand(n.Cmd) {
87− return true
88− }
89− for _, redir := range n.Redirs {
90− if containsBannedCommand(redir) {
91− return true
92− }
93− }
94− case *syntax.BinaryCmd:
95− return containsBannedCommand(n.X) || containsBannedCommand(n.Y)
96− case *syntax.Block:
97− for _, stmt := range n.Stmts {
98− if containsBannedCommand(stmt) {
99− return true
100− }
101− }
102− case *syntax.IfClause:
103− for _, stmt := range n.Cond {
104− if containsBannedCommand(stmt) {
105− return true
106− }
107− }
108− for _, stmt := range n.Then {
109− if containsBannedCommand(stmt) {
110− return true
111− }
112− }
113− if n.Else != nil && containsBannedCommand(n.Else) {
114− return true
115− }
116− case *syntax.WhileClause:
117− for _, stmt := range n.Cond {
118− if containsBannedCommand(stmt) {
119− return true
120− }
121− }
122− for _, stmt := range n.Do {
123− if containsBannedCommand(stmt) {
124− return true
125− }
126− }
127− case *syntax.ForClause:
128− for _, stmt := range n.Do {
129− if containsBannedCommand(stmt) {
130− return true
131− }
132− }
133− if containsBannedCommand(n.Loop) {
134− return true
135− }
136− case *syntax.CaseClause:
137− for _, item := range n.Items {
138− for _, stmt := range item.Stmts {
139− if containsBannedCommand(stmt) {
140− return true
141− }
142− }
143− }
144− case *syntax.FuncDecl:
145− return containsBannedCommand(n.Body)
146− case *syntax.ArithmExp:
147− return containsBannedCommand(n.X)
148− case *syntax.Redirect:
149− return containsBannedCommand(n.Word)
150− }
151− return false
152−}
153−
154−func checkWordForBannedCommands(word *syntax.Word) bool {
155− if word == nil {
156− return false
157− }
158−
159− for _, part := range word.Parts {
160− switch p := part.(type) {
161− case *syntax.SglQuoted:
162− if checkQuotedStringForBannedCommands(p.Value) {
163− return true
164− }
165− case *syntax.DblQuoted:
166− var content strings.Builder
167− for _, qpart := range p.Parts {
168− if lit, ok := qpart.(*syntax.Lit); ok {
169− content.WriteString(lit.Value)
170− }
171− }
172− if checkQuotedStringForBannedCommands(content.String()) {
173− return true
174− }
175− }
176− }
177− return false
178−}
179−
180−func checkQuotedStringForBannedCommands(content string) bool {
181− parser := syntax.NewParser()
182− file, err := parser.Parse(strings.NewReader(content), "")
183− if err != nil {
184− return false
185− }
186−
187− if len(file.Stmts) == 0 {
188− return false
189− }
190−
191− // Simple heuristic: if it looks like prose rather than commands, don't flag it
192− if len(file.Stmts) == 1 {
193− stmt := file.Stmts[0]
194− if callExpr, ok := stmt.Cmd.(*syntax.CallExpr); ok {
195− if len(callExpr.Args) > 2 {
196− allText := true
197− for i, arg := range callExpr.Args {
198− if i == 0 {
199− continue
200− }
201− argStr := getWordValue(arg)
202− if strings.HasPrefix(argStr, "-") {
203− allText = false
204− break
205− }
206− }
207− if allText {
208− return false
209− }
210− }
211− }
212− }
213−
214− for _, stmt := range file.Stmts {
215− if containsBannedCommand(stmt) {
216− return true
217− }
218− }
219− return false
220−}
221−
222−func getWordValue(word *syntax.Word) string {
223− if word == nil || len(word.Parts) == 0 {
224− return ""
225− }
226−
227− var result strings.Builder
228− for _, part := range word.Parts {
229− switch p := part.(type) {
230− case *syntax.Lit:
231− result.WriteString(p.Value)
232− case *syntax.SglQuoted:
233− result.WriteString(p.Value)
234− case *syntax.DblQuoted:
235− for _, qpart := range p.Parts {
236− if lit, ok := qpart.(*syntax.Lit); ok {
237− result.WriteString(lit.Value)
238− }
239− }
240− }
241− }
242− return result.String()
243−}
244−
245−func validateCommand(command string) error {
246− parser := syntax.NewParser()
247− file, err := parser.Parse(strings.NewReader(command), "")
248− if err != nil {
249− parts := strings.Fields(command)
250− if len(parts) > 0 {
251− baseCmd := parts[0]
252− for _, banned := range bannedCommands {
253− if strings.EqualFold(baseCmd, banned) {
254− return fmt.Errorf("command '%s' is not allowed", baseCmd)
255− }
256− }
257− }
258− return nil
259− }
260−
261− for _, stmt := range file.Stmts {
262− if containsBannedCommand(stmt) {
263− return fmt.Errorf("command contains banned operations")
264− }
265− }
266− return nil
267−}
268−
26943var bannedCommands = []string{
27044 "alias", "curl", "curlie", "wget", "axel", "aria2c",
27145 "nc", "telnet", "lynx", "w3m", "links", "httpie", "xh",
272− "http-prompt", "chrome", "firefox", "safari", "sudo",
46+ "http-prompt", "chrome", "firefox", "safari",
27347}
27448
27549// getSafeReadOnlyCommands returns platform-appropriate safe commands
@@ -515,9 +289,11 @@ func (b *bashTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
515289 return NewTextErrorResponse("missing command"), nil
516290 }
517291
518−
519− if err := validateCommand(params.Command); err != nil {
520− return NewTextErrorResponse(err.Error()), nil
292+ baseCmd := strings.Fields(params.Command)[0]
293+ for _, banned := range bannedCommands {
294+ if strings.EqualFold(baseCmd, banned) {
295+ return NewTextErrorResponse(fmt.Sprintf("command '%s' is not allowed", baseCmd)), nil
296+ }
521297 }
522298
523299 isSafeReadOnly := false