@@ -10,7 +10,6 @@ import (
11 "github.com/charmbracelet/crush/internal/permission"
11 "github.com/charmbracelet/crush/internal/permission"
12 "github.com/charmbracelet/crush/internal/shell"
12 "github.com/charmbracelet/crush/internal/shell"
13− "mvdan.cc/sh/v3/syntax"
16type BashParams struct {
15type BashParams struct {
@@ -41,235 +40,10 @@ const (
41 BashNoOutput = "no output"
40 BashNoOutput = "no output"
44−func containsBannedCommand(node syntax.Node) bool {
49− switch n := node.(type) {
50− case *syntax.CallExpr:
52− cmdName := getWordValue(n.Args[0])
53− for _, banned := range bannedCommands {
54− if strings.EqualFold(cmdName, banned) {
59− for _, arg := range n.Args {
60− if containsBannedCommand(arg) {
65− if checkWordForBannedCommands(n) {
68− for _, part := range n.Parts {
69− if containsBannedCommand(part) {
73− case *syntax.CmdSubst:
74− for _, stmt := range n.Stmts {
75− if containsBannedCommand(stmt) {
79− case *syntax.Subshell:
80− for _, stmt := range n.Stmts {
81− if containsBannedCommand(stmt) {
86− if containsBannedCommand(n.Cmd) {
89− for _, redir := range n.Redirs {
90− if containsBannedCommand(redir) {
94− case *syntax.BinaryCmd:
95− return containsBannedCommand(n.X) || containsBannedCommand(n.Y)
97− for _, stmt := range n.Stmts {
98− if containsBannedCommand(stmt) {
102− case *syntax.IfClause:
103− for _, stmt := range n.Cond {
104− if containsBannedCommand(stmt) {
108− for _, stmt := range n.Then {
109− if containsBannedCommand(stmt) {
113− if n.Else != nil && containsBannedCommand(n.Else) {
116− case *syntax.WhileClause:
117− for _, stmt := range n.Cond {
118− if containsBannedCommand(stmt) {
122− for _, stmt := range n.Do {
123− if containsBannedCommand(stmt) {
127− case *syntax.ForClause:
128− for _, stmt := range n.Do {
129− if containsBannedCommand(stmt) {
133− if containsBannedCommand(n.Loop) {
136− case *syntax.CaseClause:
137− for _, item := range n.Items {
138− for _, stmt := range item.Stmts {
139− if containsBannedCommand(stmt) {
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)
154−func checkWordForBannedCommands(word *syntax.Word) bool {
159− for _, part := range word.Parts {
160− switch p := part.(type) {
161− case *syntax.SglQuoted:
162− if checkQuotedStringForBannedCommands(p.Value) {
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)
172− if checkQuotedStringForBannedCommands(content.String()) {
180−func checkQuotedStringForBannedCommands(content string) bool {
181− parser := syntax.NewParser()
182− file, err := parser.Parse(strings.NewReader(content), "")
187− if len(file.Stmts) == 0 {
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 {
197− for i, arg := range callExpr.Args {
201− argStr := getWordValue(arg)
202− if strings.HasPrefix(argStr, "-") {
214− for _, stmt := range file.Stmts {
215− if containsBannedCommand(stmt) {
222−func getWordValue(word *syntax.Word) string {
223− if word == nil || len(word.Parts) == 0 {
227− var result strings.Builder
228− for _, part := range word.Parts {
229− switch p := part.(type) {
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)
242− return result.String()
245−func validateCommand(command string) error {
246− parser := syntax.NewParser()
247− file, err := parser.Parse(strings.NewReader(command), "")
249− parts := strings.Fields(command)
252− for _, banned := range bannedCommands {
253− if strings.EqualFold(baseCmd, banned) {
254− return fmt.Errorf("command '%s' is not allowed", baseCmd)
261− for _, stmt := range file.Stmts {
262− if containsBannedCommand(stmt) {
263− return fmt.Errorf("command contains banned operations")
269var bannedCommands = []string{
43var bannedCommands = []string{
270 "alias", "curl", "curlie", "wget", "axel", "aria2c",
44 "alias", "curl", "curlie", "wget", "axel", "aria2c",
271 "nc", "telnet", "lynx", "w3m", "links", "httpie", "xh",
45 "nc", "telnet", "lynx", "w3m", "links", "httpie", "xh",
272− "http-prompt", "chrome", "firefox", "safari", "sudo",
46+ "http-prompt", "chrome", "firefox", "safari",
275// getSafeReadOnlyCommands returns platform-appropriate safe commands
49// getSafeReadOnlyCommands returns platform-appropriate safe commands
@@ -515,9 +289,11 @@ func (b *bashTool) Run(ctx context.Context, call ToolCall) (ToolResponse, error)
515 return NewTextErrorResponse("missing command"), nil
289 return NewTextErrorResponse("missing command"), nil
518−
292+ baseCmd := strings.Fields(params.Command)[0]
519− if err := validateCommand(params.Command); err != nil {
293+ for _, banned := range bannedCommands {
294+ if strings.EqualFold(baseCmd, banned) {
520− return NewTextErrorResponse(err.Error()), nil
295+ return NewTextErrorResponse(fmt.Sprintf("command '%s' is not allowed", baseCmd)), nil
523 isSafeReadOnly := false
299 isSafeReadOnly := false