on main help sign in
feat(hooks): propagate CRUSH/AGENT env vars to builtin shell

Share CRUSH=1/AGENT=crush/AI_AGENT=crush between the bash tool's Shell and the hook runner so the two surfaces can't drift.

commit3b3f992f059643fa07612e5c143623e6986f338c authorChristian Rocha <christian@rocha.is> dateMon 27 Apr 2026, 09:28 (6 months ago) parent22d6f44
3 files changed+24−6
internal/hooks/hooks_test.go+7 internal/hooks/input.go+2 internal/shell/shell.go+15−6
internal/hooks/hooks_test.go+7
@@ -195,6 +195,13 @@ func TestBuildEnv(t *testing.T) {
195195 require.Equal(t, "/project", envMap["CRUSH_PROJECT_DIR"])
196196 require.Equal(t, "ls", envMap["CRUSH_TOOL_INPUT_COMMAND"])
197197 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"])
198205}
199206
200207func splitFirst(s, sep string) []string {
internal/hooks/input.go+2
@@ -7,6 +7,7 @@ import (
77 "os"
88 "strings"
99
10+ "github.com/charmbracelet/crush/internal/shell"
1011 "github.com/tidwall/gjson"
1112)
1213
@@ -51,6 +52,7 @@ func BuildPayload(eventName, sessionID, cwd, toolName, toolInputJSON string) []b
5152// It includes all current process env vars plus hook-specific ones.
5253func BuildEnv(eventName, toolName, sessionID, cwd, projectDir, toolInputJSON string) []string {
5354 env := os.Environ()
55+ env = append(env, shell.CrushEnvMarkers()...)
5456 env = append(env,
5557 fmt.Sprintf("CRUSH_EVENT=%s", eventName),
5658 fmt.Sprintf("CRUSH_TOOL_NAME=%s", toolName),
internal/shell/shell.go+15−6
@@ -34,6 +34,20 @@ const (
3434 ShellTypePowerShell
3535)
3636
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+
3751// Logger interface for optional logging
3852type Logger interface {
3953 InfoPersist(msg string, keysAndValues ...any)
@@ -81,12 +95,7 @@ func NewShell(opts *Options) *Shell {
8195 }
8296
8397 // Allow tools to detect execution by Crush.
84− env = append(
85− env,
86− "CRUSH=1",
87− "AGENT=crush",
88− "AI_AGENT=crush",
89− )
98+ env = append(env, CrushEnvMarkers()...)
9099
91100 logger := opts.Logger
92101 if logger == nil {