From 9c84118695caf1292bc62003db40af2cec0ac85b Mon Sep 17 00:00:00 2001 From: Oleg Samarin Date: Sat, 23 May 2026 22:07:55 +0300 Subject: [PATCH] Fixed diff panel appearing despite bypassPermissions/acceptEdits when Claude cd'd into subdir When Claude's working directory at edit time is a subdirectory of the session root (e.g. session started at /proj, Claude cd'd into /proj/submodules/Kotlin), the EDIT_MODE_REGISTRY lookup by exact key missed and fell back to EditMode.DEFAULT, causing the diff panel to appear even in bypassPermissions and acceptEdits modes. Fix: getEditModeForCwd() now falls back to a longest-prefix match when the exact key is not found. Added getSessionRootForCwd() using the same logic so the acceptEdits directory boundary check uses the session root rather than Claude's current cwd. Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 1 + pom.xml | 2 +- .../claudecodegui/mcp/NetBeansMCPHandler.java | 48 +++++++++++++++++-- .../mcp/NetBeansMCPHandlerHookTest.java | 37 +++++++++++++- 4 files changed, 81 insertions(+), 7 deletions(-) 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 + "\"}"; } }