Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.)
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>io.github.nbplugins</groupId>
<artifactId>netbeans-plugin-claude-code-gui</artifactId>
<version>1.2.10-SNAPSHOT</version>
<version>1.2.11-SNAPSHOT</version>
<packaging>nbm</packaging>
<name>Netbeans Plugin Claude Code GUI</name>
<description>NetBeans plugin that provides a GUI for Claude Code CLI — run claude code sessions directly inside the IDE.</description>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -526,18 +526,20 @@ public CompletableFuture<String> 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);

// bypassPermissions: auto-allow everything regardless of location
// 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());
}
Expand Down Expand Up @@ -749,8 +751,44 @@ private static java.util.Optional<ClaudeSessionTab> 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;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> 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<String> 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
// -------------------------------------------------------------------------
Expand All @@ -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 + "\"}";
}
}
Loading