Skip to content

Commit 4894b0d

Browse files
A3Ackermangastown/crew/navaniclaude
authored
fix(hooks): replace export PATH with {{GT_BIN}} in all remaining templates (#3315)
* fix(hooks): replace export PATH with {{GT_BIN}} in all remaining templates Claude, Cursor, Copilot, and Codex hook templates still used 'export PATH=...' format while Gemini was migrated to {{GT_BIN}} in PR #2742. The needsUpgrade() function detects 'export PATH=' as stale and triggers a full settings overwrite on every session start, clobbering user customizations like mcpServers and permissions. Migrate all 8 remaining template files to use {{GT_BIN}} placeholder resolved at install time. Update 4 tests to compare against resolveAndSubstitute() output instead of raw template bytes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(doctor): update settings check to accept resolved {{GT_BIN}} paths The claude-settings doctor check looked for 'PATH=' and 'gt costs record' patterns in hook commands. After migrating templates to {{GT_BIN}}, installed files contain resolved absolute paths (e.g. '/usr/local/bin/gt prime --hook') instead of 'export PATH=... && gt prime --hook'. Update patterns to match the command payload rather than the PATH prefix: 'prime --hook' and 'costs record'. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(doctor): update priming test for resolved {{GT_BIN}} paths TestPrimingCheck_FixNoPrimeHook checked for literal 'gt prime' in recreated settings.json. After {{GT_BIN}} migration, the resolved path is '/path/to/binary prime --hook'. Check for 'prime --hook' instead, consistent with claude_settings_check.go. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: gastown/crew/navani <user.email=28374790+A3Ackerman@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5c9901c commit 4894b0d

12 files changed

Lines changed: 88 additions & 67 deletions

internal/doctor/claude_settings_check.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -546,7 +546,7 @@ func (c *ClaudeSettingsCheck) checkSettings(path, _ string) []string {
546546
// Check for required elements based on template
547547
// All templates should have:
548548
// 1. enabledPlugins
549-
// 2. PATH export in hooks
549+
// 2. SessionStart hook with prime --hook
550550
// 3. Stop hook with gt costs record (for autonomous)
551551
// Check enabledPlugins
552552
if _, ok := actual["enabledPlugins"]; !ok {
@@ -559,13 +559,13 @@ func (c *ClaudeSettingsCheck) checkSettings(path, _ string) []string {
559559
return append(missing, "hooks")
560560
}
561561

562-
// Check SessionStart hook has PATH export
563-
if !c.hookHasPattern(hooks, "SessionStart", "PATH=") {
564-
missing = append(missing, "PATH export")
562+
// Check SessionStart hook has prime --hook (either via PATH export or resolved {{GT_BIN}})
563+
if !c.hookHasPattern(hooks, "SessionStart", "prime --hook") {
564+
missing = append(missing, "SessionStart hook (prime --hook)")
565565
}
566566

567-
// Check Stop hook exists with gt costs record (for all roles)
568-
if !c.hookHasPattern(hooks, "Stop", "gt costs record") {
567+
// Check Stop hook exists with costs record (for all roles)
568+
if !c.hookHasPattern(hooks, "Stop", "costs record") {
569569
missing = append(missing, "Stop hook")
570570
}
571571

internal/doctor/claude_settings_check_test.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ func createValidSettings(t *testing.T, path string) {
4848
"hooks": []any{
4949
map[string]any{
5050
"type": "command",
51-
"command": "export PATH=/usr/local/bin:$PATH",
51+
"command": "/usr/local/bin/gt prime --hook",
5252
},
5353
},
5454
},
@@ -94,7 +94,7 @@ func createStaleSettings(t *testing.T, path string, missingElements ...string) {
9494
"hooks": []any{
9595
map[string]any{
9696
"type": "command",
97-
"command": "export PATH=/usr/local/bin:$PATH",
97+
"command": "/usr/local/bin/gt prime --hook",
9898
},
9999
},
100100
},
@@ -120,16 +120,16 @@ func createStaleSettings(t *testing.T, path string, missingElements ...string) {
120120
case "hooks":
121121
delete(settings, "hooks")
122122
case "PATH":
123-
// Remove PATH from SessionStart hooks
123+
// Remove prime --hook from SessionStart hooks
124124
hooks := settings["hooks"].(map[string]any)
125125
sessionStart := hooks["SessionStart"].([]any)
126126
hookObj := sessionStart[0].(map[string]any)
127127
innerHooks := hookObj["hooks"].([]any)
128-
// Filter out PATH command
128+
// Filter out prime --hook command
129129
var filtered []any
130130
for _, h := range innerHooks {
131131
hMap := h.(map[string]any)
132-
if cmd, ok := hMap["command"].(string); ok && !strings.Contains(cmd, "PATH=") {
132+
if cmd, ok := hMap["command"].(string); ok && !strings.Contains(cmd, "prime --hook") {
133133
filtered = append(filtered, h)
134134
}
135135
}
@@ -302,10 +302,10 @@ func TestClaudeSettingsCheck_MissingHooks(t *testing.T) {
302302
}
303303
}
304304

305-
func TestClaudeSettingsCheck_MissingPATH(t *testing.T) {
305+
func TestClaudeSettingsCheck_MissingSessionStartPrime(t *testing.T) {
306306
tmpDir := t.TempDir()
307307

308-
// Create mayor settings.json missing PATH export (content validation)
308+
// Create mayor settings.json missing gt prime in SessionStart (content validation)
309309
mayorSettings := filepath.Join(tmpDir, "mayor", ".claude", "settings.json")
310310
createStaleSettings(t, mayorSettings, "PATH")
311311

@@ -315,17 +315,17 @@ func TestClaudeSettingsCheck_MissingPATH(t *testing.T) {
315315
result := check.Run(ctx)
316316

317317
if result.Status != StatusError {
318-
t.Errorf("expected StatusError for missing PATH, got %v", result.Status)
318+
t.Errorf("expected StatusError for missing prime --hook, got %v", result.Status)
319319
}
320320
found := false
321321
for _, d := range result.Details {
322-
if strings.Contains(d, "PATH export") {
322+
if strings.Contains(d, "SessionStart hook") {
323323
found = true
324324
break
325325
}
326326
}
327327
if !found {
328-
t.Errorf("expected details to mention PATH export, got %v", result.Details)
328+
t.Errorf("expected details to mention SessionStart hook, got %v", result.Details)
329329
}
330330
}
331331

internal/doctor/priming_check_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -884,7 +884,7 @@ func TestPrimingCheck_FixNoPrimeHook(t *testing.T) {
884884
t.Fatalf("settings.json should exist after fix: %v", err)
885885
}
886886

887-
if !strings.Contains(string(newData), "gt prime") {
888-
t.Errorf("recreated settings.json should contain 'gt prime', got: %s", string(newData))
887+
if !strings.Contains(string(newData), "prime --hook") {
888+
t.Errorf("recreated settings.json should contain 'prime --hook', got: %s", string(newData))
889889
}
890890
}

internal/hooks/installer_test.go

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,12 @@ func TestInstallForRole_RoleAware(t *testing.T) {
3535
t.Fatal("settings.json not created")
3636
}
3737

38-
// Verify content matches expected template
38+
// Verify content matches resolved template (with {{GT_BIN}} substituted)
3939
got, _ := os.ReadFile(path)
40-
want, _ := templateFS.ReadFile("templates/claude/" + tt.wantFile)
40+
want, err := resolveAndSubstitute("claude", tt.wantFile, tt.role)
41+
if err != nil {
42+
t.Fatalf("resolveAndSubstitute: %v", err)
43+
}
4144
if string(got) != string(want) {
4245
t.Errorf("content mismatch: got %d bytes, want %d bytes (from %s)", len(got), len(want), tt.wantFile)
4346
}
@@ -355,7 +358,10 @@ func TestInstallForRole_CursorRoleAware(t *testing.T) {
355358
}
356359

357360
got, _ := os.ReadFile(filepath.Join(dir, ".cursor", "hooks.json"))
358-
want, _ := templateFS.ReadFile("templates/cursor/hooks-autonomous.json")
361+
want, err := resolveAndSubstitute("cursor", "hooks-autonomous.json", "polecat")
362+
if err != nil {
363+
t.Fatalf("resolveAndSubstitute: %v", err)
364+
}
359365
if string(got) != string(want) {
360366
t.Error("cursor autonomous: content mismatch")
361367
}
@@ -367,7 +373,10 @@ func TestInstallForRole_CursorRoleAware(t *testing.T) {
367373
}
368374

369375
got, _ = os.ReadFile(filepath.Join(dir2, ".cursor", "hooks.json"))
370-
want, _ = templateFS.ReadFile("templates/cursor/hooks-interactive.json")
376+
want, err = resolveAndSubstitute("cursor", "hooks-interactive.json", "crew")
377+
if err != nil {
378+
t.Fatalf("resolveAndSubstitute: %v", err)
379+
}
371380
if string(got) != string(want) {
372381
t.Error("cursor interactive: content mismatch")
373382
}
@@ -400,11 +409,14 @@ func TestInstallForRole_CodexRoleAware(t *testing.T) {
400409
}
401410

402411
got, _ := os.ReadFile(filepath.Join(dir, ".codex", "hooks.json"))
403-
want, _ := templateFS.ReadFile("templates/codex/hooks-interactive.json")
412+
want, err := resolveAndSubstitute("codex", "hooks-interactive.json", "crew")
413+
if err != nil {
414+
t.Fatalf("resolveAndSubstitute: %v", err)
415+
}
404416
if string(got) != string(want) {
405417
t.Error("codex interactive: content mismatch")
406418
}
407-
if !strings.Contains(string(got), "gt costs record >/dev/null 2>&1 &") {
419+
if !strings.Contains(string(got), "costs record >/dev/null 2>&1 &") {
408420
t.Error("codex interactive: stop hook should silence gt costs record output")
409421
}
410422

@@ -415,11 +427,14 @@ func TestInstallForRole_CodexRoleAware(t *testing.T) {
415427
}
416428

417429
got, _ = os.ReadFile(filepath.Join(dir2, ".codex", "hooks.json"))
418-
want, _ = templateFS.ReadFile("templates/codex/hooks-autonomous.json")
430+
want, err = resolveAndSubstitute("codex", "hooks-autonomous.json", "polecat")
431+
if err != nil {
432+
t.Fatalf("resolveAndSubstitute: %v", err)
433+
}
419434
if string(got) != string(want) {
420435
t.Error("codex autonomous: content mismatch")
421436
}
422-
if !strings.Contains(string(got), "gt costs record >/dev/null 2>&1 &") {
437+
if !strings.Contains(string(got), "costs record >/dev/null 2>&1 &") {
423438
t.Error("codex autonomous: stop hook should silence gt costs record output")
424439
}
425440
}
@@ -433,7 +448,10 @@ func TestInstallForRole_CopilotRoleAware(t *testing.T) {
433448
}
434449

435450
got, _ := os.ReadFile(filepath.Join(dir, ".github/hooks", "gastown.json"))
436-
want, _ := templateFS.ReadFile("templates/copilot/gastown-autonomous.json")
451+
want, err := resolveAndSubstitute("copilot", "gastown-autonomous.json", "polecat")
452+
if err != nil {
453+
t.Fatalf("resolveAndSubstitute: %v", err)
454+
}
437455
if string(got) != string(want) {
438456
t.Error("copilot autonomous: content mismatch")
439457
}
@@ -445,7 +463,10 @@ func TestInstallForRole_CopilotRoleAware(t *testing.T) {
445463
}
446464

447465
got, _ = os.ReadFile(filepath.Join(dir2, ".github/hooks", "gastown.json"))
448-
want, _ = templateFS.ReadFile("templates/copilot/gastown-interactive.json")
466+
want, err = resolveAndSubstitute("copilot", "gastown-interactive.json", "crew")
467+
if err != nil {
468+
t.Fatalf("resolveAndSubstitute: %v", err)
469+
}
449470
if string(got) != string(want) {
450471
t.Error("copilot interactive: content mismatch")
451472
}

internal/hooks/templates/claude/settings-autonomous.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"hooks": [
1212
{
1313
"type": "command",
14-
"command": "export PATH=\"$HOME/go/bin:$HOME/.local/bin:$PATH\" && gt tap guard pr-workflow"
14+
"command": "{{GT_BIN}} tap guard pr-workflow"
1515
}
1616
]
1717
},
@@ -20,7 +20,7 @@
2020
"hooks": [
2121
{
2222
"type": "command",
23-
"command": "export PATH=\"$HOME/go/bin:$HOME/.local/bin:$PATH\" && gt tap guard pr-workflow"
23+
"command": "{{GT_BIN}} tap guard pr-workflow"
2424
}
2525
]
2626
},
@@ -29,7 +29,7 @@
2929
"hooks": [
3030
{
3131
"type": "command",
32-
"command": "export PATH=\"$HOME/go/bin:$HOME/.local/bin:$PATH\" && gt tap guard pr-workflow"
32+
"command": "{{GT_BIN}} tap guard pr-workflow"
3333
}
3434
]
3535
},
@@ -103,7 +103,7 @@
103103
"hooks": [
104104
{
105105
"type": "command",
106-
"command": "export PATH=\"$HOME/go/bin:$HOME/bin:$PATH\" && gt prime --hook && gt mail check --inject"
106+
"command": "{{GT_BIN}} prime --hook && {{GT_BIN}} mail check --inject"
107107
}
108108
]
109109
}
@@ -114,7 +114,7 @@
114114
"hooks": [
115115
{
116116
"type": "command",
117-
"command": "export PATH=\"$HOME/go/bin:$HOME/bin:$PATH\" && gt prime --hook"
117+
"command": "{{GT_BIN}} prime --hook"
118118
}
119119
]
120120
}
@@ -125,7 +125,7 @@
125125
"hooks": [
126126
{
127127
"type": "command",
128-
"command": "export PATH=\"$HOME/go/bin:$HOME/bin:$PATH\" && gt mail check --inject"
128+
"command": "{{GT_BIN}} mail check --inject"
129129
}
130130
]
131131
}
@@ -136,7 +136,7 @@
136136
"hooks": [
137137
{
138138
"type": "command",
139-
"command": "export PATH=\"$HOME/go/bin:$HOME/bin:$PATH\" && gt costs record &"
139+
"command": "{{GT_BIN}} costs record &"
140140
}
141141
]
142142
}

internal/hooks/templates/claude/settings-interactive.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"hooks": [
1212
{
1313
"type": "command",
14-
"command": "export PATH=\"$HOME/go/bin:$HOME/.local/bin:$PATH\" && gt tap guard pr-workflow"
14+
"command": "{{GT_BIN}} tap guard pr-workflow"
1515
}
1616
]
1717
},
@@ -20,7 +20,7 @@
2020
"hooks": [
2121
{
2222
"type": "command",
23-
"command": "export PATH=\"$HOME/go/bin:$HOME/.local/bin:$PATH\" && gt tap guard pr-workflow"
23+
"command": "{{GT_BIN}} tap guard pr-workflow"
2424
}
2525
]
2626
},
@@ -29,7 +29,7 @@
2929
"hooks": [
3030
{
3131
"type": "command",
32-
"command": "export PATH=\"$HOME/go/bin:$HOME/.local/bin:$PATH\" && gt tap guard pr-workflow"
32+
"command": "{{GT_BIN}} tap guard pr-workflow"
3333
}
3434
]
3535
}
@@ -40,7 +40,7 @@
4040
"hooks": [
4141
{
4242
"type": "command",
43-
"command": "export PATH=\"$HOME/go/bin:$HOME/bin:$PATH\" && gt prime --hook"
43+
"command": "{{GT_BIN}} prime --hook"
4444
}
4545
]
4646
}
@@ -51,7 +51,7 @@
5151
"hooks": [
5252
{
5353
"type": "command",
54-
"command": "export PATH=\"$HOME/go/bin:$HOME/bin:$PATH\" && gt prime --hook"
54+
"command": "{{GT_BIN}} prime --hook"
5555
}
5656
]
5757
}
@@ -62,7 +62,7 @@
6262
"hooks": [
6363
{
6464
"type": "command",
65-
"command": "export PATH=\"$HOME/go/bin:$HOME/bin:$PATH\" && gt mail check --inject"
65+
"command": "{{GT_BIN}} mail check --inject"
6666
}
6767
]
6868
}
@@ -73,7 +73,7 @@
7373
"hooks": [
7474
{
7575
"type": "command",
76-
"command": "export PATH=\"$HOME/go/bin:$HOME/bin:$PATH\" && gt costs record &"
76+
"command": "{{GT_BIN}} costs record &"
7777
}
7878
]
7979
}

