@tanagram/cli 0.4.11 → 0.4.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # Tanagram
1
+ # Tanagram CLI
2
2
 
3
3
  A lightweight Go CLI that enforces policies from `AGENTS.md` files on your local git changes.
4
4
 
@@ -23,8 +23,10 @@ webui/src/Button.tsx:42 - [No hardcoded colors] Don't use hard-coded color value
23
23
  # 1. Install globally via npm
24
24
  npm install -g @tanagram/cli
25
25
 
26
- # 2. Setup Claude Code hook (automatic)
26
+ # 2. Automatically add a Claude Code hook
27
27
  tanagram config claude
28
+ # and/or if you use Cursor:
29
+ tanagram config cursor
28
30
 
29
31
  # 3. Run tanagram (will prompt for API key on first run)
30
32
  tanagram
@@ -45,28 +47,6 @@ Tanagram uses Claude AI (via Anthropic API) to extract policies from your instru
45
47
  2. Create an API key in the dashboard
46
48
  3. Run `tanagram` and enter your key when prompted
47
49
 
48
- ### Local Development
49
-
50
- ```bash
51
- cd cli
52
- npm install # Builds the Go binary
53
- ./bin/tanagram
54
- ```
55
-
56
- ### Install Locally for Testing
57
-
58
- Install globally from the local directory to test as if it were published:
59
-
60
- ```bash
61
- cd /Users/molinar/tanagram/cli
62
- npm install -g .
63
- ```
64
-
65
- Then run from anywhere:
66
- ```bash
67
- tanagram
68
- ```
69
-
70
50
  ## Usage
71
51
 
72
52
  ```bash
@@ -113,35 +93,26 @@ tanagram config list
113
93
  **Manual setup (alternative):**
114
94
  If you prefer to manually edit your settings, add this to your `~/.claude/settings.json` (user settings) or `.claude/settings.json` (project settings):
115
95
 
116
- ```json
117
- "hooks": {
118
- "PostToolUse": [
119
- {
120
- "matcher": "Edit|Write",
121
- "hooks": [
122
- {
123
- "type": "command",
124
- "command": "tanagram"
125
- }
126
- ]
127
- }
128
- ]
129
- }
130
- ```
131
-
132
- For example, your full `settings.json` file might look like this:
133
-
134
96
  ```json
