Initial commit
diff --git a/claudetool/bashkit/bashkit.go b/claudetool/bashkit/bashkit.go
new file mode 100644
index 0000000..a56eef0
--- /dev/null
+++ b/claudetool/bashkit/bashkit.go
@@ -0,0 +1,97 @@
+package bashkit
+
+import (
+ "fmt"
+ "strings"
+
+ "mvdan.cc/sh/v3/syntax"
+)
+
+var checks = []func(*syntax.CallExpr) error{
+ noGitConfigUsernameEmailChanges,
+}
+
+// Check inspects bashScript and returns an error if it ought not be executed.
+// Check DOES NOT PROVIDE SECURITY against malicious actors.
+// It is intended to catch straightforward mistakes in which a model
+// does things despite having been instructed not to do them.
+func Check(bashScript string) error {
+ r := strings.NewReader(bashScript)
+ parser := syntax.NewParser()
+ file, err := parser.Parse(r, "")
+ if err != nil {
+ // Execution will fail, but we'll get a better error message from bash.
+ // Note that if this were security load bearing, this would be a terrible idea:
+ // You could smuggle stuff past Check by exploiting differences in what is considered syntactically valid.
+ // But it is not.
+ return nil
+ }
+
+ syntax.Walk(file, func(node syntax.Node) bool {
+ if err != nil {
+ return false
+ }
+ callExpr, ok := node.(*syntax.CallExpr)
+ if !ok {
+ return true
+ }
+ for _, check := range checks {
+ err = check(callExpr)
+ if err != nil {
+ return false
+ }
+ }
+ return true
+ })
+
+ return err
+}
+
+// noGitConfigUsernameEmailChanges checks for git config username/email changes.
+// It uses simple heuristics, and has both false positives and false negatives.
+func noGitConfigUsernameEmailChanges(cmd *syntax.CallExpr) error {
+ if hasGitConfigUsernameEmailChanges(cmd) {
+ return fmt.Errorf("permission denied: changing git config username/email is not allowed, use env vars instead")
+ }
+ return nil
+}
+
+func hasGitConfigUsernameEmailChanges(cmd *syntax.CallExpr) bool {
+ if len(cmd.Args) < 3 {
+ return false
+ }
+ if cmd.Args[0].Lit() != "git" {
+ return false
+ }
+
+ configIndex := -1
+ for i, arg := range cmd.Args {
+ if arg.Lit() == "config" {
+ configIndex = i
+ break
+ }
+ }
+
+ if configIndex < 0 || configIndex == len(cmd.Args)-1 {
+ return false
+ }
+
+ // check for user.name or user.email
+ keyIndex := -1
+ for i, arg := range cmd.Args {
+ if i < configIndex {
+ continue
+ }
+ if arg.Lit() == "user.name" || arg.Lit() == "user.email" {
+ keyIndex = i
+ break
+ }
+ }
+
+ if keyIndex < 0 || keyIndex == len(cmd.Args)-1 {
+ return false
+ }
+
+ // user.name/user.email is followed by a value
+ return true
+}