internal/hooks/templates/codex/hooks-autonomous.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"hooks": [
77
{
88
"type": "command",
9-
"command": "export PATH=\"$HOME/go/bin:$HOME/.local/bin:$HOME/bin:$PATH\" && gt prime --hook && gt mail check --inject"
9+
"command": "{{GT_BIN}} prime --hook && {{GT_BIN}} mail check --inject"
1010
}
1111
]
1212
}
@@ -17,7 +17,7 @@
1717
"hooks": [
1818
{
1919
"type": "command",
20-
"command": "export PATH=\"$HOME/go/bin:$HOME/.local/bin:$HOME/bin:$PATH\" && gt costs record >/dev/null 2>&1 &"
20+
"command": "{{GT_BIN}} costs record >/dev/null 2>&1 &"
2121
}
2222
]
2323
}

internal/hooks/templates/codex/hooks-interactive.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"hooks": [
77
{
88
"type": "command",
9-
"command": "export PATH=\"$HOME/go/bin:$HOME/.local/bin:$HOME/bin:$PATH\" && gt prime --hook"
9+
"command": "{{GT_BIN}} prime --hook"
1010
}
1111
]
1212
}
@@ -17,7 +17,7 @@
1717
"hooks": [
1818
{
1919
"type": "command",
20-
"command": "export PATH=\"$HOME/go/bin:$HOME/.local/bin:$HOME/bin:$PATH\" && gt costs record >/dev/null 2>&1 &"
20+
"command": "{{GT_BIN}} costs record >/dev/null 2>&1 &"
2121
}
2222
]
2323
}

0 commit comments

Comments
 (0)