diff --git a/CHANGELOG.md b/CHANGELOG.md
index 92db04e9..0ef03a0f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,4 @@
+- Fixed diff panel appearing for file edits despite bypassPermissions or acceptEdits mode when Claude's working directory was a subdirectory of the session root
- Fixed occasional ConcurrentModificationException when the screen-poll timer reads the terminal buffer concurrently with PTY writes
- Fixed tab title showing "Claude Code" for sessions opened in non-project directories — now shows the directory name
- Added OpenAI-compatible proxy: profiles can now route Claude Code through any OpenAI-compatible API provider (OpenAI, Azure, Groq, DeepSeek, Ollama, local models, etc.)
diff --git a/pom.xml b/pom.xml
index e31a8c64..d8beefbd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
io.github.nbplugins
netbeans-plugin-claude-code-gui
- 1.2.10-SNAPSHOT
+ 1.2.11-SNAPSHOT
nbm
Netbeans Plugin 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/nbplugins/claudecodegui/mcp/NetBeansMCPHandler.java b/src/main/java/io/github/nbplugins/claudecodegui/mcp/NetBeansMCPHandler.java
index f14e104c..4cf6e68e 100644
--- a/src/main/java/io/github/nbplugins/claudecodegui/mcp/NetBeansMCPHandler.java
+++ b/src/main/java/io/github/nbplugins/claudecodegui/mcp/NetBeansMCPHandler.java
@@ -526,10 +526,12 @@ public CompletableFuture handlePreToolUse(String hookJson) {
return CompletableFuture.completedFuture(hookAllowJson());
}
- // cwd from hook JSON == confirmedDirectory of the session that launched Claude
+ // cwd from hook JSON == Claude's current working directory (may be a subdirectory of session root)
String cwd = hook.has("cwd") ? hook.get("cwd").asText() : null;
EditMode editMode = getEditModeForCwd(cwd);
- LOGGER.info("handlePreToolUse: editMode=" + editMode + " cwd=" + cwd);
+ // Session root is the key that matched in the registry (may differ from hook cwd when Claude cd'd into a subdir)
+ String sessionRoot = getSessionRootForCwd(cwd);
+ LOGGER.info("handlePreToolUse: editMode=" + editMode + " cwd=" + cwd + " sessionRoot=" + sessionRoot);
String filePath = getFilePath(toolInput);
@@ -537,7 +539,7 @@ public CompletableFuture handlePreToolUse(String hookJson) {
// acceptEdits: auto-allow only if file is inside the session's confirmed directory
if (editMode == EditMode.BYPASS_PERMISSIONS
|| (editMode == EditMode.ACCEPT_EDITS
- && io.github.nbplugins.claudecodegui.ui.FileDiffOpener.isFileUnderDirectory(filePath, cwd))) {
+ && io.github.nbplugins.claudecodegui.ui.FileDiffOpener.isFileUnderDirectory(filePath, sessionRoot))) {
LOGGER.info(editMode.key() + " mode — auto-allowing: " + filePath);
return CompletableFuture.completedFuture(hookAllowJson());
}
@@ -749,8 +751,44 @@ private static java.util.Optional findSessionByCwd(String cwd)
*/
private static EditMode getEditModeForCwd(String cwd) {
if (cwd == null) return EditMode.DEFAULT;
- EditMode mode = io.github.nbplugins.claudecodegui.model.ClaudeSessionModel.EDIT_MODE_REGISTRY.get(cwd);
- return mode != null ? mode : EditMode.DEFAULT;
+ // Exact match (common fast path)
+ EditMode exact = io.github.nbplugins.claudecodegui.model.ClaudeSessionModel.EDIT_MODE_REGISTRY.get(cwd);
+ if (exact != null) return exact;
+ // Prefix match: Claude may have cd'd into a subdirectory of the session root.
+ // Find the longest registered key that is an ancestor of the hook's cwd.
+ java.nio.file.Path cwdPath = java.nio.file.Path.of(cwd);
+ EditMode best = EditMode.DEFAULT;
+ int bestLen = -1;
+ for (var entry : io.github.nbplugins.claudecodegui.model.ClaudeSessionModel.EDIT_MODE_REGISTRY.entrySet()) {
+ java.nio.file.Path keyPath = java.nio.file.Path.of(entry.getKey());
+ if (cwdPath.startsWith(keyPath) && entry.getKey().length() > bestLen) {
+ best = entry.getValue();
+ bestLen = entry.getKey().length();
+ }
+ }
+ return best;
+ }
+
+ /**
+ * Returns the session root directory (registry key) that best matches {@code cwd},
+ * using the same longest-prefix logic as {@link #getEditModeForCwd}.
+ * Used to evaluate the {@code acceptEdits} directory boundary against the session root
+ * rather than Claude's current working directory (which may be a subdirectory).
+ */
+ private static String getSessionRootForCwd(String cwd) {
+ if (cwd == null) return null;
+ if (io.github.nbplugins.claudecodegui.model.ClaudeSessionModel.EDIT_MODE_REGISTRY.containsKey(cwd)) return cwd;
+ java.nio.file.Path cwdPath = java.nio.file.Path.of(cwd);
+ String best = cwd;
+ int bestLen = -1;
+ for (String key : io.github.nbplugins.claudecodegui.model.ClaudeSessionModel.EDIT_MODE_REGISTRY.keySet()) {
+ java.nio.file.Path keyPath = java.nio.file.Path.of(key);
+ if (cwdPath.startsWith(keyPath) && key.length() > bestLen) {
+ best = key;
+ bestLen = key.length();
+ }
+ }
+ return best;
}
/**
diff --git a/src/test/java/io/github/nbplugins/claudecodegui/mcp/NetBeansMCPHandlerHookTest.java b/src/test/java/io/github/nbplugins/claudecodegui/mcp/NetBeansMCPHandlerHookTest.java
index d94bc3be..8091231a 100644
--- a/src/test/java/io/github/nbplugins/claudecodegui/mcp/NetBeansMCPHandlerHookTest.java
+++ b/src/test/java/io/github/nbplugins/claudecodegui/mcp/NetBeansMCPHandlerHookTest.java
@@ -118,6 +118,37 @@ void noRegistryEntryDoesNotAutoAllow() throws Exception {
"Without a registry entry the handler must not auto-allow, got: " + result);
}
+ // -------------------------------------------------------------------------
+ // bypassPermissions — hook cwd is a subdirectory of the registered session root
+ // -------------------------------------------------------------------------
+
+ @Test
+ void bypassPermissionsAutoAllowsWhenHookCwdIsSubdirectory() throws Exception {
+ // Session was started at CWD; Claude cd'd into a subdirectory and sends hook with that cwd
+ ClaudeSessionModel.EDIT_MODE_REGISTRY.put(CWD, EditMode.BYPASS_PERMISSIONS);
+ String subCwd = CWD + "/submodules/Kotlin";
+
+ CompletableFuture future = handler.handlePreToolUse(editJsonWithCwd(FILE_INSIDE, subCwd));
+
+ assertTrue(future.isDone(), "Future must be completed immediately in bypassPermissions mode even with subdirectory cwd");
+ String result = future.get(1, TimeUnit.SECONDS);
+ assertTrue(result.contains("\"allow\""),
+ "bypassPermissions must auto-allow when hook cwd is a subdirectory of registered session root, got: " + result);
+ }
+
+ @Test
+ void acceptEditsAutoAllowsInsideProjectWhenHookCwdIsSubdirectory() throws Exception {
+ ClaudeSessionModel.EDIT_MODE_REGISTRY.put(CWD, EditMode.ACCEPT_EDITS);
+ String subCwd = CWD + "/submodules/Kotlin";
+
+ CompletableFuture future = handler.handlePreToolUse(editJsonWithCwd(FILE_INSIDE, subCwd));
+
+ assertTrue(future.isDone(), "Future must be completed immediately for file inside project when hook cwd is subdirectory");
+ String result = future.get(1, TimeUnit.SECONDS);
+ assertTrue(result.contains("\"allow\""),
+ "acceptEdits must auto-allow files inside project even when hook cwd is a subdirectory, got: " + result);
+ }
+
// -------------------------------------------------------------------------
// Non-file-edit tools are always auto-allowed
// -------------------------------------------------------------------------
@@ -137,9 +168,13 @@ void nonFileEditToolAutoAllowed() throws Exception {
// -------------------------------------------------------------------------
private static String editJson(String filePath) {
+ return editJsonWithCwd(filePath, CWD);
+ }
+
+ private static String editJsonWithCwd(String filePath, String cwd) {
return "{\"tool_name\":\"Edit\","
+ "\"tool_input\":{\"file_path\":\"" + filePath + "\","
+ "\"old_string\":\"x\",\"new_string\":\"y\"},"
- + "\"cwd\":\"" + CWD + "\"}";
+ + "\"cwd\":\"" + cwd + "\"}";
}
}