Add permission callback to Bash tool to enforce title setting before git commits
- Convert Bash tool from a plain variable to a struct with a permission callback function
- Add a constructor for creating Bash tools with custom callback functions
- Implement a permission check in agent.go that verifies a branch name has been set before allowing git commits
- If no branch name is set and a git commit command is attempted, return an error instructing to use the title tool first
- Maintain backward compatibility for tests with BashRun function
Co-Authored-By: sketch <hello@sketch.dev>
diff --git a/claudetool/bash.go b/claudetool/bash.go
index 2edd230..b3b8b03 100644
--- a/claudetool/bash.go
+++ b/claudetool/bash.go
@@ -17,14 +17,32 @@
"sketch.dev/claudetool/bashkit"
)
-// The Bash tool executes shell commands with bash -c and optional timeout
-var Bash = &ant.Tool{
- Name: bashName,
- Description: strings.TrimSpace(bashDescription),
- InputSchema: ant.MustSchema(bashInputSchema),
- Run: BashRun,
+// PermissionCallback is a function type for checking if a command is allowed to run
+type PermissionCallback func(command string) error
+
+// BashTool is a struct for executing shell commands with bash -c and optional timeout
+type BashTool struct {
+ // CheckPermission is called before running any command, if set
+ CheckPermission PermissionCallback
}
+// NewBashTool creates a new Bash tool with optional permission callback
+func NewBashTool(checkPermission PermissionCallback) *ant.Tool {
+ tool := &BashTool{
+ CheckPermission: checkPermission,
+ }
+
+ return &ant.Tool{
+ Name: bashName,
+ Description: strings.TrimSpace(bashDescription),
+ InputSchema: ant.MustSchema(bashInputSchema),
+ Run: tool.Run,
+ }
+}
+
+// The Bash tool executes shell commands with bash -c and optional timeout
+var Bash = NewBashTool(nil)
+
const (
bashName = "bash"
bashDescription = `
@@ -96,17 +114,25 @@
}
}
-func BashRun(ctx context.Context, m json.RawMessage) (string, error) {
+func (b *BashTool) Run(ctx context.Context, m json.RawMessage) (string, error) {
var req bashInput
if err := json.Unmarshal(m, &req); err != nil {
return "", fmt.Errorf("failed to unmarshal bash command input: %w", err)
}
+
// do a quick permissions check (NOT a security barrier)
err := bashkit.Check(req.Command)
if err != nil {
return "", err
}
+ // Custom permission callback if set
+ if b.CheckPermission != nil {
+ if err := b.CheckPermission(req.Command); err != nil {
+ return "", err
+ }
+ }
+
// If Background is set to true, use executeBackgroundBash
if req.Background {
result, err := executeBackgroundBash(ctx, req)
@@ -278,3 +304,9 @@
StderrFile: stderrFile,
}, nil
}
+
+// BashRun is the legacy function for testing compatibility
+func BashRun(ctx context.Context, m json.RawMessage) (string, error) {
+ // Use the default Bash tool which has no permission callback
+ return Bash.Run(ctx, m)
+}