135
97
  {
136
- "alwaysThinkingEnabled": true,
137
98
  "hooks": {
138
- "PostToolUse": [
99
+ "SessionStart": [
139
100
  {
140
- "matcher": "Edit|Write",
141
101
  "hooks": [
142
102
  {
143
- "type": "command",
144
- "command": "tanagram"
103
+ "command": "tanagram snapshot",
104
+ "type": "command"
105
+ }
106
+ ],
107
+ "matcher": "startup|clear"
108
+ }
109
+ ],
110
+ "Stop": [
111
+ {
112
+ "hooks": [
113
+ {
114
+ "command": "tanagram",
115
+ "type": "command"
145
116
  }
146
117
  ]
147
118
  }
@@ -152,6 +123,45 @@ For example, your full `settings.json` file might look like this:
152
123
 
153
124
  If you have existing hooks, you can merge this hook into your existing config.
154
125
 
126
+ ### Cursor Hook
127
+
128
+ Install the CLI as a Cursor Code [hook](https://cursor.com/docs/agent/hooks) to have Cursor automatically iterate on Tanagram's output.
129
+
130
+ **Easy setup (recommended):**
131
+ ```bash
132
+ tanagram config cursor
133
+ ```
134
+
135
+ This automatically adds the hook to your `~/.cursor/hooks.json`. It's safe to run multiple times and will preserve any existing settings.
136
+
137
+ **Check hook status:**
138
+ ```bash
139
+ tanagram config list
140
+ ```
141
+
142
+ **Manual setup (alternative):**
143
+ If you prefer to manually edit your settings, add this to your `~/.cursor/hooks.json` (user settings) or `.cursor/hooks.json` (project settings):
144
+
145
+ ```json
146
+ {
147
+ "hooks": {
148
+ "beforeSubmitPrompt": [
149
+ {
150
+ "command": "tanagram snapshot"
151
+ }
152
+ ],
153
+ "stop": [
154
+ {
155
+ "command": "tanagram"
156
+ }
157
+ ]
158
+ },
159
+ "version": 1
160
+ }
161
+ ```
162
+
163
+ If you have existing hooks, you can merge this hook into your existing config.
164
+
155
165
 
156
166
  ## How It Works
157
167
 
@@ -171,24 +181,6 @@ Policies are cached in `.tanagram/cache.gob` at your git repository root. Add th
171
181
  .tanagram/
172
182
  ```
173
183
 
174
- ## Fully LLM-Based Architecture
175
-
176
- Tanagram uses **100% LLM-powered** policy extraction and enforcement:
177
-
178
- ### Extraction Phase
179
- Claude AI extracts **ALL** policies from instruction files:
180
- - No classification needed (no MUST_NOT_USE, MUST_USE, etc.)
181
- - No regex pattern generation
182
- - Simple: Just extract policy names and descriptions
183
- - Fast: Simpler prompts = faster responses
184
-
185
- ### Detection Phase
186
- Claude AI analyzes code changes against all policies:
187
- - **Semantic understanding** - Not just pattern matching
188
- - **Context-aware** - Understands code intent and structure
189
- - **Language-agnostic** - Works with any programming language
190
- - **Detailed reasoning** - Explains why code violates each policy
191
-
192
184
  ### What Can Be Enforced
193
185
 
194
186
  **Everything!** Because the LLM reads and understands code like a human:
@@ -210,7 +202,7 @@ Claude AI analyzes code changes against all policies:
210
202
  - Won't flag Go code for missing Python type hints
211
203
  - Understands JavaScript !== Python !== Go
212
204
 
213
- ## Exit Codes
205
+ ### Exit Codes
214
206
 
215
207
  - `0` - No violations found
216
208
  - `2` - Violations found (triggers Claude Code automatic fix behavior)
@@ -246,6 +238,28 @@ Then run `tanagram` to enforce them locally!
246
238
 
247
239
  **Note:** For `.mdc` files, Tanagram extracts policies from the markdown content only (YAML frontmatter is used by Cursor and ignored during policy extraction).
248
240
 
241
+ ## Tanagram Web Integration
242
+
243
+ You can also use [Tanagram](https://tanagram.ai) to manage policies across your organization and enforce them on PRs.
244
+ If you have policies defined online, you can enforce them while you develop locally with the CLI as well.
245
+
246
+ ```bash
247
+ # Connect your account
248
+ tanagram login
249
+
250
+ # Download policies from your Tanagram account and cache them locally
251
+ tanagram sync
252
+ ```
253
+
254
+ For customers with an on-prem installation, set the `TANAGRAM_WEB_HOSTNAME` environment variable to the URL of your Tanagram instance — for example:
255
+
256
+ ```bash
257
+ export TANAGRAM_WEB_HOSTNAME=https://yourcompany.tanagram.ai
258
+
259
+ tanagram login
260
+ tanagram sync
261
+ ```
262
+
249
263
  ---
250
264
 
251
265
  Built by [@fluttermatt](https://x.com/fluttermatt) and the [Tanagram](https://tanagram.ai/) team. Talk to us [on Twitter](https://x.com/tanagram_) or email: founders AT tanagram.ai
package/api/client.go CHANGED
@@ -5,7 +5,9 @@ import (
5
5
  "fmt"
6
6
  "io"
7
7
  "net/http"
8
+ "net/url"
8
9
  "os"
10
+ "strings"
9
11
  "time"
10
12
 
11
13
  "github.com/tanagram/cli/auth"
@@ -50,8 +52,16 @@ type PolicyListResponse struct {
50
52
 
51
53
  // getAPIBaseURL returns the API base URL
52
54
  func getAPIBaseURL() string {
53
- if url := os.Getenv("TANAGRAM_API_URL"); url != "" {
54
- return url
55
+ if urlStr := os.Getenv("TANAGRAM_WEB_HOSTNAME"); urlStr != "" {
56
+ u, err := url.Parse(urlStr)
57
+ if err != nil {
58
+ return urlStr
59
+ }
60
+ if !strings.HasPrefix(u.Host, "api-") {
61
+ u.Host = "api-" + u.Host
62
+ return u.String()
63
+ }
64
+ return urlStr
55
65
  }
56
66
  return "https://api.tanagram.ai"
57
67
  }
@@ -0,0 +1,53 @@
1
+ package api
2
+
3
+ import (
4
+ "os"
5
+ "testing"
6
+ )
7
+
8
+ func TestGetAPIBaseURL(t *testing.T) {
9
+ // Save original env var
10
+ orig := os.Getenv("TANAGRAM_WEB_HOSTNAME")
11
+ defer os.Setenv("TANAGRAM_WEB_HOSTNAME", orig)
12
+
13
+ tests := []struct {
14
+ name string
15
+ envValue string
16
+ want string
17
+ }{
18
+ {
19
+ name: "default",
20
+ envValue: "",
21
+ want: "https://api.tanagram.ai",
22
+ },
23
+ {
24
+ name: "with api- prefix",
25
+ envValue: "https://api-runway.tanagram.ai",
26
+ want: "https://api-runway.tanagram.ai",
27
+ },
28
+ {
29
+ name: "without api- prefix",
30
+ envValue: "https://runway.tanagram.ai",
31
+ want: "https://api-runway.tanagram.ai",
32
+ },
33
+ {
34
+ name: "http without api- prefix",
35
+ envValue: "http://localhost:8080",
36
+ want: "http://api-localhost:8080",
37
+ },
38
+ }
39
+
40
+ for _, tt := range tests {
41
+ t.Run(tt.name, func(t *testing.T) {
42
+ if tt.envValue == "" {
43
+ os.Unsetenv("TANAGRAM_WEB_HOSTNAME")
44
+ } else {
45
+ os.Setenv("TANAGRAM_WEB_HOSTNAME", tt.envValue)
46
+ }
47
+
48
+ if got := getAPIBaseURL(); got != tt.want {
49
+ t.Errorf("getAPIBaseURL() = %v, want %v", got, tt.want)
50
+ }
51
+ })
52
+ }
53
+ }
@@ -22,36 +22,36 @@ func ConfigClaude(settingsPath string) error {
22
22
  settings["hooks"] = hooks
23
23
  }
24
24
 
25
- // Check if PreToolUse exists
26
- preToolUse, preToolUseExist := hooks["PreToolUse"].([]interface{})
27
- if !preToolUseExist {
28
- preToolUse = []interface{}{}
25
+ // Check if SessionStart exists
26
+ sessionStart, sessionStartExist := hooks["SessionStart"].([]interface{})
27
+ if !sessionStartExist {
28
+ sessionStart = []interface{}{}
29
29
  }
30
30
 
31
- // Check if PostToolUse exists
32
- postToolUse, postToolUseExist := hooks["PostToolUse"].([]interface{})
33
- if !postToolUseExist {
34
- postToolUse = []interface{}{}
31
+ // Check if Stop exists
32
+ stopHooks, stopHooksExist := hooks["Stop"].([]interface{})
33
+ if !stopHooksExist {
34
+ stopHooks = []interface{}{}
35
35
  }
36
36
 
37
37
  // Check if tanagram hooks already exist
38
- preHookExists := false
39
- postHookExists := false
38
+ sessionStartHookExists := false
39
+ stopHookExists := false
40
40
 
41
- for _, hook := range preToolUse {
41
+ for _, hook := range sessionStart {
42
42
  hookMap, ok := hook.(map[string]interface{})
43
43
  if !ok {
44
44
  continue
45
45
  }
46
46
 
47
- if matcher, ok := hookMap["matcher"].(string); ok && matcher == "Edit|Write" {
47
+ if matcher, ok := hookMap["matcher"].(string); ok && matcher == "startup|clear" {
48
48
  innerHooks, ok := hookMap["hooks"].([]interface{})
49
49
  if ok {
50
50
  for _, innerHook := range innerHooks {
51
51
  innerHookMap, ok := innerHook.(map[string]interface{})
52
52
  if ok {
53
- if cmd, ok := innerHookMap["command"].(string); ok && cmd == "tanagram snapshot" {
54
- preHookExists = true
53
+ if cmd, ok := innerHookMap["command"].(string); ok && strings.HasSuffix(cmd, "tanagram snapshot") {
54
+ sessionStartHookExists = true
55
55
  break
56
56
  }
57
57
  }
@@ -60,37 +60,34 @@ func ConfigClaude(settingsPath string) error {
60
60
  }
61
61
  }
62
62
 
63
- for _, hook := range postToolUse {
63
+ for _, hook := range stopHooks {
64
64
  hookMap, ok := hook.(map[string]interface{})
65
65
  if !ok {
66
66
  continue
67
67
  }
68
68
 
69
- if matcher, ok := hookMap["matcher"].(string); ok && matcher == "Edit|Write" {
70
- innerHooks, ok := hookMap["hooks"].([]interface{})
71
- if ok {
72
- for _, innerHook := range innerHooks {
73
- innerHookMap, ok := innerHook.(map[string]interface{})
74
- if ok {
75
- if cmd, ok := innerHookMap["command"].(string); ok && cmd == "tanagram" {
76
- postHookExists = true
77
- break
78
- }
69
+ innerHooks, ok := hookMap["hooks"].([]interface{})
70
+ if ok {
71
+ for _, innerHook := range innerHooks {
72
+ innerHookMap, ok := innerHook.(map[string]interface{})
73
+ if ok {
74
+ if cmd, ok := innerHookMap["command"].(string); ok && strings.HasSuffix(cmd, "tanagram") {
75
+ stopHookExists = true
76
+ break
79
77
  }
80
78
  }
81
79
  }
82
-
83
80
  }
84
81
  }
85
82
 
86
- if preHookExists && postHookExists {
83
+ if sessionStartHookExists && stopHookExists {
87
84
  fmt.Printf("✓ Tanagram hooks are already configured in %s\n", settingsPath)
88
85
  return nil
89
86
  }
90
87
 
91
- if !preHookExists {
92
- preHook := map[string]interface{}{
93
- "matcher": "Edit|Write",
88
+ if !sessionStartHookExists {
89
+ startHook := map[string]interface{}{
90
+ "matcher": "startup|clear",
94
91
  "hooks": []interface{}{
95
92
  map[string]interface{}{
96
93
  "type": "command",
@@ -98,13 +95,12 @@ func ConfigClaude(settingsPath string) error {
98
95
  },
99
96
  },
100
97
  }
101
- preToolUse = append(preToolUse, preHook)
102
- hooks["PreToolUse"] = preToolUse
98
+ sessionStart = append(sessionStart, startHook)
99
+ hooks["SessionStart"] = sessionStart
103
100
  }
104
101
 
105
- if !postHookExists {
106
- postHook := map[string]interface{}{
107
- "matcher": "Edit|Write",
102
+ if !stopHookExists {
103
+ stopHook := map[string]interface{}{
108
104
  "hooks": []interface{}{
109
105
  map[string]interface{}{
110
106
  "type": "command",
@@ -112,8 +108,8 @@ func ConfigClaude(settingsPath string) error {
112
108
  },
113
109
  },
114
110
  }
115
- postToolUse = append(postToolUse, postHook)
116
- hooks["PostToolUse"] = postToolUse
111
+ stopHooks = append(stopHooks, stopHook)
112
+ hooks["Stop"] = stopHooks
117
113
  }
118
114
 
119
115
  if err := saveConfig(settingsPath, settings); err != nil {
@@ -122,8 +118,8 @@ func ConfigClaude(settingsPath string) error {
122
118
 
123
119
  fmt.Printf("✓ Tanagram hooks added to %s\n", settingsPath)
124
120
  fmt.Println("\nClaude Code will now:")
125
- fmt.Println(" - Snapshot file state before each Edit/Write (PreToolUse)")
126
- fmt.Println(" - Check only Claude's changes after Edit/Write (PostToolUse)")
121
+ fmt.Println(" - Snapshot file state at session start (SessionStart)")
122
+ fmt.Println(" - Check only Claude's changes after agent completes (Stop)")
127
123
  fmt.Println(" - Send policy violations to Claude for automatic fixing")
128
124
  fmt.Println("\nThis prevents false positives from user-written code!")
129
125
 
@@ -172,7 +168,7 @@ func ConfigCursor(hooksPath string) error {
172
168
  continue
173
169
  }
174
170
 
175
- if cmd, ok := hookMap["command"].(string); ok && cmd == "tanagram snapshot" {
171
+ if cmd, ok := hookMap["command"].(string); ok && strings.HasSuffix(cmd, "tanagram snapshot") {
176
172
  beforeSubmitHookExists = true
177
173
  break
178
174
  }
@@ -184,7 +180,7 @@ func ConfigCursor(hooksPath string) error {
184
180
  continue
185
181
  }
186
182
 
187
- if cmd, ok := hookMap["command"].(string); ok && cmd == "tanagram" {
183
+ if cmd, ok := hookMap["command"].(string); ok && strings.HasSuffix(cmd, "tanagram") {
188
184
  stopHookExists = true
189
185
  break
190
186
  }
@@ -264,13 +260,13 @@ func ConfigList() error {
264
260
 
265
261
  // ClaudeHookStatus represents the status of a hook configuration
266
262
  type ClaudeHookStatus struct {
267
- FileExists bool
268
- PreHookExists bool
269
- PostHookExists bool
270
- IsUpToDate bool
271
- PreCommand string
272
- PostCommand string
273
- Error error
263
+ FileExists bool
264
+ SessionStartHookExists bool
265
+ StopHookExists bool
266
+ IsUpToDate bool
267
+ SessionStartCommand string
268
+ StopCommand string
269
+ Error error
274
270
  }
275
271
 
276
272
  // CursorHookStatus represents the status of a Cursor hook configuration
@@ -287,13 +283,13 @@ type CursorHookStatus struct {
287
283
  // checkClaudeHookStatus checks the status of Tanagram hook in a settings file
288
284
  func checkClaudeHookStatus(settingsPath string) ClaudeHookStatus {
289
285
  status := ClaudeHookStatus{
290
- FileExists: false,
291
- PreHookExists: false,
292
- PostHookExists: false,
293
- IsUpToDate: false,
294
- PreCommand: "",
295
- PostCommand: "",
296
- Error: nil,
286
+ FileExists: false,
287
+ SessionStartHookExists: false,
288
+ StopHookExists: false,
289
+ IsUpToDate: false,
290
+ SessionStartCommand: "",
291
+ StopCommand: "",
292
+ Error: nil,
297
293
  }
298
294
 
299
295
  if _, err := os.Stat(settingsPath); os.IsNotExist(err) {
@@ -313,15 +309,15 @@ func checkClaudeHookStatus(settingsPath string) ClaudeHookStatus {
313
309
  return status
314
310
  }
315
311
 
316
- if preToolUse, ok := hooks["PreToolUse"].([]interface{}); ok {
317
- for _, hook := range preToolUse {
312
+ if sessionStart, ok := hooks["SessionStart"].([]interface{}); ok {
313
+ for _, hook := range sessionStart {
318
314
  hookMap, ok := hook.(map[string]interface{})
319
315
  if !ok {
320
316
  continue
321
317
  }
322
318
 
323
319
  matcher, ok := hookMap["matcher"].(string)
324
- if !ok || matcher != "Edit|Write" {
320
+ if !ok || matcher != "startup|clear" {
325
321
  continue
326
322
  }
327
323
 
@@ -338,25 +334,20 @@ func checkClaudeHookStatus(settingsPath string) ClaudeHookStatus {
338
334
 
339
335
  cmd, cmdOk := innerHookMap["command"].(string)
340
336
  if cmdOk && strings.Contains(cmd, "tanagram snapshot") {
341
- status.PreHookExists = true
342
- status.PreCommand = cmd
337
+ status.SessionStartHookExists = true
338
+ status.SessionStartCommand = cmd
343
339
  }
344
340
  }
345
341
  }
346
342
  }
347
343
 
348
- if postToolUse, ok := hooks["PostToolUse"].([]interface{}); ok {
349
- for _, hook := range postToolUse {
344
+ if stopHooks, ok := hooks["Stop"].([]interface{}); ok {
345
+ for _, hook := range stopHooks {
350
346
  hookMap, ok := hook.(map[string]interface{})
351
347
  if !ok {
352
348
  continue
353
349
  }
354
350
 
355
- matcher, ok := hookMap["matcher"].(string)
356
- if !ok || matcher != "Edit|Write" {
357
- continue
358
- }
359
-
360
351
  innerHooks, ok := hookMap["hooks"].([]interface{})
361
352
  if !ok {
362
353
  continue
@@ -371,15 +362,15 @@ func checkClaudeHookStatus(settingsPath string) ClaudeHookStatus {
371
362
  cmd, cmdOk := innerHookMap["command"].(string)
372
363
  hookType, typeOk := innerHookMap["type"].(string)
373
364
 
374
- if cmdOk && cmd == "tanagram" && typeOk && hookType == "command" {
375
- status.PostHookExists = true
376
- status.PostCommand = cmd
365
+ if cmdOk && strings.HasSuffix(cmd, "tanagram") && typeOk && hookType == "command" {
366
+ status.StopHookExists = true
367
+ status.StopCommand = cmd
377
368
  }
378
369
  }
379
370
  }
380
371
  }
381
372
 
382
- if status.PreHookExists && status.PostHookExists {
373
+ if status.SessionStartHookExists && status.StopHookExists {
383
374
  status.IsUpToDate = true
384
375
  }
385
376
 
@@ -423,7 +414,7 @@ func checkCursorHookStatus(hooksPath string) CursorHookStatus {
423
414
  }
424
415
 
425
416
  cmd, cmdOk := hookMap["command"].(string)
426
- if cmdOk && cmd == "tanagram snapshot" {
417
+ if cmdOk && strings.HasSuffix(cmd, "tanagram snapshot") {
427
418
  status.BeforeSubmitExists = true
428
419
  status.BeforeSubmitCommand = cmd
429
420
  }
@@ -438,7 +429,7 @@ func checkCursorHookStatus(hooksPath string) CursorHookStatus {
438
429
  }
439
430
 
440
431
  cmd, cmdOk := hookMap["command"].(string)
441
- if cmdOk && cmd == "tanagram" {
432
+ if cmdOk && strings.HasSuffix(cmd, "tanagram") {
442
433
  status.StopExists = true
443
434
  status.StopCommand = cmd
444
435
  }
@@ -465,7 +456,7 @@ func printClaudeHookStatus(status ClaudeHookStatus, path string) {
465
456
  return
466
457
  }
467
458
 
468
- if !status.PreHookExists && !status.PostHookExists {
459
+ if !status.SessionStartHookExists && !status.StopHookExists {
469
460
  fmt.Printf(" ○ Not configured\n")
470
461
  fmt.Printf(" → Run: tanagram config claude\n")
471
462
  return
@@ -473,20 +464,20 @@ func printClaudeHookStatus(status ClaudeHookStatus, path string) {
473
464
 
474
465
  if status.IsUpToDate {
475
466
  fmt.Printf(" ✓ Configured and up to date\n")
476
- fmt.Printf(" PreToolUse: %s\n", status.PreCommand)
477
- fmt.Printf(" PostToolUse: %s\n", status.PostCommand)
467
+ fmt.Printf(" SessionStart: %s\n", status.SessionStartCommand)
468
+ fmt.Printf(" Stop: %s\n", status.StopCommand)
478
469
  fmt.Printf(" Location: %s\n", path)
479
470
  } else {
480
471
  fmt.Printf(" ⚠ Configured but incomplete\n")
481
- if status.PreHookExists {
482
- fmt.Printf(" ✓ PreToolUse: %s\n", status.PreCommand)
472
+ if status.SessionStartHookExists {
473
+ fmt.Printf(" ✓ SessionStart: %s\n", status.SessionStartCommand)
483
474
  } else {
484
- fmt.Printf(" ✗ PreToolUse: missing\n")
475
+ fmt.Printf(" ✗ SessionStart: missing\n")
485
476
  }
486
- if status.PostHookExists {
487
- fmt.Printf(" ✓ PostToolUse: %s\n", status.PostCommand)
477
+ if status.StopHookExists {
478
+ fmt.Printf(" ✓ Stop: %s\n", status.StopCommand)
488
479
  } else {
489
- fmt.Printf(" ✗ PostToolUse: missing\n")
480
+ fmt.Printf(" ✗ Stop: missing\n")
490
481
  }
491
482
  fmt.Printf(" → Run: tanagram config claude\n")
492
483
  }
@@ -550,7 +541,7 @@ func ensureClaudeConfigured() error {
550
541
  }
551
542
 
552
543
  // If file doesn't exist or hooks aren't configured, set them up
553
- if !status.FileExists || !status.PreHookExists || !status.PostHookExists {
544
+ if !status.FileExists || !status.SessionStartHookExists || !status.StopHookExists {
554
545
  fmt.Println("Setting up Claude Code integration...")
555
546
  if err := ConfigClaude(settingsPath); err != nil {
556
547
  // Don't fail the command if hook setup fails - just warn