@@ -133,8 +133,8 @@ func buildPlanOps(req installPlanRequest) ([]fileOp, []string, []string) {
133133 case "claude-code" :
134134 ops = append (ops , claudeCodePlan (req .DaemonURL , req .ConfigDirOverride , req .AgentlockBinary , req .StatusLineScript , req .HarnessConfigDirs , req .ExistingFiles ))
135135 case "claude-desktop" :
136- op , ws := claudeDesktopPlan (req .DaemonURL , req .ConfigDirOverride , req .AgentlockBinary , req .HarnessConfigDirs , req .ExistingFiles )
137- ops = append (ops , op )
136+ desktopOps , ws := claudeDesktopPlan (req .DaemonURL , req .ConfigDirOverride , req .AgentlockBinary , req .HarnessConfigDirs , req .ExistingFiles )
137+ ops = append (ops , desktopOps ... )
138138 warnings = append (warnings , ws ... )
139139 case "codex" :
140140 codexOps , ws := codexPlan (req .DaemonURL , req .ConfigDirOverride , req .AgentlockBinary , req .HarnessConfigDirs , req .ExistingFiles )
@@ -481,7 +481,13 @@ func installApplyHandler(d Deps) http.HandlerFunc {
481481func harnessForPath (path string ) string {
482482 dir := filepath .Base (filepath .Dir (path ))
483483 switch {
484- case strings .HasSuffix (path , "claude_desktop_config.json" ):
484+ case strings .HasSuffix (path , "claude_desktop_config.json" ),
485+ strings .HasSuffix (path , "extensions-installations.json" ):
486+ return "claude-desktop"
487+ case strings .HasSuffix (path , "manifest.json" ) &&
488+ strings .Contains (path , "/Claude Extensions/" ):
489+ // Bundle manifest of a Desktop Extension. Path shape:
490+ // <config-dir>/Claude Extensions/<ext-id>/manifest.json.
485491 return "claude-desktop"
486492 case strings .HasSuffix (path , "settings.json" ):
487493 // Gemini also writes settings.json (in ~/.gemini); disambiguate
@@ -682,7 +688,18 @@ func installUninstallHandler(d Deps) http.HandlerFunc {
682688 case "gemini" :
683689 newBytes , removed , stripErr = stripGeminiSettings (existing )
684690 case "claude-desktop" :
685- newBytes , removed , stripErr = stripClaudeDesktopConfig (existing )
691+ // Three file shapes land here:
692+ // - claude_desktop_config.json (manual mcpServers path)
693+ // - extensions-installations.json (Desktop Extensions registry)
694+ // - <Claude Extensions>/<id>/manifest.json (bundle manifests, the actual launch source)
695+ if strings .HasSuffix (e .SettingsPath , "extensions-installations.json" ) {
696+ newBytes , removed , stripErr = stripExtensionRegistry (existing )
697+ } else if strings .HasSuffix (e .SettingsPath , "manifest.json" ) &&
698+ strings .Contains (e .SettingsPath , "/Claude Extensions/" ) {
699+ newBytes , removed , stripErr = stripBundleManifest (existing )
700+ } else {
701+ newBytes , removed , stripErr = stripClaudeDesktopConfig (existing )
702+ }
686703 default :
687704 // Default to Claude's settings.json shape. Older manifests
688705 // without a Harness field land here, which is the right
@@ -890,26 +907,81 @@ func installUninstallHarnessesHandler(d Deps) http.HandlerFunc {
890907 }
891908 ops = append (ops , op )
892909 case "claude-desktop" :
893- p , err := claudeDesktopConfigPath (req .ConfigDirOverride , req .HarnessConfigDirs )
910+ // Strip both files Claude Desktop install touches: the
911+ // mcpServers config (claude_desktop_config.json) and
912+ // the Desktop Extensions registry
913+ // (extensions-installations.json). Each is independent
914+ // — one can be missing without affecting the other.
915+ cfgP , err := claudeDesktopConfigPath (req .ConfigDirOverride , req .HarnessConfigDirs )
894916 if err != nil {
895917 failures ++
896918 ops = append (ops , uninstallOp {Op : "strip" , Path : "<unresolved>" , Error : err .Error ()})
897- continue
898- }
899- existing := []byte (req .ExistingFiles [p ])
900- newBytes , removed , stripErr := stripClaudeDesktopConfig (existing )
901- op := uninstallOp {Op : "strip" , Path : p }
902- if stripErr != nil {
903- failures ++
904- op .Error = stripErr .Error ()
905- log .Printf ("install.uninstall_harnesses: strip claude-desktop %s: %v" , p , stripErr )
906919 } else {
907- op .EntriesRemoved = removed
908- if removed > 0 {
909- op .Content = string (newBytes )
920+ cfgExisting := []byte (req .ExistingFiles [cfgP ])
921+ newBytes , removed , stripErr := stripClaudeDesktopConfig (cfgExisting )
922+ op := uninstallOp {Op : "strip" , Path : cfgP }
923+ if stripErr != nil {
924+ failures ++
925+ op .Error = stripErr .Error ()
926+ log .Printf ("install.uninstall_harnesses: strip claude-desktop %s: %v" , cfgP , stripErr )
927+ } else {
928+ op .EntriesRemoved = removed
929+ if removed > 0 {
930+ op .Content = string (newBytes )
931+ }
932+ }
933+ ops = append (ops , op )
934+ }
935+ if regP , err := extensionsRegistryPath (req .ConfigDirOverride , req .HarnessConfigDirs ); err == nil {
936+ regExisting := []byte (req .ExistingFiles [regP ])
937+ if len (regExisting ) > 0 {
938+ newBytes , removed , stripErr := stripExtensionRegistry (regExisting )
939+ op := uninstallOp {Op : "strip" , Path : regP }
940+ if stripErr != nil {
941+ failures ++
942+ op .Error = stripErr .Error ()
943+ log .Printf ("install.uninstall_harnesses: strip claude-desktop extensions %s: %v" , regP , stripErr )
944+ } else {
945+ op .EntriesRemoved = removed
946+ if removed > 0 {
947+ op .Content = string (newBytes )
948+ }
949+ }
950+ ops = append (ops , op )
951+ }
952+ }
953+ // Strip every per-extension bundle manifest the CLI sent
954+ // us. The on-disk manifest is THE launch source for
955+ // Desktop Extensions (probed empirically) so this is
956+ // what actually un-gates the user.
957+ bundlesDir , _ := claudeDesktopExtensionsDir (req .ConfigDirOverride , req .HarnessConfigDirs )
958+ if bundlesDir != "" {
959+ absBundles := bundlesDir
960+ if a , err := filepath .Abs (bundlesDir ); err == nil {
961+ absBundles = a
962+ }
963+ for path , body := range req .ExistingFiles {
964+ if ! strings .HasSuffix (path , "/manifest.json" ) {
965+ continue
966+ }
967+ if filepath .Dir (filepath .Dir (path )) != absBundles {
968+ continue
969+ }
970+ newBytes , removed , stripErr := stripBundleManifest ([]byte (body ))
971+ op := uninstallOp {Op : "strip" , Path : path }
972+ if stripErr != nil {
973+ failures ++
974+ op .Error = stripErr .Error ()
975+ log .Printf ("install.uninstall_harnesses: strip claude-desktop bundle %s: %v" , path , stripErr )
976+ } else {
977+ op .EntriesRemoved = removed
978+ if removed > 0 {
979+ op .Content = string (newBytes )
980+ }
981+ }
982+ ops = append (ops , op )
910983 }
911984 }
912- ops = append (ops , op )
913985 default :
914986 if devHome == "" || ! knownHarnessID (h ) {
915987 // No real installer + not in dev mode → nothing to do.
0 commit comments