Skip to content
This repository was archived by the owner on Feb 20, 2026. It is now read-only.

Commit 782efeb

Browse files
jlaustillclaude
andcommitted
Add --no-web terminal mode and fix bypass permissions stream handling
- Add --no-web CLI option for terminal-only mode (no browser) - Implement terminal attach with proper PTY size handling and resize support - Fix bypass permissions setup to consume stream data (prevents hanging) - Use settings.local.json with correct format for bypass mode config Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent d2854e3 commit 782efeb

4 files changed

Lines changed: 117 additions & 37 deletions

File tree

src/cli.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ program
125125
"Start with 'claude' or 'bash' shell",
126126
/^(claude|bash)$/i,
127127
)
128+
.option("--no-web", "Disable web UI (use terminal attach)")
128129
.action(async (options) => {
129130
console.log(chalk.blue("🚀 Starting new Claude Sandbox container..."));
130131

@@ -136,6 +137,7 @@ program
136137
config.targetBranch = options.branch;
137138
config.remoteBranch = options.remoteBranch;
138139
config.prNumber = options.pr;
140+
config.useWebUI = options.web !== false;
139141
if (options.shell) {
140142
config.defaultShell = options.shell.toLowerCase();
141143
}

src/container.ts

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -851,18 +851,7 @@ exec claude --dangerously-skip-permissions' > /start-claude.sh && \\
851851
try {
852852
console.log(chalk.blue("• Configuring bypass permissions mode..."));
853853

854-
// Create or update settings.json to enable bypass permissions mode
855-
// This skips the "By proceeding, you accept all responsibility" confirmation prompt
856-
const settingsContent = JSON.stringify(
857-
{
858-
permissions: {
859-
defaultMode: "bypassPermissions",
860-
},
861-
},
862-
null,
863-
2,
864-
);
865-
854+
// The settings need to have defaultMode at the root level per Claude Code docs
866855
const setupExec = await container.exec({
867856
Cmd: [
868857
"/bin/bash",
@@ -871,26 +860,34 @@ exec claude --dangerously-skip-permissions' > /start-claude.sh && \\
871860
# Ensure .claude directory exists
872861
mkdir -p /home/claude/.claude &&
873862
874-
# Check if settings.json exists
875-
if [ -f /home/claude/.claude/settings.json ]; then
876-
# Merge with existing settings using jq if available, otherwise replace
863+
# Update settings.local.json to set bypass permissions mode
864+
# Using settings.local.json as it takes precedence for user preferences
865+
SETTINGS_FILE="/home/claude/.claude/settings.local.json"
866+
867+
if [ -f "$SETTINGS_FILE" ]; then
868+
# File exists - try to merge with jq, fallback to simple approach
877869
if command -v jq &> /dev/null; then
878-
# Use jq to merge settings, preserving existing values
879-
jq '.permissions.defaultMode = "bypassPermissions"' /home/claude/.claude/settings.json > /tmp/settings.json.tmp &&
880-
mv /tmp/settings.json.tmp /home/claude/.claude/settings.json
870+
jq '. + {"defaultMode": "bypassPermissions"}' "$SETTINGS_FILE" > /tmp/settings.tmp && mv /tmp/settings.tmp "$SETTINGS_FILE"
881871
else
882-
# No jq, just overwrite (existing settings will be lost)
883-
echo '${settingsContent}' > /home/claude/.claude/settings.json
872+
# No jq - use python if available
873+
python3 -c "
874+
import json
875+
with open('$SETTINGS_FILE', 'r') as f:
876+
data = json.load(f)
877+
data['defaultMode'] = 'bypassPermissions'
878+
with open('$SETTINGS_FILE', 'w') as f:
879+
json.dump(data, f, indent=2)
880+
" 2>/dev/null || echo '{"defaultMode": "bypassPermissions"}' > "$SETTINGS_FILE"
884881
fi
885882
else
886883
# Create new settings file
887-
echo '${settingsContent}' > /home/claude/.claude/settings.json
884+
echo '{"defaultMode": "bypassPermissions"}' > "$SETTINGS_FILE"
888885
fi &&
889886
890887
# Fix permissions
891888
chown -R claude:claude /home/claude/.claude &&
892889
chmod 700 /home/claude/.claude &&
893-
chmod 600 /home/claude/.claude/settings.json
890+
chmod 600 "$SETTINGS_FILE"
894891
`,
895892
],
896893
AttachStdout: true,
@@ -899,8 +896,9 @@ exec claude --dangerously-skip-permissions' > /start-claude.sh && \\
899896

900897
const stream = await setupExec.start({});
901898

902-
// Wait for completion
899+
// Wait for completion (must consume stream data for it to end)
903900
await new Promise<void>((resolve, reject) => {
901+
stream.on("data", () => {}); // Consume data to allow stream to end
904902
stream.on("end", resolve);
905903
stream.on("error", reject);
906904
});

src/index.ts

Lines changed: 93 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -156,25 +156,33 @@ export class ClaudeSandbox {
156156
await this.gitMonitor.start(branchName);
157157
console.log(chalk.blue("✓ Git monitoring started"));
158158

159-
// Always launch web UI
160-
this.webServer = new WebUIServer(this.docker);
159+
// Launch web UI or attach to terminal directly
160+
if (this.config.useWebUI !== false) {
161+
this.webServer = new WebUIServer(this.docker);
161162

162-
// Pass repo info to web server
163-
this.webServer.setRepoInfo(process.cwd(), branchName);
163+
// Pass repo info to web server
164+
this.webServer.setRepoInfo(process.cwd(), branchName);
164165

165-
const webUrl = await this.webServer.start();
166+
const webUrl = await this.webServer.start();
166167

167-
// Open browser to the web UI with container ID
168-
const fullUrl = `${webUrl}?container=${containerId}`;
169-
await this.webServer.openInBrowser(fullUrl);
168+
// Open browser to the web UI with container ID
169+
const fullUrl = `${webUrl}?container=${containerId}`;
170+
await this.webServer.openInBrowser(fullUrl);
170171

171-
console.log(chalk.green(`\n✓ Web UI available at: ${fullUrl}`));
172-
console.log(
173-
chalk.yellow("Keep this terminal open to maintain the session"),
174-
);
172+
console.log(chalk.green(`\n✓ Web UI available at: ${fullUrl}`));
173+
console.log(
174+
chalk.yellow("Keep this terminal open to maintain the session"),
175+
);
175176

176-
// Keep the process running
177-
await new Promise(() => {}); // This will keep the process alive
177+
// Keep the process running
178+
await new Promise(() => {}); // This will keep the process alive
179+
} else {
180+
// Terminal mode - attach directly to container
181+
console.log(chalk.green("\n✓ Attaching to container terminal..."));
182+
console.log(chalk.yellow("Press Ctrl+P, Ctrl+Q to detach\n"));
183+
184+
await this.attachToContainer(containerId);
185+
}
178186
} catch (error) {
179187
console.error(chalk.red("Error:"), error);
180188
throw error;
@@ -267,6 +275,77 @@ export class ClaudeSandbox {
267275
await this.webServer.stop();
268276
}
269277
}
278+
279+
private async attachToContainer(containerId: string): Promise<void> {
280+
const container = this.docker.getContainer(containerId);
281+
282+
// Get current terminal size
283+
const getTerminalSize = () => ({
284+
h: process.stdout.rows || 24,
285+
w: process.stdout.columns || 80,
286+
});
287+
288+
const termSize = getTerminalSize();
289+
290+
// Execute the startup script in an interactive session
291+
const dockerExec = await container.exec({
292+
Cmd: ["/bin/bash", "-l", "-c", "/home/claude/start-session.sh"],
293+
AttachStdin: true,
294+
AttachStdout: true,
295+
AttachStderr: true,
296+
Tty: true,
297+
});
298+
299+
const stream = await dockerExec.start({
300+
hijack: true,
301+
stdin: true,
302+
Tty: true,
303+
});
304+
305+
// Set initial terminal size
306+
await dockerExec.resize(termSize);
307+
308+
// Handle terminal resize events
309+
const resizeHandler = async () => {
310+
try {
311+
await dockerExec.resize(getTerminalSize());
312+
} catch {
313+
// Ignore resize errors (exec might have ended)
314+
}
315+
};
316+
process.stdout.on("resize", resizeHandler);
317+
318+
// Set up raw mode for proper terminal handling
319+
if (process.stdin.isTTY) {
320+
process.stdin.setRawMode(true);
321+
}
322+
process.stdin.resume();
323+
324+
// Pipe streams
325+
process.stdin.pipe(stream);
326+
stream.pipe(process.stdout);
327+
328+
// Handle stream end
329+
stream.on("end", async () => {
330+
process.stdout.off("resize", resizeHandler);
331+
if (process.stdin.isTTY) {
332+
process.stdin.setRawMode(false);
333+
}
334+
process.stdin.pause();
335+
await this.cleanup();
336+
process.exit(0);
337+
});
338+
339+
// Handle Ctrl+C gracefully
340+
process.on("SIGINT", async () => {
341+
process.stdout.off("resize", resizeHandler);
342+
if (process.stdin.isTTY) {
343+
process.stdin.setRawMode(false);
344+
}
345+
await this.cleanup();
346+
process.exit(0);
347+
});
348+
}
270349
}
271350

272351
export * from "./types";

src/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export interface SandboxConfig {
2626
remoteBranch?: string;
2727
prNumber?: string;
2828
dockerSocketPath?: string;
29+
useWebUI?: boolean;
2930
}
3031

3132
export interface Credentials {

0 commit comments

Comments
 (0)