on main help sign in
feat(hooks): propagate CRUSH/AGENT env vars to builtin shell
3b3f992Christian Rocha
internal/hooks/hooks_test.go+7
@@ -195,6 +195,13 @@ func TestBuildEnv(t *testing.T) {
195 require.Equal(t, "/project", envMap["CRUSH_PROJECT_DIR"])
195 require.Equal(t, "/project", envMap["CRUSH_PROJECT_DIR"])
196 require.Equal(t, "ls", envMap["CRUSH_TOOL_INPUT_COMMAND"])
196 require.Equal(t, "ls", envMap["CRUSH_TOOL_INPUT_COMMAND"])
197 require.Equal(t, "/tmp/f.txt", envMap["CRUSH_TOOL_INPUT_FILE_PATH"])
197 require.Equal(t, "/tmp/f.txt", envMap["CRUSH_TOOL_INPUT_FILE_PATH"])
198+
199+ // Shared Crush markers must be present so hook-authored scripts can
200+ // detect they're running under Crush the same way bash-tool-invoked
201+ // scripts can.
202+ require.Equal(t, "1", envMap["CRUSH"])
203+ require.Equal(t, "crush", envMap["AGENT"])
204+ require.Equal(t, "crush", envMap["AI_AGENT"])
198}
205}
199
206
200func splitFirst(s, sep string) []string {
207func splitFirst(s, sep string) []string {
internal/hooks/input.go+2
@@ -7,6 +7,7 @@ import (
7 "os"
7 "os"
8 "strings"
8 "strings"
9
9
10+ "github.com/charmbracelet/crush/internal/shell"
10 "github.com/tidwall/gjson"
11 "github.com/tidwall/gjson"
11)
12)
12
13
@@ -51,6 +52,7 @@ func BuildPayload(eventName, sessionID, cwd, toolName, toolInputJSON string) []b
51// It includes all current process env vars plus hook-specific ones.
52// It includes all current process env vars plus hook-specific ones.
52func BuildEnv(eventName, toolName, sessionID, cwd, projectDir, toolInputJSON string) []string {
53func BuildEnv(eventName, toolName, sessionID, cwd, projectDir, toolInputJSON string) []string {
53 env := os.Environ()
54 env := os.Environ()
55+ env = append(env, shell.CrushEnvMarkers()...)
54 env = append(env,
56 env = append(env,
55 fmt.Sprintf("CRUSH_EVENT=%s", eventName),
57 fmt.Sprintf("CRUSH_EVENT=%s", eventName),
56 fmt.Sprintf("CRUSH_TOOL_NAME=%s", toolName),
58 fmt.Sprintf("CRUSH_TOOL_NAME=%s", toolName),
internal/shell/shell.go+15−6
@@ -34,6 +34,20 @@ const (
34 ShellTypePowerShell
34 ShellTypePowerShell
35)
35)
36
36
37+// CrushEnvMarkers returns a fresh slice of the environment variables that
38+// Crush unconditionally sets on every shell it spawns — both the interactive
39+// bash tool's [Shell] and the hook runner's [Run] calls. Tools that want to
40+// detect "am I being invoked by an AI agent?" can check any of these.
41+// Keeping them in one place guarantees the two shell surfaces cannot drift.
42+// A fresh slice is returned on every call so callers may append freely.
43+func CrushEnvMarkers() []string {
44+ return []string{
45+ "CRUSH=1",
46+ "AGENT=crush",
47+ "AI_AGENT=crush",
48+ }
49+}
50+
37// Logger interface for optional logging
51// Logger interface for optional logging
38type Logger interface {
52type Logger interface {
39 InfoPersist(msg string, keysAndValues ...any)
53 InfoPersist(msg string, keysAndValues ...any)
@@ -81,12 +95,7 @@ func NewShell(opts *Options) *Shell {
81 }
95 }
82
96
83 // Allow tools to detect execution by Crush.
97 // Allow tools to detect execution by Crush.
84− env = append(
98+ env = append(env, CrushEnvMarkers()...)
85− env,
86− "CRUSH=1",
87− "AGENT=crush",
88− "AI_AGENT=crush",
89− )
90
99
91 logger := opts.Logger
100 logger := opts.Logger
92 if logger == nil {
101 if logger == nil {