Skip to content

Commit e0353ec

Browse files
committed
fix: remove orphaned plugin versions on launch/sync to prevent duplicate hook execution
Also sets terminal title to 'claude (project)' on launch for clean VS Code tabs. Closes #6, closes #7
1 parent f2fb410 commit e0353ec

1 file changed

Lines changed: 77 additions & 0 deletions

File tree

cmd/claude-rig/commands.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2921,6 +2921,9 @@ func cmdSync(args []string) error {
29212921
saveRigConfig(dir, cfg)
29222922
fmt.Printf(" %s — synced %d plugin(s)\n", name, len(synced))
29232923
}
2924+
if removed := removeOrphanedVersions(dir); removed > 0 {
2925+
fmt.Printf(" %s — removed %d orphaned plugin version(s)\n", name, removed)
2926+
}
29242927
}
29252928

29262929
// 4. MCP servers
@@ -3056,6 +3059,11 @@ func cmdLaunch(args []string) error {
30563059
fmt.Fprintf(os.Stderr, "Warning: could not sync inherited contents: %v\n", err)
30573060
}
30583061

3062+
// Remove orphaned plugin versions so Claude Code doesn't fire their hooks
3063+
if n := removeOrphanedVersions(dir); n > 0 {
3064+
fmt.Fprintf(os.Stderr, "Cleaned %d orphaned plugin version(s)\n", n)
3065+
}
3066+
30593067
// Resolve binary: pinned version → latest on disk → system symlink fallback
30603068
rigCfg := loadRigConfig(dir)
30613069
binary := claudeCodeBinary()
@@ -3136,6 +3144,11 @@ func cmdLaunch(args []string) error {
31363144
extraArgs = append(extraArgs, "--plugin-dir", pluginDir)
31373145
}
31383146

3147+
// Set terminal title to "claude (project)" for clean VS Code tabs
3148+
if wd, err := os.Getwd(); err == nil {
3149+
fmt.Fprintf(os.Stderr, "\033]0;claude (%s)\007", filepath.Base(wd))
3150+
}
3151+
31393152
execArgs := append([]string{binary}, defaultArgs...)
31403153
execArgs = append(execArgs, extraArgs...)
31413154
return execLaunch(binPath, execArgs, env)
@@ -3395,6 +3408,9 @@ func cmdUpdatePlugins(args []string) error {
33953408
if cleaned := cleanOrphanedInstalled(dir); cleaned > 0 {
33963409
fmt.Fprintf(&buf, " Cleaned %d stale orphan markers\n", cleaned)
33973410
}
3411+
if removed := removeOrphanedVersions(dir); removed > 0 {
3412+
fmt.Fprintf(&buf, " Removed %d orphaned version dir(s)\n", removed)
3413+
}
33983414

33993415
// Reconcile plugin MCP servers in .mcp.json
34003416
if err := syncPluginMCP(dir); err != nil {
@@ -3594,6 +3610,67 @@ func cleanOrphanedInstalled(rigDir string) int {
35943610
return cleaned
35953611
}
35963612

3613+
// removeOrphanedVersions deletes plugin cache version directories that have an
3614+
// .orphaned_at marker and are NOT referenced by installed_plugins.json.
3615+
// This prevents Claude Code's loader from firing hooks on dead versions.
3616+
func removeOrphanedVersions(rigDir string) int {
3617+
installedPath := filepath.Join(rigDir, "plugins", "installed_plugins.json")
3618+
data, err := os.ReadFile(installedPath)
3619+
if err != nil {
3620+
return 0
3621+
}
3622+
3623+
var manifest struct {
3624+
Plugins map[string][]struct {
3625+
InstallPath string `json:"installPath"`
3626+
} `json:"plugins"`
3627+
}
3628+
if err := json.Unmarshal(data, &manifest); err != nil {
3629+
return 0
3630+
}
3631+
3632+
activePaths := make(map[string]bool)
3633+
for _, entries := range manifest.Plugins {
3634+
for _, e := range entries {
3635+
if e.InstallPath != "" {
3636+
p := e.InstallPath
3637+
// Resolve symlinks so we match both the symlink and its target
3638+
if resolved, err := filepath.EvalSymlinks(p); err == nil {
3639+
activePaths[resolved] = true
3640+
}
3641+
activePaths[p] = true
3642+
}
3643+
}
3644+
}
3645+
3646+
removed := 0
3647+
cacheDir := filepath.Join(rigDir, "plugins", "cache")
3648+
_ = filepath.WalkDir(cacheDir, func(path string, d os.DirEntry, err error) error {
3649+
if err != nil || d.IsDir() || d.Name() != ".orphaned_at" {
3650+
return nil
3651+
}
3652+
versionDir := filepath.Dir(path)
3653+
resolved := versionDir
3654+
if r, err := filepath.EvalSymlinks(versionDir); err == nil {
3655+
resolved = r
3656+
}
3657+
if activePaths[versionDir] || activePaths[resolved] {
3658+
return nil // active version, don't touch
3659+
}
3660+
if os.RemoveAll(versionDir) == nil {
3661+
removed++
3662+
// Clean empty parent dirs up to cache/
3663+
for dir := filepath.Dir(versionDir); dir != cacheDir; dir = filepath.Dir(dir) {
3664+
if err := os.Remove(dir); err != nil {
3665+
break
3666+
}
3667+
}
3668+
}
3669+
return nil
3670+
})
3671+
return removed
3672+
}
3673+
35973674
// cleanSyncedAfterUninstall reconciles rig.json's synced_plugins list with the
35983675
// current installed_plugins.json after a plugin uninstall. Removes entries from
35993676
// synced_plugins that are no longer installed, and cleans up orphaned cache symlinks.

0 commit comments

Comments
 (0)