Skip to content

Commit dfc0c6d

Browse files
nicksenapclaude
andcommitted
Replace hardcoded CLAUDE.md copy with post_create lifecycle hook
Move CLAUDE.md copying from a standalone function with interactive prompt to the post_create hook system. Legacy fallback copies from parent dir when no hook is configured. Doctor nudges users to migrate. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b9a1700 commit dfc0c6d

File tree

2 files changed

+39
-60
lines changed

2 files changed

+39
-60
lines changed

cmd/create.go

Lines changed: 21 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
11
package cmd
22

33
import (
4+
"errors"
45
"os"
56
"path/filepath"
67
"strings"
78

89
"github.com/nicksenap/grove/internal/config"
910
"github.com/nicksenap/grove/internal/console"
1011
"github.com/nicksenap/grove/internal/discover"
12+
"github.com/nicksenap/grove/internal/lifecycle"
1113
"github.com/nicksenap/grove/internal/models"
1214
"github.com/nicksenap/grove/internal/picker"
1315
"github.com/nicksenap/grove/internal/workspace"
1416
"github.com/spf13/cobra"
1517
)
1618

1719
var (
18-
createBranch string
19-
createRepos string
20-
createPreset string
21-
createAll bool
22-
createCopyClaudeMD *bool
20+
createBranch string
21+
createRepos string
22+
createPreset string
23+
createAll bool
2324
)
2425

2526
var createCmd = &cobra.Command{
@@ -143,25 +144,14 @@ var createCmd = &cobra.Command{
143144
exitError(err.Error())
144145
}
145146

146-
// Copy CLAUDE.md if requested
147+
// Fire post_create hook if configured
147148
wsPath := filepath.Join(cfg.WorkspaceDir, name)
148-
if createCopyClaudeMD != nil {
149-
if *createCopyClaudeMD {
150-
copyCLAUDEmd(repoMap, repoNames, wsPath)
151-
}
152-
} else if console.IsTerminal(os.Stdin) {
153-
// Auto-detect: check if any repo has a CLAUDE.md
154-
for _, repoName := range repoNames {
155-
if src, ok := repoMap[repoName]; ok {
156-
claudeMD := filepath.Join(src, "CLAUDE.md")
157-
if _, err := os.Stat(claudeMD); err == nil {
158-
if console.Confirm("Copy CLAUDE.md into workspace?", true) {
159-
copyCLAUDEmd(repoMap, repoNames, wsPath)
160-
}
161-
break
162-
}
163-
}
164-
}
149+
vars := lifecycle.Vars{Name: name, Path: wsPath, Branch: branch}
150+
if err := lifecycle.Run("post_create", vars); errors.Is(err, lifecycle.ErrNoHook) {
151+
// TODO: remove when matured — legacy fallback for users without [hooks] config.
152+
copyParentCLAUDEmd(wsPath)
153+
} else if err != nil {
154+
console.Warningf("post_create hook failed: %s", err)
165155
}
166156
},
167157
}
@@ -171,39 +161,20 @@ func init() {
171161
createCmd.Flags().StringVarP(&createRepos, "repos", "r", "", "Comma-separated repo names")
172162
createCmd.Flags().StringVarP(&createPreset, "preset", "p", "", "Use named preset")
173163
createCmd.Flags().BoolVar(&createAll, "all", false, "Use all discovered repos")
174-
createCmd.Flags().Bool("copy-claude-md", false, "Copy CLAUDE.md into workspace dir")
175-
createCmd.Flags().Bool("no-copy-claude-md", false, "Don't copy CLAUDE.md")
176164

177165
createCmd.RegisterFlagCompletionFunc("repos", completeRepoNames)
178166
createCmd.RegisterFlagCompletionFunc("preset", completePresetNames)
179-
180-
// Resolve --copy-claude-md / --no-copy-claude-md
181-
createCmd.PreRun = func(cmd *cobra.Command, args []string) {
182-
if cmd.Flags().Changed("copy-claude-md") {
183-
v := true
184-
createCopyClaudeMD = &v
185-
} else if cmd.Flags().Changed("no-copy-claude-md") {
186-
v := false
187-
createCopyClaudeMD = &v
188-
}
189-
}
190167
}
191168

192-
func copyCLAUDEmd(repoMap map[string]string, repoNames []string, wsPath string) {
193-
for _, repoName := range repoNames {
194-
src, ok := repoMap[repoName]
195-
if !ok {
196-
continue
197-
}
198-
claudeMD := filepath.Join(src, "CLAUDE.md")
199-
data, err := os.ReadFile(claudeMD)
200-
if err != nil {
201-
continue
202-
}
203-
dst := filepath.Join(wsPath, "CLAUDE.md")
204-
os.WriteFile(dst, data, 0o644)
205-
return // only copy from first repo that has it
169+
170+
// TODO: remove when matured — legacy fallback for users without [hooks] config.
171+
func copyParentCLAUDEmd(wsPath string) {
172+
src := filepath.Join(wsPath, "..", "CLAUDE.md")
173+
data, err := os.ReadFile(src)
174+
if err != nil {
175+
return
206176
}
177+
os.WriteFile(filepath.Join(wsPath, "CLAUDE.md"), data, 0o644)
207178
}
208179

209180
func deriveName(branch string) string {

cmd/doctor.go

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -63,20 +63,28 @@ func checkMissingHooks() []models.DoctorIssue {
6363
return nil
6464
}
6565

66-
if _, ok := cfg.Hooks["on_close"]; ok {
67-
return nil
66+
var issues []models.DoctorIssue
67+
68+
if _, ok := cfg.Hooks["on_close"]; !ok {
69+
// Only flag if Zellij is present — that's what the old hardcoded behavior supported.
70+
if os.Getenv("ZELLIJ_SESSION_NAME") != "" {
71+
issues = append(issues, models.DoctorIssue{
72+
Workspace: "—",
73+
Issue: "no on_close hook configured (using legacy Zellij fallback)",
74+
SuggestedAction: "add [hooks] on_close to ~/.grove/config.toml",
75+
})
76+
}
6877
}
6978

70-
// Only flag if Zellij is present — that's what the old hardcoded behavior supported.
71-
if os.Getenv("ZELLIJ_SESSION_NAME") == "" {
72-
return nil
79+
if _, ok := cfg.Hooks["post_create"]; !ok {
80+
issues = append(issues, models.DoctorIssue{
81+
Workspace: "—",
82+
Issue: "no post_create hook configured (using legacy CLAUDE.md copy)",
83+
SuggestedAction: `add [hooks] post_create = "cp {path}/../CLAUDE.md {path}/CLAUDE.md 2>/dev/null || true"`,
84+
})
7385
}
7486

75-
return []models.DoctorIssue{{
76-
Workspace: "—",
77-
Issue: "no on_close hook configured (using legacy Zellij fallback)",
78-
SuggestedAction: "add [hooks] on_close to ~/.grove/config.toml",
79-
}}
87+
return issues
8088
}
8189

8290
func init() {

0 commit comments

Comments
 (0)