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)
+}
diff --git a/loop/agent.go b/loop/agent.go
index 0b8d593..9b391d0 100644
--- a/loop/agent.go
+++ b/loop/agent.go
@@ -18,6 +18,7 @@
"sketch.dev/ant"
"sketch.dev/claudetool"
+ "sketch.dev/claudetool/bashkit"
)
const (
@@ -646,11 +647,41 @@
convo.SystemPrompt = fmt.Sprintf(agentSystemPrompt, editPrompt, a.config.ClientGOOS, a.config.ClientGOARCH, a.workingDir, a.repoRoot, a.initialCommit)
+ // Define a permission callback for the bash tool to check if the branch name is set before allowing git commits
+ bashPermissionCheck := func(command string) error {
+ // Check if branch name is set
+ a.mu.Lock()
+ branchSet := a.branchName != ""
+ a.mu.Unlock()
+
+ // If branch is set, all commands are allowed
+ if branchSet {
+ return nil
+ }
+
+ // If branch is not set, check if this is a git commit command
+ willCommit, err := bashkit.WillRunGitCommit(command)
+ if err != nil {
+ // If there's an error checking, we should allow the command to proceed
+ return nil
+ }
+
+ // If it's a git commit and branch is not set, return an error
+ if willCommit {
+ return fmt.Errorf("you must use the title tool before making git commits")
+ }
+
+ return nil
+ }
+
+ // Create a custom bash tool with the permission check
+ bashTool := claudetool.NewBashTool(bashPermissionCheck)
+
// Register all tools with the conversation
// When adding, removing, or modifying tools here, double-check that the termui tool display
// template in termui/termui.go has pretty-printing support for all tools.
convo.Tools = []*ant.Tool{
- claudetool.Bash, claudetool.Keyword,
+ bashTool, claudetool.Keyword,
claudetool.Think, a.titleTool(), makeDoneTool(a.codereview, a.config.GitUsername, a.config.GitEmail),
a.codereview.Tool(),
}