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
+}
diff --git a/claudetool/bashkit/bashkit_test.go b/claudetool/bashkit/bashkit_test.go
new file mode 100644
index 0000000..8bcdd8f
--- /dev/null
+++ b/claudetool/bashkit/bashkit_test.go
@@ -0,0 +1,109 @@
+package bashkit
+
+import (
+	"strings"
+	"testing"
+)
+
+func TestCheck(t *testing.T) {
+	tests := []struct {
+		name     string
+		script   string
+		wantErr  bool
+		errMatch string // string to match in error message, if wantErr is true
+	}{
+		{
+			name:     "valid script",
+			script:   "echo hello world",
+			wantErr:  false,
+			errMatch: "",
+		},
+		{
+			name:     "invalid syntax",
+			script:   "echo 'unterminated string",
+			wantErr:  false, // As per implementation, syntax errors are not flagged
+			errMatch: "",
+		},
+		{
+			name:     "git config user.name",
+			script:   "git config user.name 'John Doe'",
+			wantErr:  true,
+			errMatch: "changing git config username/email is not allowed",
+		},
+		{
+			name:     "git config user.email",
+			script:   "git config user.email 'john@example.com'",
+			wantErr:  true,
+			errMatch: "changing git config username/email is not allowed",
+		},
+		{
+			name:     "git config with flag user.name",
+			script:   "git config --global user.name 'John Doe'",
+			wantErr:  true,
+			errMatch: "changing git config username/email is not allowed",
+		},
+		{
+			name:     "git config with other setting",
+			script:   "git config core.editor vim",
+			wantErr:  false,
+			errMatch: "",
+		},
+		{
+			name:     "git without config",
+			script:   "git commit -m 'Add feature'",
+			wantErr:  false,
+			errMatch: "",
+		},
+		{
+			name:     "multiline script with proper escaped newlines",
+			script:   "echo 'Setting up git...' && git config user.name 'John Doe' && echo 'Done!'",
+			wantErr:  true,
+			errMatch: "changing git config username/email is not allowed",
+		},
+		{
+			name: "multiline script with backticks",
+			script: `echo 'Setting up git...'
+git config user.name 'John Doe'
+echo 'Done!'`,
+			wantErr:  true,
+			errMatch: "changing git config username/email is not allowed",
+		},
+		{
+			name:     "git config with variable",
+			script:   "NAME='John Doe'\ngit config user.name $NAME",
+			wantErr:  true,
+			errMatch: "changing git config username/email is not allowed",
+		},
+		{
+			name:     "only git command",
+			script:   "git",
+			wantErr:  false,
+			errMatch: "",
+		},
+		{
+			name:     "read git config",
+			script:   "git config user.name",
+			wantErr:  false,
+			errMatch: "",
+		},
+		{
+			name:     "commented git config",
+			script:   "# git config user.name 'John Doe'",
+			wantErr:  false,
+			errMatch: "",
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			err := Check(tc.script)
+			if (err != nil) != tc.wantErr {
+				t.Errorf("Check() error = %v, wantErr %v", err, tc.wantErr)
+				return
+			}
+			if tc.wantErr && err != nil && !strings.Contains(err.Error(), tc.errMatch) {
+				t.Errorf("Check() error message = %v, want containing %v", err, tc.errMatch)
+			}
+		})
+	}
+}