diff --git a/pom.xml b/pom.xml
index 41ca9862..35a46541 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
io.github.nbclaudecodegui
netbeans-claude-code-gui
- 0.21.28-SNAPSHOT
+ 0.21.29-SNAPSHOT
nbm
Netbeans Claude Code GUI
NetBeans plugin that provides a GUI for Claude Code CLI — run claude code sessions directly inside the IDE.
diff --git a/src/main/java/io/github/nbclaudecodegui/process/ClaudeProcess.java b/src/main/java/io/github/nbclaudecodegui/process/ClaudeProcess.java
index 53b62f40..d44b97a1 100644
--- a/src/main/java/io/github/nbclaudecodegui/process/ClaudeProcess.java
+++ b/src/main/java/io/github/nbclaudecodegui/process/ClaudeProcess.java
@@ -177,7 +177,7 @@ public PtyProcess start(String workingDir, ClaudeProfile profile, String extraCl
appendSessionFlags(cmd, workingDir, env, extraCliArgs, mode, resumeSessionId);
- lastCommand = String.join(" ", cmd);
+ lastCommand = toShellCommand(cmd);
LOG.info("Claude command: " + lastCommand);
PtyProcessBuilder builder = new PtyProcessBuilder(cmd.toArray(new String[0]))
.setEnvironment(env)
@@ -616,6 +616,37 @@ static Map buildEnv(ClaudeProfile profile, java.nio.file.Path pr
* Builds a minimal {@code settings.local.json} from scratch for the given port.
* Used as a fallback when the existing file cannot be parsed.
*/
+ /**
+ * Converts a command list to a shell-pasteable string.
+ * Arguments that contain spaces, quotes, or other shell-special characters
+ * are quoted so the result can be pasted directly into a terminal.
+ * On Windows, double-quotes inside an argument are doubled ("").
+ * On other platforms, they are backslash-escaped (\").
+ */
+ static String toShellCommand(List cmd) {
+ boolean windows = System.getProperty("os.name", "").toLowerCase().contains("win");
+ return toShellCommand(cmd, windows);
+ }
+
+ static String toShellCommand(List cmd, boolean windows) {
+ StringBuilder sb = new StringBuilder();
+ for (String arg : cmd) {
+ if (sb.length() > 0) sb.append(' ');
+ boolean needsQuoting = arg.isEmpty() || arg.chars().anyMatch(c ->
+ c == ' ' || c == '\t' || c == '"' || c == '\'' || c == '\\' ||
+ c == '{' || c == '}' || c == '(' || c == ')' ||
+ c == '&' || c == '|' || c == '<' || c == '>');
+ if (!needsQuoting) {
+ sb.append(arg);
+ } else if (windows) {
+ sb.append('"').append(arg.replace("\"", "\"\"")).append('"');
+ } else {
+ sb.append('"').append(arg.replace("\"", "\\\"")).append('"');
+ }
+ }
+ return sb.toString();
+ }
+
/**
* Builds the JSON string to pass as {@code --mcp-config} to the Claude CLI.
*/
diff --git a/src/test/java/io/github/nbclaudecodegui/process/ClaudeProcessTest.java b/src/test/java/io/github/nbclaudecodegui/process/ClaudeProcessTest.java
index ee679571..849ef7ac 100644
--- a/src/test/java/io/github/nbclaudecodegui/process/ClaudeProcessTest.java
+++ b/src/test/java/io/github/nbclaudecodegui/process/ClaudeProcessTest.java
@@ -3,6 +3,7 @@
import com.pty4j.PtyProcess;
import io.github.nbclaudecodegui.settings.ClaudeProfile;
import java.io.File;
+import java.util.List;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
@@ -170,6 +171,52 @@ void testCleanupRemovesApiKeyHelperButKeepsUserContent() {
assertTrue(cleaned.contains("myCustomKey"), "cleaned should preserve user content");
}
+ // -------------------------------------------------------------------------
+ // toShellCommand
+ // -------------------------------------------------------------------------
+
+ @Test
+ void testToShellCommandNoQuoting() {
+ List cmd = List.of("claude", "--continue");
+ // plain args need no quoting on either platform
+ assertEquals("claude --continue", ClaudeProcess.toShellCommand(cmd, false));
+ assertEquals("claude --continue", ClaudeProcess.toShellCommand(cmd, true));
+ }
+
+ @Test
+ void testToShellCommandQuotesJsonArgUnix() {
+ String json = ClaudeProcess.buildMcpConfigJson(28991);
+ List cmd = List.of("claude", "--mcp-config", json, "--continue");
+ String result = ClaudeProcess.toShellCommand(cmd, false);
+ // Expected: claude --mcp-config "{\"mcpServers\":...}" --continue
+ assertTrue(result.startsWith("claude --mcp-config \""), "should quote JSON arg");
+ assertTrue(result.endsWith("\" --continue"), "should close quote before next arg");
+ String inner = result.substring("claude --mcp-config \"".length(),
+ result.lastIndexOf("\" --continue"));
+ // On Unix all " inside must be escaped as \"
+ for (int i = 0; i < inner.length(); i++) {
+ if (inner.charAt(i) == '"') {
+ assertTrue(i > 0 && inner.charAt(i - 1) == '\\',
+ "unescaped double-quote at position " + i + " in: " + inner);
+ }
+ }
+ }
+
+ @Test
+ void testToShellCommandQuotesJsonArgWindows() {
+ String json = ClaudeProcess.buildMcpConfigJson(28991);
+ List cmd = List.of("claude", "--mcp-config", json, "--continue");
+ String result = ClaudeProcess.toShellCommand(cmd, true);
+ // Expected: claude --mcp-config "{""mcpServers"":...}" --continue
+ assertTrue(result.startsWith("claude --mcp-config \""), "should quote JSON arg");
+ assertTrue(result.endsWith("\" --continue"), "should close quote before next arg");
+ String inner = result.substring("claude --mcp-config \"".length(),
+ result.lastIndexOf("\" --continue"));
+ // On Windows all " inside must be doubled as ""
+ assertFalse(inner.contains("\\\""), "Windows quoting must not use backslash-escape");
+ assertTrue(inner.contains("\"\""), "Windows quoting must double double-quotes");
+ }
+
// -------------------------------------------------------------------------
// buildMcpConfigJson
// -------------------------------------------------------------------------