diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0ef03a0f..cc95fb21 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,4 @@
+- Added "Auto Mode" to the edit-mode selector (Claude Code 2.1.83+): file edits in auto mode are auto-approved; when auto mode is selected on an older Claude Code version, the plugin falls back to Bypass Permissions or Accept on Edit
- 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
diff --git a/pom.xml b/pom.xml
index d8beefbd..72fe8ecf 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
io.github.nbplugins
netbeans-plugin-claude-code-gui
- 1.2.11-SNAPSHOT
+ 1.2.12-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/controller/ClaudeSessionController.java b/src/main/java/io/github/nbplugins/claudecodegui/controller/ClaudeSessionController.java
index 2335e3cf..bdd835ad 100644
--- a/src/main/java/io/github/nbplugins/claudecodegui/controller/ClaudeSessionController.java
+++ b/src/main/java/io/github/nbplugins/claudecodegui/controller/ClaudeSessionController.java
@@ -716,6 +716,10 @@ public void onEditModeComboChanged(EditMode newMode) {
* {@code targetMode}, or until 5 attempts are exhausted.
* 5 attempts cover the full 4-mode cycle plus one extra in case of lag.
*
+ *
When {@code targetMode} is {@link EditMode#AUTO} and the mode cannot be
+ * reached (older Claude Code that does not support auto mode), falls back to
+ * {@link EditMode#BYPASS_PERMISSIONS} if available, then {@link EditMode#ACCEPT_EDITS}.
+ *
*
Runs on a daemon thread; sets lifecycle to WORKING at start and
* READY on completion.
*
@@ -729,25 +733,13 @@ private void sendShiftTabsUntilMode(EditMode targetMode) {
try {
LOG.info("sendShiftTabsUntilMode: target=" + targetMode
+ " currentModel=" + model.getEditMode());
- for (int attempt = 0; attempt < 5; attempt++) {
- connector.write(new byte[]{0x1b, '[', 'Z'});
- Thread.sleep(200);
- Optional detected =
- screenContentDetector.detectEditMode(screenLines.get());
- LOG.fine("sendShiftTabsUntilMode: attempt=" + attempt
- + " detected=" + detected.map(EditMode::key).orElse("(empty)"));
- if (detected.isPresent() && detected.get() == targetMode) {
- LOG.fine("sendShiftTabsUntilMode: reached target on attempt=" + attempt);
- return;
- }
- // DEFAULT mode has no idle-screen marker — empty detection means we are in default
- if (targetMode == EditMode.DEFAULT && detected.isEmpty()) {
- LOG.fine("sendShiftTabsUntilMode: no mode marker → treating as default on attempt=" + attempt);
- return;
- }
+ if (trySendShiftTabsUntilMode(targetMode)) return;
+ // AUTO mode not found — older CC doesn't support it; try fallbacks
+ if (targetMode == EditMode.AUTO) {
+ LOG.warning("sendShiftTabsUntilMode: auto mode not available, trying fallbacks");
+ if (trySendShiftTabsUntilMode(EditMode.BYPASS_PERMISSIONS)) return;
+ trySendShiftTabsUntilMode(EditMode.ACCEPT_EDITS);
}
- LOG.warning("sendShiftTabsUntilMode: did not reach " + targetMode
- + " after 5 attempts");
} catch (IOException | InterruptedException ex) {
LOG.warning("sendShiftTabsUntilMode failed: " + ex.getMessage());
} finally {
@@ -759,6 +751,33 @@ private void sendShiftTabsUntilMode(EditMode targetMode) {
t.start();
}
+ /**
+ * Inner loop: sends up to 5 Shift+Tab presses and returns {@code true} when
+ * {@code targetMode} is detected on screen, {@code false} if not reached.
+ */
+ private boolean trySendShiftTabsUntilMode(EditMode targetMode)
+ throws IOException, InterruptedException {
+ for (int attempt = 0; attempt < 5; attempt++) {
+ connector.write(new byte[]{0x1b, '[', 'Z'});
+ Thread.sleep(200);
+ Optional detected =
+ screenContentDetector.detectEditMode(screenLines.get());
+ LOG.fine("trySendShiftTabsUntilMode: target=" + targetMode + " attempt=" + attempt
+ + " detected=" + detected.map(EditMode::key).orElse("(empty)"));
+ if (detected.isPresent() && detected.get() == targetMode) {
+ LOG.fine("trySendShiftTabsUntilMode: reached " + targetMode + " on attempt=" + attempt);
+ return true;
+ }
+ // DEFAULT mode has no idle-screen marker — empty detection means we are in default
+ if (targetMode == EditMode.DEFAULT && detected.isEmpty()) {
+ LOG.fine("trySendShiftTabsUntilMode: no mode marker → treating as default on attempt=" + attempt);
+ return true;
+ }
+ }
+ LOG.warning("trySendShiftTabsUntilMode: did not reach " + targetMode + " after 5 attempts");
+ return false;
+ }
+
// -------------------------------------------------------------------------
// Model switching
// -------------------------------------------------------------------------
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 4cf6e68e..c0c4cf04 100644
--- a/src/main/java/io/github/nbplugins/claudecodegui/mcp/NetBeansMCPHandler.java
+++ b/src/main/java/io/github/nbplugins/claudecodegui/mcp/NetBeansMCPHandler.java
@@ -535,15 +535,15 @@ public CompletableFuture handlePreToolUse(String hookJson) {
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
+ // AUTO or higher (BYPASS_PERMISSIONS): auto-allow regardless of location
+ // ACCEPT_EDITS or higher (but below AUTO): auto-allow only inside the session root
+ if (editMode.ordinal() >= EditMode.AUTO.ordinal()
+ || (editMode.ordinal() >= EditMode.ACCEPT_EDITS.ordinal()
&& io.github.nbplugins.claudecodegui.ui.FileDiffOpener.isFileUnderDirectory(filePath, sessionRoot))) {
LOGGER.info(editMode.key() + " mode — auto-allowing: " + filePath);
return CompletableFuture.completedFuture(hookAllowJson());
}
- // plan / ask / acceptEdits-outside: fall through to show diff dialog
+ // PLAN / DEFAULT / ACCEPT_EDITS-outside: fall through to show diff dialog
String before = computeBefore(toolInput, filePath);
String after = computeAfter(toolName, toolInput, before);
diff --git a/src/main/java/io/github/nbplugins/claudecodegui/model/EditMode.java b/src/main/java/io/github/nbplugins/claudecodegui/model/EditMode.java
index 0a89f1e1..1c3c7806 100644
--- a/src/main/java/io/github/nbplugins/claudecodegui/model/EditMode.java
+++ b/src/main/java/io/github/nbplugins/claudecodegui/model/EditMode.java
@@ -9,30 +9,43 @@
* identifier in {@link ClaudeSessionModel#EDIT_MODE_REGISTRY}, in serialized
* settings, and in logging.
*
+ * Constants are ordered from least to most permissive, so
+ * ordinal comparisons ({@code >=}, {@code <=}) express permission thresholds naturally.
+ *
*
Screen-text mapping (used by {@code ScreenContentDetector.detectEditMode}):
*
* - {@link #PLAN} — bottom line contains {@code "plan mode"} or {@code "plan-mode"}
+ * - {@link #DEFAULT} — bottom line starts with {@code " esc to interrupt"}
+ * (two leading spaces)
* - {@link #ACCEPT_EDITS} — bottom line contains {@code "accept edits"}
+ * - {@link #AUTO} — bottom line contains {@code "auto mode"};
+ * available on Max/Team/Enterprise/API plans (Claude Code 2.1.83+)
* - {@link #BYPASS_PERMISSIONS} — bottom line contains {@code "bypass permissions"};
* appears when Claude was launched with {@code --dangerously-skip-permissions}
- * - {@link #DEFAULT} — bottom line starts with {@code " esc to interrupt"}
- * (two leading spaces)
*
*/
public enum EditMode {
- /** Ask-on-edit — shows a diff dialog before every file change. */
- DEFAULT("default"),
-
- /** Plan mode — Claude proposes a plan before executing. */
+ /** Plan mode — Claude proposes a plan; no file changes are auto-allowed. */
PLAN("plan"),
+ /** Default (ask-on-edit) — shows a diff dialog before every file change. */
+ DEFAULT("default"),
+
/**
* Accept-edits — auto-allows file changes inside the session's working
* directory; shows a diff dialog for files outside.
*/
ACCEPT_EDITS("acceptEdits"),
+ /**
+ * Auto mode — a classifier auto-approves safe tool calls including all file
+ * edits, blocking only dangerous operations (mass deletion, data exfiltration, etc.).
+ * Available on Max/Team/Enterprise/API plans (Claude Code 2.1.83+).
+ * Detected on screen as {@code ⏵⏵ auto mode on (shift+tab to cycle)}.
+ */
+ AUTO("auto"),
+
/**
* Bypass-permissions — auto-allows all file changes regardless of location.
* Active when Claude was launched with {@code --dangerously-skip-permissions}.
diff --git a/src/main/java/io/github/nbplugins/claudecodegui/process/ScreenContentDetector.java b/src/main/java/io/github/nbplugins/claudecodegui/process/ScreenContentDetector.java
index ae000c00..8c408c9c 100644
--- a/src/main/java/io/github/nbplugins/claudecodegui/process/ScreenContentDetector.java
+++ b/src/main/java/io/github/nbplugins/claudecodegui/process/ScreenContentDetector.java
@@ -489,6 +489,9 @@ public Optional detectEditMode(List lines) {
if (lower.contains("bypass permissions")) {
return Optional.of(EditMode.BYPASS_PERMISSIONS);
}
+ if (lower.contains("auto mode")) {
+ return Optional.of(EditMode.AUTO);
+ }
}
// " esc to interrupt" with two leading spaces is the reliable Ask/default-mode signal.
// Without leading spaces the screen is in an unknown / transitioning state.
diff --git a/src/main/java/io/github/nbplugins/claudecodegui/ui/ClaudeSessionTab.java b/src/main/java/io/github/nbplugins/claudecodegui/ui/ClaudeSessionTab.java
index 10bb86ab..767bdc89 100644
--- a/src/main/java/io/github/nbplugins/claudecodegui/ui/ClaudeSessionTab.java
+++ b/src/main/java/io/github/nbplugins/claudecodegui/ui/ClaudeSessionTab.java
@@ -120,9 +120,9 @@ public class ClaudeSessionTab extends TopComponent
// Edit mode constants
// -------------------------------------------------------------------------
- /** Base edit modes always shown in the combo (indices 0–2). */
- static final String[] EDIT_MODE_LABELS = {"Plan Mode", "Ask on Edit", "Accept on Edit"};
- static final EditMode[] EDIT_MODE_VALUES = {EditMode.PLAN, EditMode.DEFAULT, EditMode.ACCEPT_EDITS};
+ /** Base edit modes always shown in the combo (indices 0–3). */
+ static final String[] EDIT_MODE_LABELS = {"Plan Mode", "Ask on Edit", "Accept on Edit", "Auto Mode"};
+ static final EditMode[] EDIT_MODE_VALUES = {EditMode.PLAN, EditMode.DEFAULT, EditMode.ACCEPT_EDITS, EditMode.AUTO};
/** Label / value for the 4th entry added dynamically when bypass-permissions is detected. */
private static final String BYPASS_LABEL = "Bypass Permissions";
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 8091231a..51267bfe 100644
--- a/src/test/java/io/github/nbplugins/claudecodegui/mcp/NetBeansMCPHandlerHookTest.java
+++ b/src/test/java/io/github/nbplugins/claudecodegui/mcp/NetBeansMCPHandlerHookTest.java
@@ -72,6 +72,33 @@ void bypassPermissionsAutoAllowsFileOutsideProject() throws Exception {
"bypassPermissions must allow files outside project, got: " + result);
}
+ // -------------------------------------------------------------------------
+ // auto mode — auto-allow regardless of location
+ // -------------------------------------------------------------------------
+
+ @Test
+ void autoModeAutoAllowsFileInsideProject() throws Exception {
+ ClaudeSessionModel.EDIT_MODE_REGISTRY.put(CWD, EditMode.AUTO);
+
+ CompletableFuture future = handler.handlePreToolUse(editJson(FILE_INSIDE));
+
+ assertTrue(future.isDone(), "Future must be completed immediately in auto mode");
+ String result = future.get(1, TimeUnit.SECONDS);
+ assertTrue(result.contains("\"allow\""), "auto mode must allow files inside project, got: " + result);
+ }
+
+ @Test
+ void autoModeAutoAllowsFileOutsideProject() throws Exception {
+ ClaudeSessionModel.EDIT_MODE_REGISTRY.put(CWD, EditMode.AUTO);
+
+ CompletableFuture future = handler.handlePreToolUse(editJson(FILE_OUTSIDE));
+
+ assertTrue(future.isDone(), "Future must be completed immediately in auto mode");
+ String result = future.get(1, TimeUnit.SECONDS);
+ assertTrue(result.contains("\"allow\""),
+ "auto mode must allow files outside project (unconditional), got: " + result);
+ }
+
// -------------------------------------------------------------------------
// acceptEdits — auto-allow only inside cwd
// -------------------------------------------------------------------------
diff --git a/src/test/java/io/github/nbplugins/claudecodegui/process/ScreenContentDetectorTest.java b/src/test/java/io/github/nbplugins/claudecodegui/process/ScreenContentDetectorTest.java
index 48736400..91fb7525 100644
--- a/src/test/java/io/github/nbplugins/claudecodegui/process/ScreenContentDetectorTest.java
+++ b/src/test/java/io/github/nbplugins/claudecodegui/process/ScreenContentDetectorTest.java
@@ -213,6 +213,27 @@ void detectEditModeDefaultWhenEscToInterruptLeadingSpaces() {
assertEquals(EditMode.DEFAULT, result.get());
}
+ @org.junit.jupiter.api.Test
+ void detectEditModeAutoMode() {
+ // Screen text from Claude Code 2.1.83+ with auto mode active
+ List lines = new ArrayList<>(List.of(
+ "Some output",
+ "⏵⏵ auto mode on (shift+tab to cycle) · ← for agents"
+ ));
+ for (int i = 0; i < 10; i++) lines.add("");
+ java.util.Optional result = detector.detectEditMode(lines);
+ assertTrue(result.isPresent());
+ assertEquals(EditMode.AUTO, result.get());
+ }
+
+ @org.junit.jupiter.api.Test
+ void detectEditModeAutoModeCaseInsensitive() {
+ List lines = List.of("Some output", "Auto Mode on | esc to interrupt");
+ java.util.Optional result = detector.detectEditMode(lines);
+ assertTrue(result.isPresent());
+ assertEquals(EditMode.AUTO, result.get());
+ }
+
@org.junit.jupiter.api.Test
void detectEditModeEmptyWhenNullOrEmpty() {
assertTrue(detector.detectEditMode(null).isEmpty());
diff --git a/src/test/java/io/github/nbplugins/claudecodegui/ui/ClaudeSessionTabBypassComboTest.java b/src/test/java/io/github/nbplugins/claudecodegui/ui/ClaudeSessionTabBypassComboTest.java
index 51f89b52..96380231 100644
--- a/src/test/java/io/github/nbplugins/claudecodegui/ui/ClaudeSessionTabBypassComboTest.java
+++ b/src/test/java/io/github/nbplugins/claudecodegui/ui/ClaudeSessionTabBypassComboTest.java
@@ -8,9 +8,12 @@
/**
* Regression tests for the bypass-permissions combo entry:
- * once detected, the 4th "Bypass Permissions" entry must remain visible
+ * once detected, the 5th "Bypass Permissions" entry must remain visible
* when the user switches to another edit mode, and must only be removed
* when a new session starts (STARTING lifecycle).
+ *
+ * Base combo has 4 static entries: Plan Mode, Ask on Edit, Accept on Edit, Auto Mode.
+ * Bypass Permissions is added dynamically at index 4 when detected.
*/
class ClaudeSessionTabBypassComboTest {
@@ -18,40 +21,44 @@ class ClaudeSessionTabBypassComboTest {
void bypassEntry_appearsWhenBypassDetected() {
ClaudeSessionTab tab = new ClaudeSessionTab();
assertEquals(ClaudeSessionTab.EDIT_MODE_VALUES.length, tab.editModeItemCount(),
- "Initially combo should have 3 items");
+ "Initially combo should have 4 items (Plan, Ask on Edit, Accept on Edit, Auto Mode)");
tab.onEditModeChanged(EditMode.BYPASS_PERMISSIONS);
assertEquals(ClaudeSessionTab.EDIT_MODE_VALUES.length + 1, tab.editModeItemCount(),
- "After BYPASS_PERMISSIONS, combo should have 4 items");
+ "After BYPASS_PERMISSIONS, combo should have 5 items");
}
@Test
void bypassEntry_persistsAfterSwitchingToAnotherMode() {
ClaudeSessionTab tab = new ClaudeSessionTab();
tab.onEditModeChanged(EditMode.BYPASS_PERMISSIONS);
- assertEquals(4, tab.editModeItemCount());
+ assertEquals(5, tab.editModeItemCount());
tab.onEditModeChanged(EditMode.PLAN);
- assertEquals(4, tab.editModeItemCount(),
+ assertEquals(5, tab.editModeItemCount(),
"Bypass entry must remain after switching to PLAN mode");
tab.onEditModeChanged(EditMode.DEFAULT);
- assertEquals(4, tab.editModeItemCount(),
+ assertEquals(5, tab.editModeItemCount(),
"Bypass entry must remain after switching to DEFAULT mode");
tab.onEditModeChanged(EditMode.ACCEPT_EDITS);
- assertEquals(4, tab.editModeItemCount(),
+ assertEquals(5, tab.editModeItemCount(),
"Bypass entry must remain after switching to ACCEPT_EDITS mode");
+
+ tab.onEditModeChanged(EditMode.AUTO);
+ assertEquals(5, tab.editModeItemCount(),
+ "Bypass entry must remain after switching to AUTO mode");
}
@Test
void bypassEntry_removedOnNewSession() {
ClaudeSessionTab tab = new ClaudeSessionTab();
tab.onEditModeChanged(EditMode.BYPASS_PERMISSIONS);
- assertEquals(4, tab.editModeItemCount());
+ assertEquals(5, tab.editModeItemCount());
tab.onLifecycleChanged(SessionLifecycle.STARTING);
- assertEquals(3, tab.editModeItemCount(),
+ assertEquals(4, tab.editModeItemCount(),
"Bypass entry must be removed when a new session starts");
}
@@ -61,7 +68,8 @@ void bypassEntry_notAddedWithoutBypassSession() {
tab.onEditModeChanged(EditMode.PLAN);
tab.onEditModeChanged(EditMode.DEFAULT);
tab.onEditModeChanged(EditMode.ACCEPT_EDITS);
- assertEquals(3, tab.editModeItemCount(),
+ tab.onEditModeChanged(EditMode.AUTO);
+ assertEquals(4, tab.editModeItemCount(),
"Bypass entry must not appear in a non-bypass session");
}
@@ -70,7 +78,7 @@ void bypassEntry_notAddedTwice() {
ClaudeSessionTab tab = new ClaudeSessionTab();
tab.onEditModeChanged(EditMode.BYPASS_PERMISSIONS);
tab.onEditModeChanged(EditMode.BYPASS_PERMISSIONS);
- assertEquals(4, tab.editModeItemCount(),
+ assertEquals(5, tab.editModeItemCount(),
"Bypass entry must not be added twice");
}
}