@@ -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