diff --git a/internal/hooks/hooks_test.go b/internal/hooks/hooks_test.go index 2eee4dc8e145ef84ab8b9a620d033019e0202b14..79236aaf2479557a59a93267c0b3695001cf9971 100644 --- a/internal/hooks/hooks_test.go +++ b/internal/hooks/hooks_test.go @@ -195,6 +195,13 @@ func TestBuildEnv(t *testing.T) { require.Equal(t, "/project", envMap["CRUSH_PROJECT_DIR"]) require.Equal(t, "ls", envMap["CRUSH_TOOL_INPUT_COMMAND"]) require.Equal(t, "/tmp/f.txt", envMap["CRUSH_TOOL_INPUT_FILE_PATH"]) + + // Shared Crush markers must be present so hook-authored scripts can + // detect they're running under Crush the same way bash-tool-invoked + // scripts can. + require.Equal(t, "1", envMap["CRUSH"]) + require.Equal(t, "crush", envMap["AGENT"]) + require.Equal(t, "crush", envMap["AI_AGENT"]) } func splitFirst(s, sep string) []string { diff --git a/internal/hooks/input.go b/internal/hooks/input.go index af77d5d149bcf85cd3747a831d5e841ad9050fe1..dfe1a52a42abb26eb81b8db1ffcbbf13c17544f8 100644 --- a/internal/hooks/input.go +++ b/internal/hooks/input.go @@ -7,6 +7,7 @@ import ( "os" "strings" + "github.com/charmbracelet/crush/internal/shell" "github.com/tidwall/gjson" ) @@ -51,6 +52,7 @@ func BuildPayload(eventName, sessionID, cwd, toolName, toolInputJSON string) []b // It includes all current process env vars plus hook-specific ones. func BuildEnv(eventName, toolName, sessionID, cwd, projectDir, toolInputJSON string) []string { env := os.Environ() + env = append(env, shell.CrushEnvMarkers()...) env = append(env, fmt.Sprintf("CRUSH_EVENT=%s", eventName), fmt.Sprintf("CRUSH_TOOL_NAME=%s", toolName), diff --git a/internal/shell/shell.go b/internal/shell/shell.go index 6cb9d9f34301cca82ac35075010c199990300eb8..a9c1c83c117fe9b58a55e3440e985ad3046e478a 100644 --- a/internal/shell/shell.go +++ b/internal/shell/shell.go @@ -34,6 +34,20 @@ const ( ShellTypePowerShell ) +// CrushEnvMarkers returns a fresh slice of the environment variables that +// Crush unconditionally sets on every shell it spawns — both the interactive +// bash tool's [Shell] and the hook runner's [Run] calls. Tools that want to +// detect "am I being invoked by an AI agent?" can check any of these. +// Keeping them in one place guarantees the two shell surfaces cannot drift. +// A fresh slice is returned on every call so callers may append freely. +func CrushEnvMarkers() []string { + return []string{ + "CRUSH=1", + "AGENT=crush", + "AI_AGENT=crush", + } +} + // Logger interface for optional logging type Logger interface { InfoPersist(msg string, keysAndValues ...any) @@ -81,12 +95,7 @@ func NewShell(opts *Options) *Shell { } // Allow tools to detect execution by Crush. - env = append( - env, - "CRUSH=1", - "AGENT=crush", - "AI_AGENT=crush", - ) + env = append(env, CrushEnvMarkers()...) logger := opts.Logger if logger == nil {