diff --git a/CHANGELOG.adoc b/CHANGELOG.adoc
index 9b633d56ba..208ff51e5a 100644
--- a/CHANGELOG.adoc
+++ b/CHANGELOG.adoc
@@ -6,6 +6,7 @@ This file documents all notable changes to https://github.com/devonfw/IDEasy[IDE
Release with new features and bugfixes:
+* https://github.com/devonfw/IDEasy/issues/2142[#2142]: Move IDE-specific metadata (.idea, .vscode) out of workspace
* https://github.com/devonfw/IDEasy/issues/989[#989]: Allow expressions in template variable definitions
The full list of changes for this release can be found in https://github.com/devonfw/IDEasy/milestone/50?closed=1[milestone 2026.09.002].
diff --git a/cli/src/main/java/com/devonfw/tools/ide/migration/IdeMigrator.java b/cli/src/main/java/com/devonfw/tools/ide/migration/IdeMigrator.java
index a79d56afb4..23958c07ba 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/migration/IdeMigrator.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/migration/IdeMigrator.java
@@ -9,6 +9,7 @@
import com.devonfw.tools.ide.log.IdeLogLevel;
import com.devonfw.tools.ide.migration.v2025.Mig202502001;
import com.devonfw.tools.ide.migration.v2025.Mig202510001;
+import com.devonfw.tools.ide.migration.v2026.Mig202609002;
import com.devonfw.tools.ide.step.Step;
import com.devonfw.tools.ide.version.IdeVersion;
import com.devonfw.tools.ide.version.VersionIdentifier;
@@ -31,7 +32,7 @@ public class IdeMigrator implements IdeMigration {
public IdeMigrator() {
// migrations must be strictly in ascending order (from oldest to newest version)
- this(List.of(new Mig202502001(), new Mig202510001()));
+ this(List.of(new Mig202502001(), new Mig202510001(), new Mig202609002()));
}
/**
diff --git a/cli/src/main/java/com/devonfw/tools/ide/migration/v2026/Mig202609002.java b/cli/src/main/java/com/devonfw/tools/ide/migration/v2026/Mig202609002.java
new file mode 100644
index 0000000000..d190ecdc76
--- /dev/null
+++ b/cli/src/main/java/com/devonfw/tools/ide/migration/v2026/Mig202609002.java
@@ -0,0 +1,55 @@
+package com.devonfw.tools.ide.migration.v2026;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.devonfw.tools.ide.context.IdeContext;
+import com.devonfw.tools.ide.io.FileAccess;
+import com.devonfw.tools.ide.migration.IdeVersionMigration;
+
+/**
+ * Migration to 2026.09.002. Moves the VSCode user-data folder ({@code .vscode/.userdata}) out of each workspace into the dedicated
+ * {@code $IDE_HOME/.ide/vscode/«workspace»/config} folder so workspaces stay clean and independent of the IDE being used. See
+ * #2142.
+ */
+public class Mig202609002 extends IdeVersionMigration {
+
+ private static final Logger LOG = LoggerFactory.getLogger(Mig202609002.class);
+
+ /**
+ * The constructor.
+ */
+ public Mig202609002() {
+
+ super("2026.09.002");
+ }
+
+ @Override
+ public void run(IdeContext context) {
+
+ Path workspacesPath = context.getWorkspacesBasePath();
+ if (workspacesPath == null) {
+ return;
+ }
+ FileAccess fileAccess = context.getFileAccess();
+ Path vscodeMetaPath = context.getIdeHome().resolve(IdeContext.FOLDER_DOT_IDE).resolve("vscode");
+ List workspaces = fileAccess.listChildren(workspacesPath, Files::isDirectory);
+ for (Path workspace : workspaces) {
+ Path oldUserData = workspace.resolve(".vscode").resolve(".userdata");
+ if (fileAccess.isExpectedFolder(oldUserData)) {
+ Path target = vscodeMetaPath.resolve(workspace.getFileName().toString()).resolve("config");
+ if (Files.exists(target)) {
+ LOG.warn("Skipping migration of {} since target already exists: {}", oldUserData, target);
+ continue;
+ }
+ fileAccess.mkdirs(target.getParent());
+ fileAccess.move(oldUserData, target);
+ }
+ }
+ }
+
+}
diff --git a/cli/src/main/java/com/devonfw/tools/ide/tool/vscode/Vscode.java b/cli/src/main/java/com/devonfw/tools/ide/tool/vscode/Vscode.java
index 57933bfd93..f1131384d1 100644
--- a/cli/src/main/java/com/devonfw/tools/ide/tool/vscode/Vscode.java
+++ b/cli/src/main/java/com/devonfw/tools/ide/tool/vscode/Vscode.java
@@ -86,7 +86,7 @@ protected void configureToolArgs(ProcessContext pc, ProcessMode processMode, Lis
if (this.context.getSystemInfo().isWsl()) {
pc.withEnvVar("DONT_PROMPT_WSL_INSTALL", "1");
}
- Path vsCodeConf = this.context.getWorkspacePath().resolve(".vscode/.userdata");
+ Path vsCodeConf = getIdeMetadataPath().resolve("config");
pc.addArg("--new-window");
pc.addArg("--user-data-dir=" + vsCodeConf);
Path vsCodeExtensionFolder = this.context.getIdeHome().resolve("plugins/vscode");
diff --git a/cli/src/test/java/com/devonfw/tools/ide/migration/v2026/Mig202609002Test.java b/cli/src/test/java/com/devonfw/tools/ide/migration/v2026/Mig202609002Test.java
new file mode 100644
index 0000000000..b5c833535d
--- /dev/null
+++ b/cli/src/test/java/com/devonfw/tools/ide/migration/v2026/Mig202609002Test.java
@@ -0,0 +1,74 @@
+package com.devonfw.tools.ide.migration.v2026;
+
+import java.nio.file.Path;
+
+import org.junit.jupiter.api.Test;
+
+import com.devonfw.tools.ide.context.AbstractIdeContextTest;
+import com.devonfw.tools.ide.context.IdeContext;
+import com.devonfw.tools.ide.context.IdeTestContext;
+import com.devonfw.tools.ide.io.FileAccess;
+
+/**
+ * Test of {@link Mig202609002}.
+ */
+class Mig202609002Test extends AbstractIdeContextTest {
+
+ /**
+ * Tests that an existing {@code .vscode/.userdata} folder is moved out of the workspace into {@code $IDE_HOME/.ide/vscode/«workspace»/config}.
+ */
+ @Test
+ void testMovesVscodeUserDataOutOfWorkspace() {
+
+ // arrange
+ IdeTestContext context = newContext("vscode");
+ FileAccess fileAccess = context.getFileAccess();
+ Path workspace = context.getWorkspacePath();
+ Path oldUserData = workspace.resolve(".vscode").resolve(".userdata");
+ fileAccess.mkdirs(oldUserData);
+ fileAccess.writeFileContent("dummy", oldUserData.resolve("state.json"));
+ // act
+ new Mig202609002().run(context);
+ // assert
+ Path newConfig = context.getIdeHome().resolve(IdeContext.FOLDER_DOT_IDE).resolve("vscode").resolve(context.getWorkspaceName()).resolve("config");
+ assertThat(newConfig.resolve("state.json")).exists().hasContent("dummy");
+ assertThat(oldUserData).doesNotExist();
+ }
+
+ /**
+ * Tests that the migration is a no-op (and does not fail) when no {@code .vscode/.userdata} folder exists.
+ */
+ @Test
+ void testDoesNothingWhenNoUserData() {
+
+ // arrange
+ IdeTestContext context = newContext("vscode");
+ Path vscodeMeta = context.getIdeHome().resolve(IdeContext.FOLDER_DOT_IDE).resolve("vscode");
+ // act
+ new Mig202609002().run(context);
+ // assert
+ assertThat(vscodeMeta).doesNotExist();
+ }
+
+ /**
+ * Tests that the migration skips a workspace whose target folder already exists, without failing and without overwriting the existing data.
+ */
+ @Test
+ void testSkipsWhenTargetAlreadyExists() {
+
+ // arrange
+ IdeTestContext context = newContext("vscode");
+ FileAccess fileAccess = context.getFileAccess();
+ Path oldUserData = context.getWorkspacePath().resolve(".vscode").resolve(".userdata");
+ fileAccess.mkdirs(oldUserData);
+ fileAccess.writeFileContent("old", oldUserData.resolve("state.json"));
+ Path target = context.getIdeHome().resolve(IdeContext.FOLDER_DOT_IDE).resolve("vscode").resolve(context.getWorkspaceName()).resolve("config");
+ fileAccess.mkdirs(target);
+ fileAccess.writeFileContent("new", target.resolve("state.json"));
+ // act
+ new Mig202609002().run(context);
+ // assert
+ assertThat(oldUserData).exists();
+ assertThat(target.resolve("state.json")).exists().hasContent("new");
+ }
+}
diff --git a/cli/src/test/java/com/devonfw/tools/ide/tool/vscode/VscodeTest.java b/cli/src/test/java/com/devonfw/tools/ide/tool/vscode/VscodeTest.java
index 72e57032a0..c6ecaada29 100644
--- a/cli/src/test/java/com/devonfw/tools/ide/tool/vscode/VscodeTest.java
+++ b/cli/src/test/java/com/devonfw/tools/ide/tool/vscode/VscodeTest.java
@@ -1,5 +1,6 @@
package com.devonfw.tools.ide.tool.vscode;
+import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
@@ -10,6 +11,7 @@
import org.junit.jupiter.api.Test;
import com.devonfw.tools.ide.context.AbstractIdeContextTest;
+import com.devonfw.tools.ide.context.IdeContext;
import com.devonfw.tools.ide.context.IdeTestContext;
import com.devonfw.tools.ide.context.ProcessContextTestImpl;
import com.devonfw.tools.ide.environment.EnvironmentVariablesType;
@@ -152,6 +154,26 @@ void testConfigureToolArgsDoesNotSetWslEnvVarOnNonWsl() {
assertThat(pc.getEnvVar("DONT_PROMPT_WSL_INSTALL")).isNull();
}
+ /**
+ * Tests that {@link Vscode#configureToolArgs(ProcessContext, ProcessMode, List)} points {@code --user-data-dir} to the IDE metadata folder
+ * ({@code $IDE_HOME/.ide/vscode/«workspace»/config}) instead of a {@code .vscode} folder inside the workspace.
+ */
+ @Test
+ void testConfigureToolArgsUsesIdeMetadataPathForUserData() {
+
+ // arrange
+ IdeTestContext context = newContext(PROJECT_VSCODE);
+ context.setSystemInfo(SystemInfoMock.LINUX_X64);
+ Vscode commandlet = new Vscode(context);
+ ArgCapturingProcessContext pc = new ArgCapturingProcessContext(context);
+ // act
+ commandlet.configureToolArgs(pc, ProcessMode.DEFAULT, List.of());
+ // assert
+ Path expectedUserData = context.getIdeHome().resolve(IdeContext.FOLDER_DOT_IDE).resolve("vscode").resolve(context.getWorkspaceName()).resolve("config");
+ assertThat(pc.capturedArgs).contains("--user-data-dir=" + expectedUserData);
+ assertThat(pc.capturedArgs).noneMatch(arg -> arg.contains(".vscode"));
+ }
+
/**
* Tests that {@code VSCODE_OPTIONS} is honoured by appending its tokens as additional command-line arguments when starting the IDE (analogue to the
* global {@code IDE_OPTIONS} used for IDEasy itself, see issue #788).
@@ -300,6 +322,26 @@ String getEnvVar(String key) {
}
}
+ /**
+ * {@link ProcessContextTestImpl} subclass that captures the CLI arguments added via {@link #addArg(String)} for test assertions.
+ */
+ private static class ArgCapturingProcessContext extends ProcessContextTestImpl {
+
+ private final List capturedArgs = new ArrayList<>();
+
+ private ArgCapturingProcessContext(IdeTestContext context) {
+
+ super(context);
+ }
+
+ @Override
+ public ProcessContext addArg(String arg) {
+
+ this.capturedArgs.add(arg);
+ return super.addArg(arg);
+ }
+ }
+
private void checkVscodiumInstallation(IdeTestContext context) {
assertThat(context.getSoftwarePath().resolve("vscode/bin/codium.cmd")).exists().hasContent("@echo test for windows");